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

Issue:
Rust binding



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index e0b55a78..581a403c 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -79,6 +79,28 @@ jobs:
       - name: CMake test
         working-directory: ${{github.workspace}}/examples/c/build
         run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test
+  
+  linux-wrappers-rust:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Get dependencies
+        run: |
+          sudo apt-get install cargo rustc
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: Cargo test
+        working-directory: ${{github.workspace}}/wrappers/rust/regorust/
+        run: cargo test
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
 
   windows:
     runs-on: windows-latest
@@ -122,4 +144,96 @@ jobs:
 
       - name: CMake test
         working-directory: ${{github.workspace}}/examples/c/build
-        run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test
\ No newline at end of file
+        run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test
+
+  windows-wrappers-rust:
+    runs-on: windows-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch "+refs/pull/*/merge:refs/remotes/pull/*"
+
+      - name: Cargo test
+        working-directory: ${{github.workspace}}/wrappers/rust/regorust
+        run: cargo test
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+
+  macos:
+    runs-on: macos-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Get dependencies
+        run: |
+          brew update && brew install ninja
+
+      - name: CMake config
+        run: cmake -B ${{github.workspace}}/build --preset release-clang-opa
+
+      - name: CMake build
+        working-directory: ${{github.workspace}}/build
+        run: ninja
+
+      - name: CMake test
+        working-directory: ${{github.workspace}}/build
+        run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test
+
+  macos-wrappers-c:
+    runs-on: macos-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Get dependencies
+        run: |
+          brew update && brew install ninja
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: CMake config
+        working-directory: ${{github.workspace}}/examples/c
+        run: cmake -B ${{github.workspace}}/examples/c/build --preset release-clang
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+
+      - name: CMake build
+        working-directory: ${{github.workspace}}/examples/c/build
+        run: ninja install
+
+      - name: CMake test
+        working-directory: ${{github.workspace}}/examples/c/build
+        run: ctest -V --build-config Release --timeout 120 --output-on-failure -T Test
+  
+  macos-wrappers-rust:
+    runs-on: macos-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Get dependencies
+        run: |
+          brew update && brew install rust
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: Cargo test
+        working-directory: ${{github.workspace}}/wrappers/rust/regorust/
+        run: cargo test
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
index 99b619d9..dda73a4a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,23 @@
 # Changelog
 
+## 2023-09-10 - Version 0.3.4
+Adding a Rust wrapper.
+
+**New Features**
+- `wrappers/rust/` contains a Rust crate, `regorust`, which wraps the library
+  and exposes a Rust API.
+
+**Breaking changes**
+- _C API_: The `regoAddInput*` functions are now named `regoSetInput*` and can
+  be used multiple times on the same interpreter.
+- _C API_: `regoGet/SetStrictBuiltInErrors` has been added.
+- The public headers have been reduced to just two files:
+  * `rego.hh` which contains the C++ API
+  * `rego_c.h` which contains the C API
+- The `rego.hh` header is now a C++ header and will not compile in C.
+- Many definitions have been moved to the (non-public) `internal.hh` header,
+  and code which relied on having access to those definitions will break.
+
 ## 2023-09-07 - Version 0.3.3
 Adding a C API.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c7ef096d..e7258f75 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -72,8 +72,8 @@ option(REGOCPP_COPY_EXAMPLES "Specifies whether to copy the examples to the inst
 
 FetchContent_Declare(
   trieste
-  GIT_REPOSITORY https://github.com/microsoft/trieste
-  GIT_TAG        140e55346b5b6f367538790c98d9d7543376b515
+  GIT_REPOSITORY https://github.com/matajoh/trieste
+  GIT_TAG        7bf503b81db295ddbddd8d0010f5d820057ec8a8
 )
 
 set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 
@@ -131,6 +131,12 @@ if(REGOCPP_OPA_TESTS)
   endif()
 endif()
 
+find_program(CCACHE_PATH ccache)
+if(CCACHE_PATH)
+  message("Using ccache")
+  set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PATH})
+endif()
+
 set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)
 
 # -------------------- Walk the subdirectories --------------------
diff --git a/VERSION b/VERSION
index 87a08711..448a0fa1 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.3.3
\ No newline at end of file
+0.3.4
\ No newline at end of file
diff --git a/examples/c/main.c b/examples/c/main.c
index 25fb0610..56368c06 100644
--- a/examples/c/main.c
+++ b/examples/c/main.c
@@ -258,8 +258,6 @@ int main(int argc, char** argv)
     regoSetLoggingEnabled(true);
   }
 
-  regoSetExecutable(rego, argv[0]);
-
   for (data_index = 0; data_index < config.data_files_count; data_index++)
   {
     const char* file = config.data_files[data_index];
@@ -279,7 +277,7 @@ int main(int argc, char** argv)
 
   if (config.input_file != NULL)
   {
-    err = regoAddInputJSONFile(rego, config.input_file);
+    err = regoSetInputJSONFile(rego, config.input_file);
     if (err == REGO_ERROR)
     {
       goto error;
diff --git a/include/rego/args.h b/include/rego/args.h
deleted file mode 100644
index 5818b2d5..00000000
--- a/include/rego/args.h
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "value.h"
-
-#include <set>
-#include <vector>
-
-namespace rego
-{
-  /**
-   * The Args class provides an interable interface over the Cartesian product
-   * of all sets of arguments to a function. It will contain N sets of argument
-   * sources, and will produce s_0 * s_1 * ... * s_n arguments, where s_i is the
-   * size of the i-th argument source.
-   */
-  class Args
-  {
-  public:
-    Args();
-    void push_back_source(const Values& values);
-    void mark_invalid(const std::set<Value>& active) const;
-    void mark_invalid_except(const std::set<Value>& active) const;
-    Values at(std::size_t index) const;
-    std::size_t size() const;
-    std::string str() const;
-    std::size_t source_size() const;
-    const Values& source_at(std::size_t index) const;
-    Args subargs(std::size_t start) const;
-    friend std::ostream& operator<<(std::ostream& os, const Args& args);
-
-  private:
-    std::vector<Values> m_values;
-    std::vector<size_t> m_stride;
-    std::size_t m_size;
-  };
-}
\ No newline at end of file
diff --git a/include/rego/bigint.h b/include/rego/bigint.h
deleted file mode 100644
index 324e34af..00000000
--- a/include/rego/bigint.h
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <trieste/source.h>
-
-namespace rego
-{
-  using namespace trieste;
-  /**
-   * This is a basic, non-optimized implementation of a big integer using
-   * strings. In most circumstances this would be considerably slower than other
-   * approaches, but given the way in which Trieste nodes store their content as
-   * Location objects into a source document, and this class operates over those
-   * Locations, it is actual quite efficient when compared to parsing and
-   * serializing the Location into a vector of unsigned longs.
-   */
-  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
deleted file mode 100644
index 47943b16..00000000
--- a/include/rego/builtins.h
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <functional>
-#include <string>
-#include <trieste/ast.h>
-#include <trieste/source.h>
-
-namespace rego
-{
-  using namespace trieste;
-  struct BuiltInDef;
-  using BuiltIn = std::shared_ptr<BuiltInDef>;
-  using BuiltInBehavior = Node (*)(const Nodes&);
-
-  const std::size_t AnyArity = std::numeric_limits<std::size_t>::max();
-  struct BuiltInDef
-  {
-    Location name;
-    std::size_t arity;
-    BuiltInBehavior behavior;
-    static BuiltIn create(
-      const Location& name, std::size_t arity, BuiltInBehavior behavior);
-  };
-
-  class BuiltIns
-  {
-  public:
-    BuiltIns() noexcept;
-    bool is_builtin(const Location& name) const;
-    Node call(const Location& name, const Nodes& args) const;
-    BuiltIns& register_builtin(const BuiltIn& built_in);
-    const BuiltIn& at(const Location& name) const;
-    bool strict_errors() const;
-    BuiltIns& strict_errors(bool strict_errors);
-
-    template <typename T>
-    BuiltIns& register_builtins(const T& built_ins)
-    {
-      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> m_builtins;
-    bool m_strict_errors;
-  };
-}
\ No newline at end of file
diff --git a/include/rego/encoding.h b/include/rego/encoding.h
deleted file mode 100644
index d18fc1c8..00000000
--- a/include/rego/encoding.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma once
-
-#include <trieste/source.h>
-
-namespace rego
-{
-  std::string to_json(
-    const trieste::Node& node, bool sort = false, bool rego_set = true);
-}
\ No newline at end of file
diff --git a/include/rego/errors.h b/include/rego/errors.h
deleted file mode 100644
index ede7f705..00000000
--- a/include/rego/errors.h
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <string>
-#include <trieste/ast.h>
-
-namespace rego
-{
-  using namespace trieste;
-
-  const std::string EvalTypeError = "eval_type_error";
-  const std::string EvalBuiltInError = "eval_builtin_error";
-  const std::string RegoTypeError = "rego_type_error";
-  const std::string EvalConflictError = "eval_conflict_error";
-  const std::string WellFormedError = "wellformed_error";
-  const std::string RuntimeError = "runtime_error";
-
-  Node err(
-    NodeRange& r,
-    const std::string& msg,
-    const std::string& code = WellFormedError);
-
-  Node err(
-    Node node,
-    const std::string& msg,
-    const std::string& code = WellFormedError);
-}
\ No newline at end of file
diff --git a/include/rego/helpers.h b/include/rego/helpers.h
deleted file mode 100644
index 9a4fcf5e..00000000
--- a/include/rego/helpers.h
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "bigint.h"
-#include "encoding.h"
-#include "tokens.h"
-
-namespace rego
-{
-  const inline auto ScalarToken =
-    T(JSONInt) / T(JSONFloat) / T(JSONTrue) / T(JSONFalse) / T(JSONNull);
-  const inline auto ArithToken =
-    T(Add) / T(Subtract) / T(Multiply) / T(Divide) / T(Modulo);
-  const inline auto ArithInfixArg = T(Expr) / T(NumTerm) / T(Ref) /
-    T(UnaryExpr) / T(ArithInfix) / T(RefTerm) / T(ExprCall);
-  const inline auto BinInfixArg = T(Expr) / T(Ref) / T(RefTerm) / T(ExprCall) /
-    T(Set) / T(SetCompr) / T(BinInfix);
-  const auto inline RefArg = T(RefArgDot) / T(RefArgBrack);
-  const inline auto BoolToken = T(Equals) / T(NotEquals) / T(GreaterThan) /
-    T(LessThan) / T(GreaterThanOrEquals) / T(LessThanOrEquals);
-  const inline auto TermToken = T(Var) / T(Ref) / T(Array) / T(Object) /
-    T(Set) / T(ArrayCompr) / T(ObjectCompr) / T(SetCompr);
-  const inline auto StringToken = T(JSONString) / T(RawString);
-  const inline auto ExprToken = T(Term) / ArithToken / BoolToken / StringToken /
-    T(Expr) / ScalarToken / TermToken / T(JSONString) / T(Array) / T(Set) /
-    T(Object) / T(Paren) / T(Not) / T(Dot) / T(And) / T(Or) / T(ExprCall);
-  const auto inline MembershipToken = ScalarToken / T(JSONString) /
-    T(RawString) / T(Var) / T(Object) / T(Array) / T(Set) / T(Dot) / T(Paren) /
-    ArithToken / BoolToken / T(And) / T(Or) / T(ExprCall);
-  const auto inline RuleRefToken = T(Var) / T(Dot) / T(Array);
-
-  inline const std::set<std::string> Keywords(
-    {"if", "in", "contains", "every"});
-
-  inline const std::set<Token> RuleTypes(
-    {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
-
-  struct UnwrapResult
-  {
-    Node node;
-    bool success;
-  };
-
-  class UnwrapOpt
-  {
-  public:
-    UnwrapOpt(std::size_t index);
-    bool exclude_got() const;
-    UnwrapOpt& exclude_got(bool exclude_got);
-    bool specify_number() const;
-    UnwrapOpt& specify_number(bool specify_number);
-    const std::string& code() const;
-    UnwrapOpt& code(const std::string& value);
-    const std::string& pre() const;
-    UnwrapOpt& pre(const std::string& value);
-    const std::string& message() const;
-    UnwrapOpt& message(const std::string& value);
-    const std::string& func() const;
-    UnwrapOpt& func(const std::string& value);
-    const std::vector<Token>& types() const;
-    UnwrapOpt& types(const std::vector<Token>& value);
-    UnwrapOpt& type(const Token& value);
-
-    Node unwrap(const Nodes& args) const;
-
-  private:
-    bool m_exclude_got;
-    bool m_specify_number;
-    std::string m_code;
-    std::string m_prefix;
-    std::string m_message;
-    std::string m_func;
-    std::vector<Token> m_types;
-    std::size_t m_index;
-  };
-
-  bool all_alnum(const std::string_view& str);
-  bool contains_local(const Node& node);
-  bool contains_ref(const Node& node);
-  Node concat_refs(const Node& lhs, const Node& rhs);
-  std::string flatten_ref(const Node& ref);
-  bool is_in(const Node& node, const std::set<Token>& token);
-  bool in_query(const Node& node);
-  bool is_constant(const Node& node);
-  bool is_instance(const Node& value, const std::set<Token>& types);
-  bool is_falsy(const Node& node);
-  bool is_truthy(const Node& node);
-  bool is_undefined(const Node& node);
-  std::string strip_quotes(const std::string_view& str);
-  BigInt get_int(const Node& node);
-  double get_double(const Node& node);
-  std::string get_string(const Node& node);
-  bool get_bool(const Node& node);
-  std::string type_name(const Token& type, bool specify_number = false);
-  std::string type_name(const Node& node, bool specify_number = false);
-  Node unwrap_arg(const Nodes& args, const UnwrapOpt& options);
-  UnwrapResult unwrap(const Node& term, const std::set<Token>& types);
-}
\ No newline at end of file
diff --git a/include/rego/interpreter.h b/include/rego/interpreter.h
deleted file mode 100644
index af0741d5..00000000
--- a/include/rego/interpreter.h
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "builtins.h"
-#include "rego_c.h"
-
-#include <string>
-#include <trieste/parse.h>
-#include <trieste/wf.h>
-
-namespace rego
-{
-  using namespace trieste;
-  class Interpreter
-  {
-  public:
-    Interpreter(bool disable_well_formed_checks = false);
-    ~Interpreter();
-    void add_module_file(const std::filesystem::path& path);
-    void add_module(const std::string& name, const std::string& contents);
-    void add_data_json_file(const std::filesystem::path& path);
-    void add_data_json(const std::string& json);
-    void add_data(const Node& node);
-    void add_input_json_file(const std::filesystem::path& path);
-    void add_input_json(const std::string& json);
-    void add_input(const Node& node);
-    std::string query(const std::string& query_expr) const;
-    Node raw_query(const std::string& query_expr) const;
-    Interpreter& debug_path(const std::filesystem::path& prefix);
-    const std::filesystem::path& debug_path() const;
-    Interpreter& debug_enabled(bool enabled);
-    bool debug_enabled() const;
-    Interpreter& well_formed_checks_enabled(bool enabled);
-    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:
-    friend const char* ::regoGetError(regoInterpreter* rego);
-    friend void setError(regoInterpreter* rego, const std::string& error);
-    friend regoOutput* ::regoQuery(
-      regoInterpreter* rego, const char* query_expr);
-
-    std::string output_to_string(const Node& output) const;
-    void write_ast(
-      std::size_t index, const std::string& pass, const Node& ast) const;
-    Node get_errors(const Node& ast) const;
-    Parse m_parser;
-    wf::Wellformed m_wf_parser;
-    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;
-
-    std::string m_c_error;
-  };
-}
\ No newline at end of file
diff --git a/include/rego/log.h b/include/rego/log.h
deleted file mode 100644
index 878158ff..00000000
--- a/include/rego/log.h
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <functional>
-#include <iostream>
-#include <map>
-#include <string>
-#include <trieste/source.h>
-#include <vector>
-
-namespace rego
-{
-  std::ostream& operator<<(std::ostream& os, const std::set<Location>& locs);
-  std::ostream& operator<<(std::ostream& os, const std::vector<Location>& locs);
-
-  struct Logger
-  {
-    static bool enabled;
-    static std::string indent;
-
-    static inline void increase_print_indent()
-    {
-      indent += "  ";
-    }
-
-    static inline void decrease_print_indent()
-    {
-      indent = indent.substr(0, indent.size() - 2);
-    }
-
-    template <typename T>
-    static inline void print(const T& value)
-    {
-      if (enabled)
-      {
-        std::cout << value << std::endl;
-      }
-    }
-
-    template <typename T, typename... Types>
-    static void print(T head, Types... tail)
-    {
-      if (enabled)
-      {
-        std::cout << head;
-        print(tail...);
-      }
-    }
-
-    template <typename T>
-    static inline void print_vector_inline(const std::vector<T>& values)
-    {
-      if (enabled)
-      {
-        std::cout << indent << "[";
-        std::string sep = "";
-        for (auto& value : values)
-        {
-          std::cout << sep << value;
-          sep = ", ";
-        }
-        std::cout << "]" << std::endl;
-      }
-    }
-
-    template <typename T, typename P>
-    static inline void print_vector_custom(
-      const std::vector<T>& values, P (*transform)(const T&))
-    {
-      if (enabled)
-      {
-        for (auto& value : values)
-        {
-          print(indent, transform(value));
-        }
-      }
-    }
-
-    template <typename K, typename V>
-    static inline void print_map_values(const std::map<K, V>& values)
-    {
-      if (enabled)
-      {
-        print(indent, "{");
-        for (auto& [_, value] : values)
-        {
-          print(indent, "  ", value);
-        }
-        print(indent, "}");
-      }
-    }
-  };
-
-// clang-format off
-#  define LOG(...) Logger::print(Logger::indent, __VA_ARGS__)
-#  define LOG_HEADER(message, header) Logger::print(Logger::indent, (header), (message), (header))
-#  define LOG_VECTOR(vector) Logger::print_vector_inline((vector))
-#  define LOG_VECTOR_CUSTOM(vector, transform) Logger::print_vector_custom((vector), (transform))
-#  define LOG_MAP_VALUES(map) Logger::print_map_values((map))
-#  define LOG_INDENT() Logger::increase_print_indent()
-#  define LOG_UNINDENT() Logger::decrease_print_indent()
-  // clang-format on
-}
\ No newline at end of file
diff --git a/include/rego/passes.h b/include/rego/passes.h
deleted file mode 100644
index 41ec6284..00000000
--- a/include/rego/passes.h
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "builtins.h"
-
-#include <trieste/pass.h>
-
-namespace rego
-{
-  PassDef input_data();
-  PassDef modules();
-  PassDef imports();
-  PassDef keywords();
-  PassDef lists();
-  PassDef ifs();
-  PassDef elses();
-  PassDef rules();
-  PassDef build_calls();
-  PassDef membership();
-  PassDef build_refs();
-  PassDef structure();
-  PassDef strings();
-  PassDef merge_data();
-  PassDef lift_refheads();
-  PassDef symbols();
-  PassDef replace_argvals();
-  PassDef lift_query();
-  PassDef constants();
-  PassDef explicit_enums();
-  PassDef body_locals(const BuiltIns& builtins);
-  PassDef value_locals(const BuiltIns& builtins);
-  PassDef rules_to_compr();
-  PassDef compr();
-  PassDef absolute_refs();
-  PassDef merge_modules();
-  PassDef datarule();
-  PassDef skips();
-  PassDef unary();
-  PassDef multiply_divide();
-  PassDef add_subtract();
-  PassDef comparison();
-  PassDef assign(const BuiltIns& builtins);
-  PassDef skip_refs(const BuiltIns& builtins);
-  PassDef simple_refs();
-  PassDef init();
-  PassDef implicit_enums();
-  PassDef rulebody();
-  PassDef lift_to_rule();
-  PassDef functions();
-  PassDef unify(const BuiltIns& builtins);
-  PassDef query();
-}
\ No newline at end of file
diff --git a/include/rego/rego.h b/include/rego/rego.h
deleted file mode 100644
index 78afe12e..00000000
--- a/include/rego/rego.h
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "encoding.h"
-#include "interpreter.h"
-#include "log.h"
-#include "tokens.h"
-
-#include <trieste/driver.h>
-
-namespace rego
-{
-  using namespace trieste;
-
-  Parse parser();
-  Driver& driver(const BuiltIns& builtins);
-  using PassCheck = std::tuple<std::string, Pass, const wf::Wellformed*>;
-  std::vector<PassCheck> passes(const BuiltIns& builtins);
-  Node version();
-}
\ No newline at end of file
diff --git a/include/rego/wf.h b/include/rego/rego.hh
similarity index 54%
rename from include/rego/wf.h
rename to include/rego/rego.hh
index 7285325e..a580de54 100644
--- a/include/rego/wf.h
+++ b/include/rego/rego.hh
@@ -3,11 +3,215 @@
 
 #pragma once
 
-#include "tokens.h"
-#include "trieste/token.h"
+#include <trieste/driver.h>
+#include "rego_c.h"
 
 namespace rego
 {
+  using namespace trieste;
+
+  // grammar tokens
+  inline const auto Module = TokenDef("module", flag::symtab);
+  inline const auto Package = TokenDef("package");
+  inline const auto Policy = TokenDef("policy");
+  inline const auto Rule = TokenDef("rule", flag::symtab);
+  inline const auto RuleHead = TokenDef("rule-head");
+  inline const auto RuleHeadComp = TokenDef("rule-head-comp");
+  inline const auto RuleHeadFunc = TokenDef("rule-head-func");
+  inline const auto RuleHeadSet = TokenDef("rule-head-set");
+  inline const auto RuleHeadObj = TokenDef("rule-head-obj");
+  inline const auto RuleArgs = TokenDef("rule-args");
+  inline const auto Query = TokenDef("query");
+  inline const auto Literal = TokenDef("literal");
+  inline const auto Expr = TokenDef("expr");
+  inline const auto ExprInfix = TokenDef("expr-infix");
+  inline const auto ExprCall = TokenDef("expr-call");
+  inline const auto ExprEvery = TokenDef("expr-every");
+  inline const auto UnaryExpr = TokenDef("unary-expr");
+  inline const auto Term = TokenDef("term");
+  inline const auto InfixOperator = TokenDef("infix-operator");
+  inline const auto BoolOperator = TokenDef("bool-operator");
+  inline const auto ArithOperator = TokenDef("arith-operator");
+  inline const auto AssignOperator = TokenDef("assign-operator");
+  inline const auto Ref = TokenDef("ref");
+  inline const auto RefArgBrack = TokenDef("ref-arg-brack");
+  inline const auto RefArgDot = TokenDef("ref-arg-dot");
+  inline const auto Var = TokenDef("var", flag::print);
+  inline const auto Scalar = TokenDef("scalar");
+  inline const auto String = TokenDef("string");
+  inline const auto Array = TokenDef("array");
+  inline const auto Object = TokenDef("object");
+  inline const auto Set = TokenDef("set");
+  inline const auto EmptySet = TokenDef("empty-set");
+  inline const auto ObjectItem = TokenDef("object-item");
+  inline const auto RawString = TokenDef("raw-string", flag::print);
+  inline const auto JSONString = TokenDef("STRING", flag::print);
+  inline const auto JSONInt = TokenDef("INT", flag::print);
+  inline const auto JSONFloat = TokenDef("FLOAT", flag::print);
+  inline const auto JSONTrue = TokenDef("true");
+  inline const auto JSONFalse = TokenDef("false");
+  inline const auto JSONNull = TokenDef("null");
+  inline const auto Equals = TokenDef("==");
+  inline const auto NotEquals = TokenDef("!=");
+  inline const auto LessThan = TokenDef("<");
+  inline const auto GreaterThan = TokenDef(">");
+  inline const auto LessThanOrEquals = TokenDef("<=");
+  inline const auto GreaterThanOrEquals = TokenDef(">=");
+  inline const auto Add = TokenDef("+");
+  inline const auto Subtract = TokenDef("-");
+  inline const auto Multiply = TokenDef("*");
+  inline const auto Divide = TokenDef("/");
+  inline const auto Modulo = TokenDef("%");
+  inline const auto And = TokenDef("&");
+  inline const auto Or = TokenDef("|");
+  inline const auto Assign = TokenDef(":=");
+  inline const auto Unify = TokenDef("=");
+  inline const auto Default = TokenDef("default");
+  inline const auto Some = TokenDef("some");
+  inline const auto SomeDecl = TokenDef("some-decl");
+  inline const auto SomeExpr = TokenDef("some-expr");
+  inline const auto If = TokenDef("if");
+  inline const auto IsIn = TokenDef("in");
+  inline const auto Contains = TokenDef("contains");
+  inline const auto Else = TokenDef("else");
+  inline const auto As = TokenDef("as");
+  inline const auto With = TokenDef("with");
+  inline const auto Every = TokenDef("every");
+  inline const auto ArrayCompr = TokenDef("array-compr");
+  inline const auto ObjectCompr = TokenDef("object-compr");
+  inline const auto SetCompr = TokenDef("set-compr");
+  inline const auto Membership = TokenDef("membership");
+
+  // intermediate tokens
+  inline const auto UnifyBody = TokenDef("unify-body");
+  inline const auto RuleRef = TokenDef("rule-ref");
+  inline const auto RefHead = TokenDef("ref-head");
+  inline const auto RefArgSeq = TokenDef("ref-arg-seq");
+  inline const auto ModuleSeq = TokenDef("module-seq");
+  inline const auto DataSeq = TokenDef("data-seq");
+  inline const auto DataItemSeq = TokenDef("data-item-seq");
+  inline const auto DataItem = TokenDef("data-item", flag::lookup);
+  inline const auto DataRule = TokenDef("data-rule", flag::lookup);
+  inline const auto DataObject = TokenDef("data-object");
+  inline const auto DataObjectItem = TokenDef("data-object-item");
+  inline const auto DataArray = TokenDef("data-array");
+  inline const auto DataSet = TokenDef("data-set");
+  inline const auto DataModule = TokenDef("data-module", flag::lookup);
+  inline const auto Submodule =
+    TokenDef("submodule", flag::lookdown | flag::lookup);
+  inline const auto DataTerm = TokenDef("data-term");
+  inline const auto ObjectItemSeq = TokenDef("object-item-seq");
+  inline const auto ImportSeq = TokenDef("import-seq");
+  inline const auto VarSeq = TokenDef("var-seq");
+  inline const auto ElseSeq = TokenDef("else-seq");
+  inline const auto WithSeq = TokenDef("with-seq");
+  inline const auto SkipSeq = TokenDef("skip-seq");
+  inline const auto ItemSeq = TokenDef("item-seq");
+  inline const auto ItemSeq1 = TokenDef("item-seq-1");
+
+  // data and input
+  inline const auto Input = TokenDef("input", flag::lookup);
+  inline const auto Data = TokenDef("data", flag::lookup);
+
+  // fused
+  inline const auto RuleComp = TokenDef(
+    "rule-comp",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleFunc = TokenDef(
+    "rule-func",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleSet = TokenDef(
+    "rule-set",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleObj = TokenDef(
+    "rule-obj",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto DefaultRule =
+    TokenDef("default-rule", flag::lookup | flag::lookdown);
+  inline const auto Local = TokenDef("local", flag::lookup | flag::shadowing);
+  inline const auto ArithInfix = TokenDef("arith-infix");
+  inline const auto BinInfix = TokenDef("bin-infix");
+  inline const auto BoolInfix = TokenDef("bool-infix");
+  inline const auto AssignInfix = TokenDef("assign-infix");
+  inline const auto ArgVar = TokenDef("arg-var", flag::lookup);
+  inline const auto ArgVal = TokenDef("arg-val");
+  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");
+  inline const auto Rego = TokenDef("rego", flag::symtab);
+  inline const auto Math = TokenDef("math");
+  inline const auto Op = TokenDef("op");
+  inline const auto Comparison = TokenDef("comparison");
+  inline const auto Function = TokenDef("function");
+  inline const auto Arg = TokenDef("arg");
+  inline const auto ArgSeq = TokenDef("arg-seq");
+  inline const auto Val = TokenDef("value");
+  inline const auto Id = TokenDef("id");
+  inline const auto Head = TokenDef("head");
+  inline const auto Tail = TokenDef("tail");
+  inline const auto Head1 = TokenDef("head-1");
+  inline const auto Tail1 = TokenDef("tail-1");
+  inline const auto Head2 = TokenDef("head-2");
+  inline const auto Tail2 = TokenDef("tail-2");
+  inline const auto Lhs = TokenDef("lhs");
+  inline const auto Rhs = TokenDef("rhs");
+  inline const auto LhsVars = TokenDef("lhs-vars");
+  inline const auto RhsVars = TokenDef("rhs-vars");
+  inline const auto Key = TokenDef("key", flag::print);
+  inline const auto RefTerm = TokenDef("ref-term");
+  inline const auto NumTerm = TokenDef("num-term");
+  inline const auto ArithArg = TokenDef("arith-arg");
+  inline const auto BinArg = TokenDef("bin-arg");
+  inline const auto BoolArg = TokenDef("bool-arg");
+  inline const auto AssignArg = TokenDef("assign-arg");
+  inline const auto RuleHeadType = TokenDef("rule-head-type");
+  inline const auto UnifyExpr = TokenDef("unify-expr");
+  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");
+  inline const auto Binding = TokenDef("binding");
+  inline const auto Body = TokenDef("body");
+  inline const auto Import =
+    TokenDef("import", flag::lookdown | flag::lookup | flag::shadowing);
+  inline const auto Keyword =
+    TokenDef("keyword", flag::lookdown | flag::lookup);
+  inline const auto Idx = TokenDef("idx");
+  inline const auto Idx1 = TokenDef("idx-1");
+  inline const auto Skip = TokenDef("skip", flag::lookup);
+  inline const auto NestedBody = TokenDef("nested-body", flag::symtab);
+  inline const auto BuiltInHook = TokenDef("builtin-hook", flag::lookup);
+  inline const auto Item = TokenDef("item");
+  inline const auto Item1 = TokenDef("item-1");
+  inline const auto Enumerate = TokenDef("enumerate");
+  inline const auto Compr = TokenDef("compr");
+  inline const auto Merge = TokenDef("merge");
+  inline const auto ImportRef = TokenDef("import-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");
+
+  // parser
+  inline const auto Brace = TokenDef("brace");
+  inline const auto Dot = TokenDef("dot");
+  inline const auto Colon = TokenDef("colon");
+  inline const auto Square = TokenDef("square");
+  inline const auto Paren = TokenDef("paren");
+  inline const auto Not = TokenDef("not");
+  inline const auto Comma = TokenDef("comma");
+  inline const auto Placeholder = TokenDef("_");
+
   using namespace wf::ops;
 
   inline const auto wf_json =
@@ -338,6 +542,10 @@ namespace rego
     ;
   // clang-format on
 
+  // clang-format off
+  inline const auto wf_pass_expand_imports = wf_pass_lift_query;
+  // clang-format on
+
   // clang-format off
   inline const auto wf_pass_constants =
     wf_pass_lift_query
@@ -568,4 +776,195 @@ namespace rego
     | (Top <<= (Binding | Term)++)
     ;
   // clang-format on
+
+  struct BuiltInDef;
+  using BuiltIn = std::shared_ptr<BuiltInDef>;
+  using BuiltInBehavior = Node (*)(const Nodes&);
+
+  const std::size_t AnyArity = std::numeric_limits<std::size_t>::max();
+  struct BuiltInDef
+  {
+    Location name;
+    std::size_t arity;
+    BuiltInBehavior behavior;
+    static BuiltIn create(
+      const Location& name, std::size_t arity, BuiltInBehavior behavior);
+  };
+
+  class BuiltIns
+  {
+  public:
+    BuiltIns() noexcept;
+    bool is_builtin(const Location& name) const;
+    Node call(const Location& name, const Nodes& args) const;
+    BuiltIns& register_builtin(const BuiltIn& built_in);
+    const BuiltIn& at(const Location& name) const;
+    bool strict_errors() const;
+    BuiltIns& strict_errors(bool strict_errors);
+
+    template <typename T>
+    BuiltIns& register_builtins(const T& built_ins)
+    {
+      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> m_builtins;
+    bool m_strict_errors;
+  };
+
+  /**
+   * This is a basic, non-optimized implementation of a big integer using
+   * strings. In most circumstances this would be considerably slower than other
+   * approaches, but given the way in which Trieste nodes store their content as
+   * Location objects into a source document, and this class operates over those
+   * Locations, it is actual quite efficient when compared to parsing and
+   * serializing the Location into a vector of unsigned longs.
+   */
+  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;
+  };
+
+  const std::string EvalTypeError = "eval_type_error";
+  const std::string EvalBuiltInError = "eval_builtin_error";
+  const std::string RegoTypeError = "rego_type_error";
+  const std::string EvalConflictError = "eval_conflict_error";
+  const std::string WellFormedError = "wellformed_error";
+  const std::string RuntimeError = "runtime_error";
+
+  Node err(
+    NodeRange& r,
+    const std::string& msg,
+    const std::string& code = WellFormedError);
+
+  Node err(
+    Node node,
+    const std::string& msg,
+    const std::string& code = WellFormedError);
+
+  Parse parser();
+  Driver& driver(const BuiltIns& builtins);
+  using PassCheck = std::tuple<std::string, Pass, const wf::Wellformed*>;
+  std::vector<PassCheck> passes(const BuiltIns& builtins);
+  Node version();
+
+  std::string to_json(
+    const trieste::Node& node, bool sort = false, bool rego_set = true);
+  void set_logging_enabled(bool enabled);
+
+class Interpreter
+  {
+  public:
+    Interpreter();
+    ~Interpreter();
+    void add_module_file(const std::filesystem::path& path);
+    void add_module(const std::string& name, const std::string& contents);
+    void add_data_json_file(const std::filesystem::path& path);
+    void add_data_json(const std::string& json);
+    void add_data(const Node& node);
+    void set_input_json_file(const std::filesystem::path& path);
+    void set_input_json(const std::string& json);
+    void set_input(const Node& node);
+    std::string query(const std::string& query_expr) const;
+    Node raw_query(const std::string& query_expr) const;
+    Interpreter& debug_path(const std::filesystem::path& prefix);
+    const std::filesystem::path& debug_path() const;
+    Interpreter& debug_enabled(bool enabled);
+    bool debug_enabled() const;
+    Interpreter& well_formed_checks_enabled(bool enabled);
+    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:
+    friend const char* ::regoGetError(regoInterpreter* rego);
+    friend void setError(regoInterpreter* rego, const std::string& error);
+    friend regoOutput* ::regoQuery(
+      regoInterpreter* rego, const char* query_expr);
+
+    void insert_module(const Node& module);
+    std::string output_to_string(const Node& output) const;
+    void write_ast(
+      std::size_t index, const std::string& pass, const Node& ast) const;
+    Node get_errors(const Node& ast) const;
+    Parse m_parser;
+    wf::Wellformed m_wf_parser;
+    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;
+
+    std::string m_c_error;
+  };
 }
\ No newline at end of file
diff --git a/include/rego/rego_c.h b/include/rego/rego_c.h
index 48c3ce88..c265832e 100644
--- a/include/rego/rego_c.h
+++ b/include/rego/rego_c.h
@@ -1,8 +1,8 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-#ifndef _REGOCPP_WRAPPER_H_
-#define _REGOCPP_WRAPPER_H_
+#ifndef _REGO_C_H_
+#define _REGO_C_H_
 
 #include "version.h"
 
@@ -39,6 +39,7 @@ typedef unsigned int regoSize;
 #define REGO_NODE_ERROR_MESSAGE 1801
 #define REGO_NODE_ERROR_AST 1802
 #define REGO_NODE_ERROR_CODE 1803
+#define REGO_NODE_ERROR_SEQ 1804
 
 #define REGO_NODE_INTERNAL 1999
 
@@ -55,16 +56,17 @@ extern "C"
     regoInterpreter* rego, const char* name, const char* contents);
   regoEnum regoAddDataJSONFile(regoInterpreter* rego, const char* path);
   regoEnum regoAddDataJSON(regoInterpreter* rego, const char* contents);
-  regoEnum regoAddInputJSONFile(regoInterpreter* rego, const char* path);
-  regoEnum regoAddInputJSON(regoInterpreter* rego, const char* contents);
+  regoEnum regoSetInputJSONFile(regoInterpreter* rego, const char* path);
+  regoEnum regoSetInputJSON(regoInterpreter* rego, const char* contents);
   void regoSetDebugEnabled(regoInterpreter* rego, regoBoolean enabled);
   regoBoolean regoGetDebugEnabled(regoInterpreter* rego);
-  void regoSetDebugPath(regoInterpreter* rego, const char* path);
+  regoEnum regoSetDebugPath(regoInterpreter* rego, const char* path);
   void regoSetWellFormedChecksEnabled(
     regoInterpreter* rego, regoBoolean enabled);
   regoBoolean regoGetWellFormedChecksEnabled(regoInterpreter* rego);
-  void regoSetExecutable(regoInterpreter* rego, const char* path);
   regoOutput* regoQuery(regoInterpreter* rego, const char* query_expr);
+  void regoSetStrictBuiltInErrors(regoInterpreter* rego, regoBoolean enabled);
+  regoBoolean regoGetStrictBuiltInErrors(regoInterpreter* rego);
   const char* regoGetError(regoInterpreter* rego);
 
   // Output functions
diff --git a/include/rego/resolver.h b/include/rego/resolver.h
deleted file mode 100644
index cbeb5e40..00000000
--- a/include/rego/resolver.h
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "bigint.h"
-#include "builtins.h"
-
-#include <optional>
-#include <string>
-
-namespace rego
-{
-  using namespace trieste;
-  using PrintNode = std::ostream& (*)(std::ostream&, const Node&);
-
-  class Resolver
-  {
-  public:
-    struct NodePrinter
-    {
-      Node node;
-      PrintNode printer;
-      std::string str() const;
-    };
-
-    static Node scalar(BigInt value);
-    static Node scalar(double value);
-    static Node scalar(bool value);
-    static Node scalar(const char* value);
-    static Node scalar(const std::string& value);
-    static Node scalar();
-    static Node term(BigInt value);
-    static Node term(double value);
-    static Node term(bool value);
-    static Node term(const char* value);
-    static Node term(const std::string& value);
-    static Node term();
-    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);
-    static NodePrinter expr_str(const Node& unifyexpr);
-    static NodePrinter enum_str(const Node& unifyexprenum);
-    static NodePrinter with_str(const Node& unifyexprwith);
-    static NodePrinter compr_str(const Node& unifyexprcompr);
-    static NodePrinter ref_str(const Node& ref);
-    static NodePrinter body_str(const Node& rego);
-    static NodePrinter not_str(const Node& rego);
-    static NodePrinter term_str(const Node& rego);
-    static NodePrinter rego_str(const Node& rego);
-    static Node negate(const Node& value);
-    static Node unary(const Node& value);
-    static Node arithinfix(const Node& op, const Node& lhs, const Node& rhs);
-    static Node bininfix(const Node& op, const Node& lhs, const Node& rhs);
-    static Node boolinfix(const Node& op, const Node& lhs, const Node& rhs);
-    static std::optional<Nodes> apply_access(
-      const Node& container, const Node& index);
-    static Node object(const Node& object_items, bool is_rule);
-    static Node array(const Node& array_members);
-    static Node set(const Node& set_members);
-    static Node set_intersection(const Node& lhs, const Node& rhs);
-    static Node set_union(const Node& lhs, const Node& rhs);
-    static Node set_difference(const Node& lhs, const Node& rhs);
-    static Nodes resolve_varseq(const Node& varseq);
-    static Nodes object_lookdown(const Node& object, const Node& query);
-    static Node inject_args(const Node& rulefunc, const Nodes& args);
-    static Node membership(
-      const Node& index, const Node& item, const Node& itemseq);
-    static Node membership(const Node& item, const Node& itemseq);
-    static std::vector<std::string> array_find(
-      const Node& array, const std::string& search);
-    static std::vector<std::string> object_find(
-      const Node& object, const std::string& search);
-    static Node to_term(const Node& value);
-    static void flatten_terms_into(const Node& termset, Node& terms);
-    static void flatten_items_into(const Node& termset, Node& terms);
-    static Node reduce_termset(const Node& termset);
-    static void insert_into_object(
-      Node& object, const std::string& path, const Node& value);
-  };
-
-  std::ostream& operator<<(
-    std::ostream& os, const Resolver::NodePrinter& printer);
-}
\ No newline at end of file
diff --git a/include/rego/tokens.h b/include/rego/tokens.h
deleted file mode 100644
index 9732f8be..00000000
--- a/include/rego/tokens.h
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <trieste/driver.h>
-
-namespace rego
-{
-  using namespace trieste;
-
-  // grammar tokens
-  inline const auto Module = TokenDef("module", flag::symtab);
-  inline const auto Package = TokenDef("package");
-  inline const auto Policy = TokenDef("policy");
-  inline const auto Rule = TokenDef("rule", flag::symtab);
-  inline const auto RuleHead = TokenDef("rule-head");
-  inline const auto RuleHeadComp = TokenDef("rule-head-comp");
-  inline const auto RuleHeadFunc = TokenDef("rule-head-func");
-  inline const auto RuleHeadSet = TokenDef("rule-head-set");
-  inline const auto RuleHeadObj = TokenDef("rule-head-obj");
-  inline const auto RuleArgs = TokenDef("rule-args");
-  inline const auto Query = TokenDef("query");
-  inline const auto Literal = TokenDef("literal");
-  inline const auto Expr = TokenDef("expr");
-  inline const auto ExprInfix = TokenDef("expr-infix");
-  inline const auto ExprCall = TokenDef("expr-call");
-  inline const auto ExprEvery = TokenDef("expr-every");
-  inline const auto UnaryExpr = TokenDef("unary-expr");
-  inline const auto Term = TokenDef("term");
-  inline const auto InfixOperator = TokenDef("infix-operator");
-  inline const auto BoolOperator = TokenDef("bool-operator");
-  inline const auto ArithOperator = TokenDef("arith-operator");
-  inline const auto AssignOperator = TokenDef("assign-operator");
-  inline const auto Ref = TokenDef("ref");
-  inline const auto RefArgBrack = TokenDef("ref-arg-brack");
-  inline const auto RefArgDot = TokenDef("ref-arg-dot");
-  inline const auto Var = TokenDef("var", flag::print);
-  inline const auto Scalar = TokenDef("scalar");
-  inline const auto String = TokenDef("string");
-  inline const auto Array = TokenDef("array");
-  inline const auto Object = TokenDef("object");
-  inline const auto Set = TokenDef("set");
-  inline const auto EmptySet = TokenDef("empty-set");
-  inline const auto ObjectItem = TokenDef("object-item");
-  inline const auto RawString = TokenDef("raw-string", flag::print);
-  inline const auto JSONString = TokenDef("STRING", flag::print);
-  inline const auto JSONInt = TokenDef("INT", flag::print);
-  inline const auto JSONFloat = TokenDef("FLOAT", flag::print);
-  inline const auto JSONTrue = TokenDef("true");
-  inline const auto JSONFalse = TokenDef("false");
-  inline const auto JSONNull = TokenDef("null");
-  inline const auto Equals = TokenDef("==");
-  inline const auto NotEquals = TokenDef("!=");
-  inline const auto LessThan = TokenDef("<");
-  inline const auto GreaterThan = TokenDef(">");
-  inline const auto LessThanOrEquals = TokenDef("<=");
-  inline const auto GreaterThanOrEquals = TokenDef(">=");
-  inline const auto Add = TokenDef("+");
-  inline const auto Subtract = TokenDef("-");
-  inline const auto Multiply = TokenDef("*");
-  inline const auto Divide = TokenDef("/");
-  inline const auto Modulo = TokenDef("%");
-  inline const auto And = TokenDef("&");
-  inline const auto Or = TokenDef("|");
-  inline const auto Assign = TokenDef(":=");
-  inline const auto Unify = TokenDef("=");
-  inline const auto Default = TokenDef("default");
-  inline const auto Some = TokenDef("some");
-  inline const auto SomeDecl = TokenDef("some-decl");
-  inline const auto SomeExpr = TokenDef("some-expr");
-  inline const auto If = TokenDef("if");
-  inline const auto IsIn = TokenDef("in");
-  inline const auto Contains = TokenDef("contains");
-  inline const auto Else = TokenDef("else");
-  inline const auto As = TokenDef("as");
-  inline const auto With = TokenDef("with");
-  inline const auto Every = TokenDef("every");
-  inline const auto ArrayCompr = TokenDef("array-compr");
-  inline const auto ObjectCompr = TokenDef("object-compr");
-  inline const auto SetCompr = TokenDef("set-compr");
-  inline const auto Membership = TokenDef("membership");
-
-  // intermediate tokens
-  inline const auto UnifyBody = TokenDef("unify-body");
-  inline const auto RuleRef = TokenDef("rule-ref");
-  inline const auto RefHead = TokenDef("ref-head");
-  inline const auto RefArgSeq = TokenDef("ref-arg-seq");
-  inline const auto ModuleSeq = TokenDef("module-seq");
-  inline const auto DataSeq = TokenDef("data-seq");
-  inline const auto DataItemSeq = TokenDef("data-item-seq");
-  inline const auto DataItem = TokenDef("data-item", flag::lookup);
-  inline const auto DataRule = TokenDef("data-rule", flag::lookup);
-  inline const auto DataObject = TokenDef("data-object");
-  inline const auto DataObjectItem = TokenDef("data-object-item");
-  inline const auto DataArray = TokenDef("data-array");
-  inline const auto DataSet = TokenDef("data-set");
-  inline const auto DataModule = TokenDef("data-module", flag::lookup);
-  inline const auto Submodule =
-    TokenDef("submodule", flag::lookdown | flag::lookup);
-  inline const auto DataTerm = TokenDef("data-term");
-  inline const auto ObjectItemSeq = TokenDef("object-item-seq");
-  inline const auto ImportSeq = TokenDef("import-seq");
-  inline const auto VarSeq = TokenDef("var-seq");
-  inline const auto ElseSeq = TokenDef("else-seq");
-  inline const auto WithSeq = TokenDef("with-seq");
-  inline const auto SkipSeq = TokenDef("skip-seq");
-  inline const auto ItemSeq = TokenDef("item-seq");
-  inline const auto ItemSeq1 = TokenDef("item-seq-1");
-
-  // data and input
-  inline const auto Input = TokenDef("input", flag::lookup);
-  inline const auto Data = TokenDef("data", flag::lookup);
-
-  // fused
-  inline const auto RuleComp = TokenDef(
-    "rule-comp",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleFunc = TokenDef(
-    "rule-func",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleSet = TokenDef(
-    "rule-set",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleObj = TokenDef(
-    "rule-obj",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto DefaultRule =
-    TokenDef("default-rule", flag::lookup | flag::lookdown);
-  inline const auto Local = TokenDef("local", flag::lookup | flag::shadowing);
-  inline const auto ArithInfix = TokenDef("arith-infix");
-  inline const auto BinInfix = TokenDef("bin-infix");
-  inline const auto BoolInfix = TokenDef("bool-infix");
-  inline const auto AssignInfix = TokenDef("assign-infix");
-  inline const auto ArgVar = TokenDef("arg-var", flag::lookup);
-  inline const auto ArgVal = TokenDef("arg-val");
-  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");
-  inline const auto Rego = TokenDef("rego", flag::symtab);
-  inline const auto Math = TokenDef("math");
-  inline const auto Op = TokenDef("op");
-  inline const auto Comparison = TokenDef("comparison");
-  inline const auto Function = TokenDef("function");
-  inline const auto Arg = TokenDef("arg");
-  inline const auto ArgSeq = TokenDef("arg-seq");
-  inline const auto Val = TokenDef("value");
-  inline const auto Id = TokenDef("id");
-  inline const auto Head = TokenDef("head");
-  inline const auto Tail = TokenDef("tail");
-  inline const auto Head1 = TokenDef("head-1");
-  inline const auto Tail1 = TokenDef("tail-1");
-  inline const auto Head2 = TokenDef("head-2");
-  inline const auto Tail2 = TokenDef("tail-2");
-  inline const auto Lhs = TokenDef("lhs");
-  inline const auto Rhs = TokenDef("rhs");
-  inline const auto LhsVars = TokenDef("lhs-vars");
-  inline const auto RhsVars = TokenDef("rhs-vars");
-  inline const auto Key = TokenDef("key", flag::print);
-  inline const auto RefTerm = TokenDef("ref-term");
-  inline const auto NumTerm = TokenDef("num-term");
-  inline const auto ArithArg = TokenDef("arith-arg");
-  inline const auto BinArg = TokenDef("bin-arg");
-  inline const auto BoolArg = TokenDef("bool-arg");
-  inline const auto AssignArg = TokenDef("assign-arg");
-  inline const auto RuleHeadType = TokenDef("rule-head-type");
-  inline const auto UnifyExpr = TokenDef("unify-expr");
-  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");
-  inline const auto Binding = TokenDef("binding");
-  inline const auto Body = TokenDef("body");
-  inline const auto Import =
-    TokenDef("import", flag::lookdown | flag::lookup | flag::shadowing);
-  inline const auto Keyword =
-    TokenDef("keyword", flag::lookdown | flag::lookup);
-  inline const auto Idx = TokenDef("idx");
-  inline const auto Idx1 = TokenDef("idx-1");
-  inline const auto Skip = TokenDef("skip", flag::lookup);
-  inline const auto NestedBody = TokenDef("nested-body", flag::symtab);
-  inline const auto BuiltInHook = TokenDef("builtin-hook", flag::lookup);
-  inline const auto Item = TokenDef("item");
-  inline const auto Item1 = TokenDef("item-1");
-  inline const auto Enumerate = TokenDef("enumerate");
-  inline const auto Compr = TokenDef("compr");
-  inline const auto Merge = TokenDef("merge");
-  inline const auto ImportRef = TokenDef("import-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");
-
-  // parser
-  inline const auto Brace = TokenDef("brace");
-  inline const auto Dot = TokenDef("dot");
-  inline const auto Colon = TokenDef("colon");
-  inline const auto Square = TokenDef("square");
-  inline const auto Paren = TokenDef("paren");
-  inline const auto Not = TokenDef("not");
-  inline const auto Comma = TokenDef("comma");
-  inline const auto Placeholder = TokenDef("_");
-}
\ No newline at end of file
diff --git a/include/rego/unifier.h b/include/rego/unifier.h
deleted file mode 100644
index 5e879608..00000000
--- a/include/rego/unifier.h
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "args.h"
-#include "builtins.h"
-#include "value.h"
-#include "variable.h"
-
-#include <map>
-#include <optional>
-#include <set>
-#include <string>
-#include <trieste/ast.h>
-#include <vector>
-
-namespace rego
-{
-  using namespace trieste;
-
-  class UnifierDef;
-  using Unifier = std::shared_ptr<UnifierDef>;
-  using CallStack = std::shared_ptr<std::vector<Location>>;
-  using ValuesLookup = std::map<std::string, Values>;
-  using WithStack = std::shared_ptr<std::vector<ValuesLookup>>;
-  using UnifierCache = std::shared_ptr<NodeMap<Unifier>>;
-
-  class UnifierDef
-  {
-  public:
-    UnifierDef(
-      const Location& rule,
-      const Node& rulebody,
-      CallStack call_stack,
-      WithStack with_stack,
-      const BuiltIns& builtins,
-      UnifierCache cache);
-    Node unify();
-    Nodes expressions() const;
-    Nodes bindings() const;
-    std::string str() const;
-    std::string dependency_str() const;
-    static Unifier create(
-      const Location& rule,
-      const Node& rulebody,
-      const CallStack& call_stack,
-      const WithStack& with_stack,
-      const BuiltIns& builtins,
-      const UnifierCache& cache);
-    std::optional<Node> resolve_rule(const Nodes& defs) const;
-    std::optional<Node> resolve_rulecomp(const Nodes& rulecomp) const;
-    std::optional<Node> resolve_ruleset(const Nodes& ruleset) const;
-    std::optional<Node> resolve_ruleobj(const Nodes& ruleobj) const;
-    std::optional<RankedNode> resolve_rulefunc(
-      const Node& rulefunc, const Nodes& args) const;
-    Node resolve_module(const Node& module) const;
-
-  private:
-    struct Dependency
-    {
-      std::string name;
-      std::set<std::size_t> dependencies;
-      std::size_t score;
-    };
-
-    Unifier rule_unifier(const Location& rule, const Node& rulebody) const;
-    void init_from_body(
-      const Node& rulebody, std::vector<Node>& statements, std::size_t root);
-    std::size_t add_variable(const Node& local);
-    std::size_t add_unifyexpr(const Node& unifyexpr);
-    void add_withpush(const Node& withpush);
-    void add_withpop(const Node& withpop);
-    void reset();
-
-    void compute_dependency_scores();
-    std::size_t compute_dependency_score(
-      std::size_t index, std::set<size_t>& visited);
-    std::size_t dependency_score(const Variable& var) const;
-    std::size_t dependency_score(const Node& expr) const;
-    std::size_t detect_cycles() const;
-    bool has_cycle(std::size_t id) const;
-
-    Values evaluate(const Location& var, const Node& value);
-    Values evaluate_function(
-      const Location& var, const std::string& func_name, const Args& args);
-    Values resolve_var(const Node& var, bool exclude_with = false);
-    Values enumerate(const Location& var, const Node& container);
-    Values resolve_skip(const Node& skip);
-    Values check_with(const Node& var, bool bypass_recurse_check = false);
-    Values apply_access(const Location& var, const Values& args);
-    std::optional<Value> call_function(
-      const Location& var, const Values& args) const;
-    std::optional<Value> call_named_function(
-      const Location& var, const std::string& name, const Values& args);
-    bool is_local(const Node& var);
-    std::size_t scan_vars(const Node& expr, std::vector<Location>& locals);
-    void pass();
-    void execute_statements(Nodes::iterator begin, Nodes::iterator end);
-    void remove_invalid_values();
-    void mark_invalid_values();
-    Variable& get_variable(const Location& name);
-    bool is_variable(const Location& name) const;
-    Node bind_variables();
-    Args create_args(const Node& args);
-    bool would_recurse(const Node& node);
-
-    bool push_rule(const Location& rule);
-    void pop_rule(const Location& rule);
-
-    void push_with(const Node& withseq);
-    void pop_with();
-
-    void push_not();
-    void pop_not();
-
-    Location m_rule;
-    std::map<Location, Variable> m_variables;
-    std::vector<Node> m_statements;
-    NodeMap<std::vector<Node>> m_nested_statements;
-    CallStack m_call_stack;
-    WithStack m_with_stack;
-    BuiltIns m_builtins;
-    std::size_t m_retries;
-    Token m_parent_type;
-    UnifierCache m_cache;
-    NodeMap<std::size_t> m_expr_ids;
-    std::vector<Dependency> m_dependency_graph;
-    bool m_negate;
-  };
-}
\ No newline at end of file
diff --git a/include/rego/value.h b/include/rego/value.h
deleted file mode 100644
index c266d213..00000000
--- a/include/rego/value.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include <trieste/source.h>
-
-namespace rego
-{
-  using namespace trieste;
-
-  class ValueDef;
-  using rank_t = std::size_t;
-  using Value = std::shared_ptr<ValueDef>;
-  using Values = std::vector<Value>;
-  using RankedNode = std::pair<rank_t, Node>;
-
-  class ValueDef
-  {
-  public:
-    void mark_as_valid();
-    void mark_as_invalid();
-    void reduce_set();
-
-    std::string str() const;
-    std::string json() const;
-    bool depends_on(const Value& value) const;
-    bool invalid() const;
-    const Location& var() const;
-    const Node& node() const;
-    const Values& sources() const;
-    Node to_term() 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);
-    friend bool operator<(const Value& lhs, const Value& rhs);
-
-    static Value create(const RankedNode& value);
-    static Value create(const Location& var, const RankedNode& value);
-    static Value create(
-      const Location& var, const RankedNode& value, const Values& sources);
-    static Value create(const Node& value);
-    static Value create(const Location& var, const Node& value);
-    static Value create(
-      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;
-    Location m_var;
-    Node m_node;
-    Values m_sources;
-    bool m_invalid;
-    rank_t m_rank;
-    ValueDef(const RankedNode& value);
-    ValueDef(const Location& var, const RankedNode& value);
-    ValueDef(
-      const Location& var, const RankedNode& value, const Values& sources);
-    ValueDef(const Node& value);
-    ValueDef(const Location& var, const Node& value);
-    ValueDef(const Location& var, const Node& value, const Values& sources);
-  };
-}
\ No newline at end of file
diff --git a/include/rego/value_map.h b/include/rego/value_map.h
deleted file mode 100644
index 823ef0a5..00000000
--- a/include/rego/value_map.h
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "value.h"
-
-#include <map>
-#include <set>
-#include <string>
-#include <trieste/ast.h>
-
-namespace rego
-{
-  class ValueMap
-  {
-  public:
-    ValueMap();
-    ValueMap(
-      const Values::const_iterator& begin, const Values::const_iterator& end);
-    bool contains(const Value& value) const;
-    bool intersect_with(const Values& values);
-    bool remove_values_not_contained_in(const Values& values);
-    void clear();
-    bool insert(const Value& value);
-    bool erase(const std::string& json);
-    bool empty() const;
-    Nodes to_terms() const;
-    Values valid_values() const;
-    bool remove_invalid_values();
-    void mark_valid_values(bool include_falsy);
-    void mark_invalid_values();
-
-    friend std::ostream& operator<<(std::ostream& os, const ValueMap& values);
-
-  private:
-    std::multimap<std::string, Value> m_map;
-    std::set<std::pair<std::string, std::string>> m_values;
-    std::set<std::string> m_keys;
-  };
-}
\ No newline at end of file
diff --git a/include/rego/variable.h b/include/rego/variable.h
deleted file mode 100644
index ff454033..00000000
--- a/include/rego/variable.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-#pragma once
-
-#include "value.h"
-#include "value_map.h"
-
-namespace rego
-{
-  class Variable
-  {
-  public:
-    Variable(const Node& local, std::size_t id);
-    bool unify(const Values& others);
-    std::string str() const;
-    bool remove_invalid_values();
-    void mark_invalid_values();
-    void mark_valid_values();
-    Values valid_values() const;
-    Node to_term() const;
-    Node bind();
-    void reset();
-
-    bool is_unify() const;
-    bool is_user_var() const;
-    Location name() const;
-    std::size_t id() const;
-
-    static bool is_unify(const std::string_view& name);
-    static bool is_user_var(const std::string_view& name);
-
-    friend std::ostream& operator<<(std::ostream& os, const Variable& variable);
-    friend std::ostream& operator<<(
-      std::ostream& os, const std::map<Location, Variable>& variables);
-
-  private:
-    bool intersect_with(const Values& others);
-    bool initialize(const Values& others);
-
-    Node m_local;
-    ValueMap m_values;
-    bool m_unify;
-    bool m_user_var;
-    bool m_initialized;
-    std::size_t m_id;
-  };
-}
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6c9164d6..3baa6f49 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -11,7 +11,6 @@ value.cc
 value_map.cc
 resolver.cc
 variable.cc
-wf.cc
 builtins.cc
 helpers.cc
 rego_c.cc
@@ -33,6 +32,7 @@ passes/lift_refheads.cc
 passes/symbols.cc
 passes/replace_argvals.cc
 passes/lift_query.cc
+passes/expand_imports.cc
 passes/constants.cc
 passes/enumerate.cc
 passes/locals.cc
@@ -83,19 +83,16 @@ target_link_libraries(rego
     trieste::trieste
 )
 
-target_precompile_headers(rego
-  PRIVATE
-    ${CMAKE_CURRENT_SOURCE_DIR}/../include/rego/errors.h
-    ${CMAKE_CURRENT_SOURCE_DIR}/../include/rego/helpers.h
-    ${CMAKE_CURRENT_SOURCE_DIR}/../include/rego/passes.h
-)
-
-
-if (MSVC)
+if(MSVC)
+  target_precompile_headers(rego
+    PRIVATE
+      internal.hh
+  )
   target_compile_options(rego PUBLIC "/Zc:__cplusplus")
   target_compile_definitions(rego PUBLIC "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING")
 endif()
 
+
 target_include_directories( rego
   PUBLIC
     $<INSTALL_INTERFACE:include>
diff --git a/src/args.cc b/src/args.cc
index 3e5a21c3..367dbfc0 100644
--- a/src/args.cc
+++ b/src/args.cc
@@ -1,9 +1,4 @@
-#include "args.h"
-
-#include "helpers.h"
-#include "value.h"
-
-#include <sstream>
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/bigint.cc b/src/bigint.cc
index b73cea91..17acfdd4 100644
--- a/src/bigint.cc
+++ b/src/bigint.cc
@@ -1,6 +1,4 @@
-#include "bigint.h"
-
-#include <string_view>
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/builtins.cc b/src/builtins.cc
index 91bd42af..e0d5f044 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -1,11 +1,4 @@
-#include "builtins.h"
-
-#include "builtins/register.h"
-#include "errors.h"
-#include "helpers.h"
-#include "rego.h"
-#include "resolver.h"
-#include "version.h"
+#include "builtins/builtins.h"
 
 namespace
 {
diff --git a/src/builtins/aggregates.cc b/src/builtins/aggregates.cc
index 63da57de..dc9960c2 100644
--- a/src/builtins/aggregates.cc
+++ b/src/builtins/aggregates.cc
@@ -1,8 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
-#include "utf8.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/arrays.cc b/src/builtins/arrays.cc
index 68f9e8ce..5e70c880 100644
--- a/src/builtins/arrays.cc
+++ b/src/builtins/arrays.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/bits.cc b/src/builtins/bits.cc
index 040df1df..a12bf9b2 100644
--- a/src/builtins/bits.cc
+++ b/src/builtins/bits.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/register.h b/src/builtins/builtins.h
similarity index 57%
rename from src/builtins/register.h
rename to src/builtins/builtins.h
index d4d4dea1..3dbad057 100644
--- a/src/builtins/register.h
+++ b/src/builtins/builtins.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "builtins.h"
+#include "internal.hh"
 
 namespace rego
 {
@@ -21,4 +21,17 @@ namespace rego
     std::vector<BuiltIn> types();
     std::vector<BuiltIn> units();
   }
+
+  struct rune
+  {
+    std::uint32_t value;
+    std::string_view source;
+  };
+
+  using runestring = std::basic_string<std::uint32_t>;
+  using runestring_view = std::basic_string_view<std::uint32_t>;
+
+  std::vector<rune> utf8_to_runes(const std::string_view& utf8);
+  runestring utf8_to_runestring(const std::string_view& utf8);
+  std::string runestring_to_utf8(const runestring_view& runes);
 }
\ No newline at end of file
diff --git a/src/builtins/casts.cc b/src/builtins/casts.cc
index e6e15d67..39799c63 100644
--- a/src/builtins/casts.cc
+++ b/src/builtins/casts.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
@@ -99,7 +96,7 @@ namespace
         double float_value = std::stod(number_str);
         return Resolver::scalar(float_value);
       }
-      catch (const std::invalid_argument)
+      catch (const std::invalid_argument&)
       {
         return err(args[0], "invalid syntax", EvalBuiltInError);
       }
diff --git a/src/builtins/encoding.cc b/src/builtins/encoding.cc
index d18e9130..3c43702e 100644
--- a/src/builtins/encoding.cc
+++ b/src/builtins/encoding.cc
@@ -1,7 +1,5 @@
 #include "base64/base64.h"
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/numbers.cc b/src/builtins/numbers.cc
index 03ffac5d..46300e82 100644
--- a/src/builtins/numbers.cc
+++ b/src/builtins/numbers.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
+#include "builtins.h"
 
 #include <numeric>
 
diff --git a/src/builtins/objects.cc b/src/builtins/objects.cc
index fafffca3..4ad8af28 100644
--- a/src/builtins/objects.cc
+++ b/src/builtins/objects.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/regex.cc b/src/builtins/regex.cc
index de78c6cf..4858992e 100644
--- a/src/builtins/regex.cc
+++ b/src/builtins/regex.cc
@@ -1,8 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "re2/stringpiece.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 #include <regex>
 
diff --git a/src/builtins/semver.cc b/src/builtins/semver.cc
index 04088ce0..2510f401 100644
--- a/src/builtins/semver.cc
+++ b/src/builtins/semver.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/sets.cc b/src/builtins/sets.cc
index 2e999902..810671c3 100644
--- a/src/builtins/sets.cc
+++ b/src/builtins/sets.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/strings.cc b/src/builtins/strings.cc
index 6e73bd1b..07165261 100644
--- a/src/builtins/strings.cc
+++ b/src/builtins/strings.cc
@@ -1,8 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
-#include "utf8.h"
+#include "builtins.h"
 
 #include <bitset>
 
diff --git a/src/builtins/time.cc b/src/builtins/time.cc
index f3580d20..0aa49b9f 100644
--- a/src/builtins/time.cc
+++ b/src/builtins/time.cc
@@ -1,4 +1,4 @@
-#include "register.h"
+#include "builtins.h"
 
 #include <chrono>
 
diff --git a/src/builtins/types.cc b/src/builtins/types.cc
index 07d5f9a5..70112f4e 100644
--- a/src/builtins/types.cc
+++ b/src/builtins/types.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/units.cc b/src/builtins/units.cc
index 9b84cf36..4d7fa7cf 100644
--- a/src/builtins/units.cc
+++ b/src/builtins/units.cc
@@ -1,8 +1,4 @@
-#include "bigint.h"
-#include "errors.h"
-#include "helpers.h"
-#include "register.h"
-#include "resolver.h"
+#include "builtins.h"
 
 namespace
 {
diff --git a/src/builtins/utf8.cc b/src/builtins/utf8.cc
index b672c0cb..31198fbd 100644
--- a/src/builtins/utf8.cc
+++ b/src/builtins/utf8.cc
@@ -1,6 +1,4 @@
-#include "utf8.h"
-
-#include <vector>
+#include "builtins.h"
 
 namespace
 {
@@ -115,88 +113,86 @@ namespace
     }
   }
 
-  rego::rune utf8_to_rune(
-    std::string_view::const_iterator pos, std::string_view::const_iterator end)
+  rego::rune utf8_to_rune(const std::string_view& utf8)
   {
-    std::uint8_t c0 = static_cast<std::uint8_t>(pos[0]);
-    std::size_t remaining = std::distance(pos, end);
+    std::uint8_t c0 = static_cast<std::uint8_t>(utf8[0]);
     if (c0 == '\\')
     {
-      if (remaining >= 1)
+      if (utf8.size() >= 1)
       {
-        switch (pos[1])
+        switch (utf8[1])
         {
           case 'f':
-            return {'\f', std::string_view(pos, pos + 2)};
+            return {'\f', utf8.substr(0, 2)};
           case 'n':
-            return {'\n', std::string_view(pos, pos + 2)};
+            return {'\n', utf8.substr(0, 2)};
           case 'r':
-            return {'\r', std::string_view(pos, pos + 2)};
+            return {'\r', utf8.substr(0, 2)};
           case 't':
-            return {'\t', std::string_view(pos, pos + 2)};
+            return {'\t', utf8.substr(0, 2)};
           case 'v':
-            return {'\v', std::string_view(pos, pos + 2)};
+            return {'\v', utf8.substr(0, 2)};
           case '\\':
-            return {'\\', std::string_view(pos, pos + 2)};
+            return {'\\', utf8.substr(0, 2)};
           case '\'':
-            return {'\'', std::string_view(pos, pos + 2)};
+            return {'\'', utf8.substr(0, 2)};
           case '\"':
-            return {'\"', std::string_view(pos, pos + 2)};
+            return {'\"', utf8.substr(0, 2)};
         }
       }
-      if (remaining >= 3 && pos[1] == 'x')
+      if (utf8.size() >= 3 && utf8[1] == 'x')
       {
-        std::string hex = std::string(pos + 2, pos + 4);
+        std::string hex = std::string(utf8.substr(2, 2));
         std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, std::string_view(pos, pos + 4)};
+        return {value, utf8.substr(0, 4)};
       }
-      if (remaining >= 5 && pos[1] == 'u')
+      if (utf8.size() >= 5 && utf8[1] == 'u')
       {
-        std::string hex = std::string(pos + 2, pos + 6);
+        std::string hex = std::string(utf8.substr(2, 4));
         std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, std::string_view(pos, pos + 6)};
+        return {value, utf8.substr(0, 6)};
       }
-      if (remaining >= 9 && pos[1] == 'U')
+      if (utf8.size() >= 9 && utf8[1] == 'U')
       {
-        std::string hex = std::string(pos + 2, pos + 10);
+        std::string hex = std::string(utf8.substr(2, 8));
         std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, std::string_view(pos, pos + 10)};
+        return {value, utf8.substr(0, 10)};
       }
     }
 
     if ((c0 & Mask1) == Mark1)
     {
       std::uint32_t value = c0 & Value1;
-      return {value, std::string_view(pos, pos + 1)};
+      return {value, utf8.substr(0, 1)};
     }
 
-    if ((c0 & Mask2) == Mark2 && remaining >= 2)
+    if ((c0 & Mask2) == Mark2 && utf8.size() >= 2)
     {
-      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
+      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
       if ((c1 & MaskX) == MarkX)
       {
         std::uint32_t value = c0 & Value2;
         value = (value << ShiftX) | (c1 & ValueX);
-        return {value, std::string_view(pos, pos + 2)};
+        return {value, utf8.substr(0, 2)};
       }
     }
-    else if ((c0 & Mask3) == Mark3 && remaining >= 3)
+    else if ((c0 & Mask3) == Mark3 && utf8.size() >= 3)
     {
-      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
-      std::uint8_t c2 = static_cast<std::uint8_t>(pos[2]);
+      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
+      std::uint8_t c2 = static_cast<std::uint8_t>(utf8[2]);
       if ((c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX)
       {
         std::uint32_t value = c0 & Value3;
         value = (value << ShiftX) | (c1 & ValueX);
         value = (value << ShiftX) | (c2 & ValueX);
-        return {value, std::string_view(pos, pos + 3)};
+        return {value, utf8.substr(0, 3)};
       }
     }
-    else if ((c0 & Mask4) == Mark4 && remaining >= 4)
+    else if ((c0 & Mask4) == Mark4 && utf8.size() >= 4)
     {
-      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
-      std::uint8_t c2 = static_cast<std::uint8_t>(pos[2]);
-      std::uint8_t c3 = static_cast<std::uint8_t>(pos[3]);
+      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
+      std::uint8_t c2 = static_cast<std::uint8_t>(utf8[2]);
+      std::uint8_t c3 = static_cast<std::uint8_t>(utf8[3]);
       if (
         (c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX && (c3 & MaskX) == MarkX)
       {
@@ -204,12 +200,12 @@ namespace
         value = (value << ShiftX) | (c1 & ValueX);
         value = (value << ShiftX) | (c2 & ValueX);
         value = (value << ShiftX) | (c3 & ValueX);
-        return {value, std::string_view(pos, pos + 4)};
+        return {value, utf8.substr(0, 4)};
       }
     }
 
     // bad
-    return {Bad, std::string_view(pos, pos + 1)};
+    return {Bad, utf8.substr(0, 1)};
   }
 
 }
@@ -220,13 +216,12 @@ namespace rego
   {
     std::vector<rune> runes;
     runes.reserve(utf8.size());
-    auto pos = utf8.begin();
-    auto end = utf8.end();
-    while (pos < end)
+    std::size_t pos = 0;
+    while (pos < utf8.size())
     {
-      rune r = utf8_to_rune(pos, end);
+      rune r = utf8_to_rune(utf8.substr(pos));
       runes.push_back(r);
-      pos = r.source.end();
+      pos += r.source.size();
     }
 
     return runes;
@@ -236,14 +231,14 @@ namespace rego
   {
     runestring runes;
     runes.reserve(utf8.size());
-    auto pos = utf8.begin();
-    auto end = utf8.end();
-    while (pos < end)
+    std::size_t pos = 0;
+    while (pos < utf8.size())
     {
-      rune r = utf8_to_rune(pos, end);
+      rune r = utf8_to_rune(utf8.substr(pos));
       runes.push_back(r.value);
-      pos = r.source.end();
+      pos += r.source.size();
     }
+
     return runes;
   }
 
diff --git a/src/builtins/utf8.h b/src/builtins/utf8.h
deleted file mode 100644
index f51022b1..00000000
--- a/src/builtins/utf8.h
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma once
-
-#include <cstdint>
-#include <string>
-
-namespace rego
-{
-  struct rune
-  {
-    std::uint32_t value;
-    std::string_view source;
-  };
-
-  using runestring = std::basic_string<std::uint32_t>;
-  using runestring_view = std::basic_string_view<std::uint32_t>;
-
-  std::vector<rune> utf8_to_runes(const std::string_view& utf8);
-  runestring utf8_to_runestring(const std::string_view& utf8);
-  std::string runestring_to_utf8(const runestring_view& runes);
-}
\ No newline at end of file
diff --git a/src/encoding.cc b/src/encoding.cc
index af329c90..5ca212dc 100644
--- a/src/encoding.cc
+++ b/src/encoding.cc
@@ -1,6 +1,4 @@
-#include "encoding.h"
-
-#include "helpers.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/helpers.cc b/src/helpers.cc
index bba554a7..2f3dfa4c 100644
--- a/src/helpers.cc
+++ b/src/helpers.cc
@@ -1,7 +1,4 @@
-#include "helpers.h"
-
-#include "errors.h"
-#include "resolver.h"
+#include "internal.hh"
 #include "version.h"
 
 #ifdef _WIN32
@@ -719,4 +716,17 @@ namespace rego
     m_func = func;
     return *this;
   }
+
+  bool is_ref_to_type(const Node& var, const std::set<Token>& types)
+  {
+    Nodes defs = var->lookup();
+    if (defs.size() > 0)
+    {
+      return types.contains(defs[0]->type());
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/internal.hh b/src/internal.hh
new file mode 100644
index 00000000..cefb5ddd
--- /dev/null
+++ b/src/internal.hh
@@ -0,0 +1,565 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include "rego/rego.hh"
+
+#define LOG(...) Logger::print(Logger::indent, __VA_ARGS__)
+#define LOG_HEADER(message, header) \
+  Logger::print(Logger::indent, (header), (message), (header))
+#define LOG_VECTOR(vector) Logger::print_vector_inline((vector))
+#define LOG_VECTOR_CUSTOM(vector, transform) \
+  Logger::print_vector_custom((vector), (transform))
+#define LOG_MAP_VALUES(map) Logger::print_map_values((map))
+#define LOG_INDENT() Logger::increase_print_indent()
+#define LOG_UNINDENT() Logger::decrease_print_indent()
+
+namespace rego
+{
+  const inline auto ScalarToken =
+    T(JSONInt) / T(JSONFloat) / T(JSONTrue) / T(JSONFalse) / T(JSONNull);
+  const inline auto ArithToken =
+    T(Add) / T(Subtract) / T(Multiply) / T(Divide) / T(Modulo);
+  const inline auto ArithInfixArg = T(Expr) / T(NumTerm) / T(Ref) /
+    T(UnaryExpr) / T(ArithInfix) / T(RefTerm) / T(ExprCall);
+  const inline auto BinInfixArg = T(Expr) / T(Ref) / T(RefTerm) / T(ExprCall) /
+    T(Set) / T(SetCompr) / T(BinInfix);
+  const auto inline RefArg = T(RefArgDot) / T(RefArgBrack);
+  const inline auto BoolToken = T(Equals) / T(NotEquals) / T(GreaterThan) /
+    T(LessThan) / T(GreaterThanOrEquals) / T(LessThanOrEquals);
+  const inline auto TermToken = T(Var) / T(Ref) / T(Array) / T(Object) /
+    T(Set) / T(ArrayCompr) / T(ObjectCompr) / T(SetCompr);
+  const inline auto StringToken = T(JSONString) / T(RawString);
+  const inline auto ExprToken = T(Term) / ArithToken / BoolToken / StringToken /
+    T(Expr) / ScalarToken / TermToken / T(JSONString) / T(Array) / T(Set) /
+    T(Object) / T(Paren) / T(Not) / T(Dot) / T(And) / T(Or) / T(ExprCall);
+  const auto inline MembershipToken = ScalarToken / T(JSONString) /
+    T(RawString) / T(Var) / T(Object) / T(Array) / T(Set) / T(Dot) / T(Paren) /
+    ArithToken / BoolToken / T(And) / T(Or) / T(ExprCall);
+  const auto inline RuleRefToken = T(Var) / T(Dot) / T(Array);
+
+  inline const std::set<std::string> Keywords(
+    {"if", "in", "contains", "every"});
+
+  inline const std::set<Token> RuleTypes(
+    {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
+
+  struct UnwrapResult
+  {
+    Node node;
+    bool success;
+  };
+
+  class UnwrapOpt
+  {
+  public:
+    UnwrapOpt(std::size_t index);
+    bool exclude_got() const;
+    UnwrapOpt& exclude_got(bool exclude_got);
+    bool specify_number() const;
+    UnwrapOpt& specify_number(bool specify_number);
+    const std::string& code() const;
+    UnwrapOpt& code(const std::string& value);
+    const std::string& pre() const;
+    UnwrapOpt& pre(const std::string& value);
+    const std::string& message() const;
+    UnwrapOpt& message(const std::string& value);
+    const std::string& func() const;
+    UnwrapOpt& func(const std::string& value);
+    const std::vector<Token>& types() const;
+    UnwrapOpt& types(const std::vector<Token>& value);
+    UnwrapOpt& type(const Token& value);
+
+    Node unwrap(const Nodes& args) const;
+
+  private:
+    bool m_exclude_got;
+    bool m_specify_number;
+    std::string m_code;
+    std::string m_prefix;
+    std::string m_message;
+    std::string m_func;
+    std::vector<Token> m_types;
+    std::size_t m_index;
+  };
+
+  bool all_alnum(const std::string_view& str);
+  bool contains_local(const Node& node);
+  bool contains_ref(const Node& node);
+  Node concat_refs(const Node& lhs, const Node& rhs);
+  std::string flatten_ref(const Node& ref);
+  bool is_in(const Node& node, const std::set<Token>& token);
+  bool in_query(const Node& node);
+  bool is_constant(const Node& node);
+  bool is_instance(const Node& value, const std::set<Token>& types);
+  bool is_falsy(const Node& node);
+  bool is_truthy(const Node& node);
+  bool is_undefined(const Node& node);
+  bool is_ref_to_type(const Node& var, const std::set<Token>& types);
+  std::string strip_quotes(const std::string_view& str);
+  BigInt get_int(const Node& node);
+  double get_double(const Node& node);
+  std::string get_string(const Node& node);
+  bool get_bool(const Node& node);
+  std::string type_name(const Token& type, bool specify_number = false);
+  std::string type_name(const Node& node, bool specify_number = false);
+  Node unwrap_arg(const Nodes& args, const UnwrapOpt& options);
+  UnwrapResult unwrap(const Node& term, const std::set<Token>& types);
+
+  using namespace trieste;
+  using PrintNode = std::ostream& (*)(std::ostream&, const Node&);
+
+  class Resolver
+  {
+  public:
+    struct NodePrinter
+    {
+      Node node;
+      PrintNode printer;
+      std::string str() const;
+    };
+
+    static Node scalar(BigInt value);
+    static Node scalar(double value);
+    static Node scalar(bool value);
+    static Node scalar(const char* value);
+    static Node scalar(const std::string& value);
+    static Node scalar();
+    static Node term(BigInt value);
+    static Node term(double value);
+    static Node term(bool value);
+    static Node term(const char* value);
+    static Node term(const std::string& value);
+    static Node term();
+    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);
+    static NodePrinter expr_str(const Node& unifyexpr);
+    static NodePrinter enum_str(const Node& unifyexprenum);
+    static NodePrinter with_str(const Node& unifyexprwith);
+    static NodePrinter compr_str(const Node& unifyexprcompr);
+    static NodePrinter ref_str(const Node& ref);
+    static NodePrinter body_str(const Node& rego);
+    static NodePrinter not_str(const Node& rego);
+    static NodePrinter term_str(const Node& rego);
+    static NodePrinter rego_str(const Node& rego);
+    static Node negate(const Node& value);
+    static Node unary(const Node& value);
+    static Node arithinfix(const Node& op, const Node& lhs, const Node& rhs);
+    static Node bininfix(const Node& op, const Node& lhs, const Node& rhs);
+    static Node boolinfix(const Node& op, const Node& lhs, const Node& rhs);
+    static std::optional<Nodes> apply_access(
+      const Node& container, const Node& index);
+    static Node object(const Node& object_items, bool is_rule);
+    static Node array(const Node& array_members);
+    static Node set(const Node& set_members);
+    static Node set_intersection(const Node& lhs, const Node& rhs);
+    static Node set_union(const Node& lhs, const Node& rhs);
+    static Node set_difference(const Node& lhs, const Node& rhs);
+    static Nodes resolve_varseq(const Node& varseq);
+    static Nodes object_lookdown(const Node& object, const Node& query);
+    static Node inject_args(const Node& rulefunc, const Nodes& args);
+    static Node membership(
+      const Node& index, const Node& item, const Node& itemseq);
+    static Node membership(const Node& item, const Node& itemseq);
+    static std::vector<std::string> array_find(
+      const Node& array, const std::string& search);
+    static std::vector<std::string> object_find(
+      const Node& object, const std::string& search);
+    static Node to_term(const Node& value);
+    static void flatten_terms_into(const Node& termset, Node& terms);
+    static void flatten_items_into(const Node& termset, Node& terms);
+    static Node reduce_termset(const Node& termset);
+    static void insert_into_object(
+      Node& object, const std::string& path, const Node& value);
+  };
+
+  std::ostream& operator<<(
+    std::ostream& os, const Resolver::NodePrinter& printer);
+
+  PassDef input_data();
+  PassDef modules();
+  PassDef imports();
+  PassDef keywords();
+  PassDef lists();
+  PassDef ifs();
+  PassDef elses();
+  PassDef rules();
+  PassDef build_calls();
+  PassDef membership();
+  PassDef build_refs();
+  PassDef structure();
+  PassDef strings();
+  PassDef merge_data();
+  PassDef lift_refheads();
+  PassDef symbols();
+  PassDef replace_argvals();
+  PassDef lift_query();
+  PassDef expand_imports();
+  PassDef constants();
+  PassDef explicit_enums();
+  PassDef body_locals(const BuiltIns& builtins);
+  PassDef value_locals(const BuiltIns& builtins);
+  PassDef rules_to_compr();
+  PassDef compr();
+  PassDef absolute_refs();
+  PassDef merge_modules();
+  PassDef datarule();
+  PassDef skips();
+  PassDef unary();
+  PassDef multiply_divide();
+  PassDef add_subtract();
+  PassDef comparison();
+  PassDef assign(const BuiltIns& builtins);
+  PassDef skip_refs(const BuiltIns& builtins);
+  PassDef simple_refs();
+  PassDef init();
+  PassDef implicit_enums();
+  PassDef rulebody();
+  PassDef lift_to_rule();
+  PassDef functions();
+  PassDef unify(const BuiltIns& builtins);
+  PassDef query();
+
+  std::ostream& operator<<(
+    std::ostream& os, const std::set<trieste::Location>& locs);
+  std::ostream& operator<<(
+    std::ostream& os, const std::vector<trieste::Location>& locs);
+
+  struct Logger
+  {
+    static bool enabled;
+    static std::string indent;
+
+    static inline void increase_print_indent()
+    {
+      indent += "  ";
+    }
+
+    static inline void decrease_print_indent()
+    {
+      indent = indent.substr(0, indent.size() - 2);
+    }
+
+    template <typename T>
+    static inline void print(const T& value)
+    {
+      if (enabled)
+      {
+        std::cout << value << std::endl;
+      }
+    }
+
+    template <typename T, typename... Types>
+    static void print(T head, Types... tail)
+    {
+      if (enabled)
+      {
+        std::cout << head;
+        print(tail...);
+      }
+    }
+
+    template <typename T>
+    static inline void print_vector_inline(const std::vector<T>& values)
+    {
+      if (enabled)
+      {
+        std::cout << indent << "[";
+        std::string sep = "";
+        for (auto& value : values)
+        {
+          std::cout << sep << value;
+          sep = ", ";
+        }
+        std::cout << "]" << std::endl;
+      }
+    }
+
+    template <typename T, typename P>
+    static inline void print_vector_custom(
+      const std::vector<T>& values, P (*transform)(const T&))
+    {
+      if (enabled)
+      {
+        for (auto& value : values)
+        {
+          print(indent, transform(value));
+        }
+      }
+    }
+
+    template <typename K, typename V>
+    static inline void print_map_values(const std::map<K, V>& values)
+    {
+      if (enabled)
+      {
+        print(indent, "{");
+        for (auto& [_, value] : values)
+        {
+          print(indent, "  ", value);
+        }
+        print(indent, "}");
+      }
+    }
+  };
+
+  class ValueDef;
+  using rank_t = std::size_t;
+  using Value = std::shared_ptr<ValueDef>;
+  using Values = std::vector<Value>;
+  using RankedNode = std::pair<rank_t, Node>;
+
+  class ValueDef
+  {
+  public:
+    void mark_as_valid();
+    void mark_as_invalid();
+    void reduce_set();
+
+    std::string str() const;
+    std::string json() const;
+    bool depends_on(const Value& value) const;
+    bool invalid() const;
+    const Location& var() const;
+    const Node& node() const;
+    const Values& sources() const;
+    Node to_term() 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);
+    friend bool operator<(const Value& lhs, const Value& rhs);
+
+    static Value create(const RankedNode& value);
+    static Value create(const Location& var, const RankedNode& value);
+    static Value create(
+      const Location& var, const RankedNode& value, const Values& sources);
+    static Value create(const Node& value);
+    static Value create(const Location& var, const Node& value);
+    static Value create(
+      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;
+    Location m_var;
+    Node m_node;
+    Values m_sources;
+    bool m_invalid;
+    rank_t m_rank;
+    ValueDef(const RankedNode& value);
+    ValueDef(const Location& var, const RankedNode& value);
+    ValueDef(
+      const Location& var, const RankedNode& value, const Values& sources);
+    ValueDef(const Node& value);
+    ValueDef(const Location& var, const Node& value);
+    ValueDef(const Location& var, const Node& value, const Values& sources);
+  };
+
+  class ValueMap
+  {
+  public:
+    ValueMap();
+    ValueMap(
+      const Values::const_iterator& begin, const Values::const_iterator& end);
+    bool contains(const Value& value) const;
+    bool intersect_with(const Values& values);
+    bool remove_values_not_contained_in(const Values& values);
+    void clear();
+    bool insert(const Value& value);
+    bool erase(const std::string& json);
+    bool empty() const;
+    Nodes to_terms() const;
+    Values valid_values() const;
+    bool remove_invalid_values();
+    void mark_valid_values(bool include_falsy);
+    void mark_invalid_values();
+
+    friend std::ostream& operator<<(std::ostream& os, const ValueMap& values);
+
+  private:
+    std::multimap<std::string, Value> m_map;
+    std::set<std::pair<std::string, std::string>> m_values;
+    std::set<std::string> m_keys;
+  };
+
+  /**
+   * The Args class provides an interable interface over the Cartesian product
+   * of all sets of arguments to a function. It will contain N sets of argument
+   * sources, and will produce s_0 * s_1 * ... * s_n arguments, where s_i is the
+   * size of the i-th argument source.
+   */
+  class Args
+  {
+  public:
+    Args();
+    void push_back_source(const Values& values);
+    void mark_invalid(const std::set<Value>& active) const;
+    void mark_invalid_except(const std::set<Value>& active) const;
+    Values at(std::size_t index) const;
+    std::size_t size() const;
+    std::string str() const;
+    std::size_t source_size() const;
+    const Values& source_at(std::size_t index) const;
+    Args subargs(std::size_t start) const;
+    friend std::ostream& operator<<(std::ostream& os, const Args& args);
+
+  private:
+    std::vector<Values> m_values;
+    std::vector<size_t> m_stride;
+    std::size_t m_size;
+  };
+
+  class Variable
+  {
+  public:
+    Variable(const Node& local, std::size_t id);
+    bool unify(const Values& others);
+    std::string str() const;
+    bool remove_invalid_values();
+    void mark_invalid_values();
+    void mark_valid_values();
+    Values valid_values() const;
+    Node to_term() const;
+    Node bind();
+    void reset();
+
+    bool is_unify() const;
+    bool is_user_var() const;
+    Location name() const;
+    std::size_t id() const;
+
+    static bool is_unify(const std::string_view& name);
+    static bool is_user_var(const std::string_view& name);
+
+    friend std::ostream& operator<<(std::ostream& os, const Variable& variable);
+    friend std::ostream& operator<<(
+      std::ostream& os, const std::map<Location, Variable>& variables);
+
+  private:
+    bool intersect_with(const Values& others);
+    bool initialize(const Values& others);
+
+    Node m_local;
+    ValueMap m_values;
+    bool m_unify;
+    bool m_user_var;
+    bool m_initialized;
+    std::size_t m_id;
+  };
+
+  class UnifierDef;
+  using Unifier = std::shared_ptr<UnifierDef>;
+  using CallStack = std::shared_ptr<std::vector<Location>>;
+  using ValuesLookup = std::map<std::string, Values>;
+  using WithStack = std::shared_ptr<std::vector<ValuesLookup>>;
+  using UnifierCache = std::shared_ptr<NodeMap<Unifier>>;
+
+  class UnifierDef
+  {
+  public:
+    UnifierDef(
+      const Location& rule,
+      const Node& rulebody,
+      CallStack call_stack,
+      WithStack with_stack,
+      const BuiltIns& builtins,
+      UnifierCache cache);
+    Node unify();
+    Nodes expressions() const;
+    Nodes bindings() const;
+    std::string str() const;
+    std::string dependency_str() const;
+    static Unifier create(
+      const Location& rule,
+      const Node& rulebody,
+      const CallStack& call_stack,
+      const WithStack& with_stack,
+      const BuiltIns& builtins,
+      const UnifierCache& cache);
+    std::optional<Node> resolve_rule(const Nodes& defs) const;
+    std::optional<Node> resolve_rulecomp(const Nodes& rulecomp) const;
+    std::optional<Node> resolve_ruleset(const Nodes& ruleset) const;
+    std::optional<Node> resolve_ruleobj(const Nodes& ruleobj) const;
+    std::optional<RankedNode> resolve_rulefunc(
+      const Node& rulefunc, const Nodes& args) const;
+    Node resolve_module(const Node& module) const;
+
+  private:
+    struct Dependency
+    {
+      std::string name;
+      std::set<std::size_t> dependencies;
+      std::size_t score;
+    };
+
+    Unifier rule_unifier(const Location& rule, const Node& rulebody) const;
+    void init_from_body(
+      const Node& rulebody, std::vector<Node>& statements, std::size_t root);
+    std::size_t add_variable(const Node& local);
+    std::size_t add_unifyexpr(const Node& unifyexpr);
+    void add_withpush(const Node& withpush);
+    void add_withpop(const Node& withpop);
+    void reset();
+
+    void compute_dependency_scores();
+    std::size_t compute_dependency_score(
+      std::size_t index, std::set<size_t>& visited);
+    std::size_t dependency_score(const Variable& var) const;
+    std::size_t dependency_score(const Node& expr) const;
+    std::size_t detect_cycles() const;
+    bool has_cycle(std::size_t id) const;
+
+    Values evaluate(const Location& var, const Node& value);
+    Values evaluate_function(
+      const Location& var, const std::string& func_name, const Args& args);
+    Values resolve_var(const Node& var, bool exclude_with = false);
+    Values enumerate(const Location& var, const Node& container);
+    Values resolve_skip(const Node& skip);
+    Values check_with(const Node& var, bool bypass_recurse_check = false);
+    Values apply_access(const Location& var, const Values& args);
+    std::optional<Value> call_function(
+      const Location& var, const Values& args) const;
+    std::optional<Value> call_named_function(
+      const Location& var, const std::string& name, const Values& args);
+    bool is_local(const Node& var);
+    std::size_t scan_vars(const Node& expr, std::vector<Location>& locals);
+    void pass();
+    void execute_statements(Nodes::iterator begin, Nodes::iterator end);
+    void remove_invalid_values();
+    void mark_invalid_values();
+    Variable& get_variable(const Location& name);
+    bool is_variable(const Location& name) const;
+    Node bind_variables();
+    Args create_args(const Node& args);
+    bool would_recurse(const Node& node);
+
+    bool push_rule(const Location& rule);
+    void pop_rule(const Location& rule);
+
+    void push_with(const Node& withseq);
+    void pop_with();
+
+    void push_not();
+    void pop_not();
+
+    Location m_rule;
+    std::map<Location, Variable> m_variables;
+    std::vector<Node> m_statements;
+    NodeMap<std::vector<Node>> m_nested_statements;
+    CallStack m_call_stack;
+    WithStack m_with_stack;
+    BuiltIns m_builtins;
+    std::size_t m_retries;
+    Token m_parent_type;
+    UnifierCache m_cache;
+    NodeMap<std::size_t> m_expr_ids;
+    std::vector<Dependency> m_dependency_graph;
+    bool m_negate;
+  };
+}
\ No newline at end of file
diff --git a/src/interpreter.cc b/src/interpreter.cc
index 50d096ed..4ac83570 100644
--- a/src/interpreter.cc
+++ b/src/interpreter.cc
@@ -1,16 +1,8 @@
-#include "interpreter.h"
-
-#include "errors.h"
-#include "helpers.h"
-#include "rego.h"
-#include "wf.h"
-
-#include <iostream>
-#include <sstream>
+#include "internal.hh"
 
 namespace rego
 {
-  Interpreter::Interpreter(bool disable_well_formed_checks) :
+  Interpreter::Interpreter() :
     m_parser(parser()),
     m_wf_parser(wf_parser),
     m_module_seq(NodeDef::create(ModuleSeq)),
@@ -18,7 +10,7 @@ namespace rego
     m_input(NodeDef::create(Input)),
     m_debug_path("."),
     m_debug_enabled(false),
-    m_well_formed_checks_enabled(!disable_well_formed_checks)
+    m_well_formed_checks_enabled(false)
   {
     wf::push_back(&wf_parser);
     m_builtins.register_standard_builtins();
@@ -29,6 +21,22 @@ namespace rego
     wf::pop_front();
   }
 
+  void Interpreter::insert_module(const Node& module)
+  {
+    // sort the modules by their package name. This allows us to merge
+    // modules with the same name after their imports are resolved.
+    auto pos = std::upper_bound(
+      m_module_seq->begin(), m_module_seq->end(), module, [](auto& a, auto& b) {
+        auto a_pkg = a->front();
+        auto b_pkg = b->front();
+        auto a_str = std::string(a_pkg->location().view());
+        auto b_str = std::string(b_pkg->location().view());
+        return a_pkg->location() < b_pkg->location();
+      });
+
+    m_module_seq->insert(pos, module);
+  }
+
   void Interpreter::add_module_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
@@ -36,8 +44,9 @@ namespace rego
       throw std::runtime_error("Module file does not exist");
     }
 
+    LOG("Adding module file: ", path);
     auto file_ast = m_parser.sub_parse(path);
-    m_module_seq->push_back(file_ast);
+    insert_module(file_ast);
   }
 
   void Interpreter::add_module(
@@ -45,7 +54,8 @@ namespace rego
   {
     auto module_source = SourceDef::synthetic(contents);
     auto module = m_parser.sub_parse(name, File, module_source);
-    m_module_seq->push_back(module);
+    insert_module(module);
+    LOG("Adding module: ", name, "(", contents.size(), " bytes)");
   }
 
   void Interpreter::add_data_json_file(const std::filesystem::path& path)
@@ -55,6 +65,7 @@ namespace rego
       throw std::runtime_error("Data file does not exist");
     }
 
+    LOG("Adding data file: ", path);
     auto file_ast = m_parser.sub_parse(path);
     m_data_seq->push_back(file_ast);
   }
@@ -64,49 +75,39 @@ namespace rego
     auto data_source = SourceDef::synthetic(json);
     auto data = m_parser.sub_parse("data", File, data_source);
     m_data_seq->push_back(data);
+    LOG("Adding data (", json.size(), " bytes)");
   }
 
   void Interpreter::add_data(const Node& node)
   {
     m_data_seq->push_back(node);
+    LOG("Adding data AST");
   }
 
-  void Interpreter::add_input_json_file(const std::filesystem::path& path)
+  void Interpreter::set_input_json_file(const std::filesystem::path& path)
   {
-    if (m_input->size() > 0)
-    {
-      throw std::runtime_error("Input already set");
-    }
-
     if (!std::filesystem::exists(path))
     {
       throw std::runtime_error("Input file does not exist");
     }
 
+    LOG("Setting input from file: ", path);
     auto file_ast = m_parser.sub_parse(path);
-    m_input->push_back(file_ast);
+    m_input = Input << file_ast;
   }
 
-  void Interpreter::add_input_json(const std::string& json)
+  void Interpreter::set_input_json(const std::string& json)
   {
-    if (m_input->size() > 0)
-    {
-      throw std::runtime_error("Input already set");
-    }
-
+    LOG("Setting input (", json.size(), " bytes)");
     auto input_source = SourceDef::synthetic(json);
-    auto input = m_parser.sub_parse("input", File, input_source);
-    m_input->push_back(input);
+    auto ast = m_parser.sub_parse("input", File, input_source);
+    m_input = Input << ast;
   }
 
-  void Interpreter::add_input(const Node& node)
+  void Interpreter::set_input(const Node& node)
   {
-    if (m_input->size() > 0)
-    {
-      throw std::runtime_error("Input already set");
-    }
-
-    m_input->push_back(node);
+    LOG("Setting input AST");
+    m_input = Input << node;
   }
 
   Node Interpreter::get_errors(const Node& node) const
@@ -135,6 +136,7 @@ namespace rego
 
   Node Interpreter::raw_query(const std::string& query_expr) const
   {
+    LOG("Query: ", query_expr);
     auto ast = NodeDef::create(Top);
     auto rego = NodeDef::create(rego::Rego);
     auto query_src = SourceDef::synthetic(query_expr);
@@ -144,21 +146,10 @@ namespace rego
       m_input->push_back(NodeDef::create(Undefined));
     }
 
-    // sort the modules by their package name. This will allow
-    // us to easily merge modules which are defined across multiple
-    // files.
-    std::sort(m_module_seq->begin(), m_module_seq->end(), [](auto& a, auto& b) {
-      auto a_pkg = a->front();
-      auto b_pkg = b->front();
-      auto a_str = std::string(a_pkg->location().view());
-      auto b_str = std::string(b_pkg->location().view());
-      return a_pkg->location() < b_pkg->location();
-    });
-
     rego->push_back(query);
-    rego->push_back(m_input);
-    rego->push_back(m_data_seq);
-    rego->push_back(m_module_seq);
+    rego->push_back(m_input->clone());
+    rego->push_back(m_data_seq->clone());
+    rego->push_back(m_module_seq->clone());
     ast->push_back(rego);
 
     bool ok = m_wf_parser.build_st(ast, std::cerr);
@@ -204,12 +195,14 @@ namespace rego
           error << "Failed at pass " << pass_name << std::endl;
           ast->errors(error);
           errors->push_back(err(ast, error.str(), "well_formed_error"));
+          LOG(error.str());
         }
 
         return errors;
       }
     }
 
+    LOG("Query result: ", ast);
     return ast;
   }
 
diff --git a/src/lang.cc b/src/lang.cc
index 119dc910..d71e8439 100644
--- a/src/lang.cc
+++ b/src/lang.cc
@@ -1,8 +1,4 @@
-#include "log.h"
-#include "passes.h"
-#include "rego.h"
-#include "tokens.h"
-#include "wf.h"
+#include "internal.hh"
 
 namespace rego
 {
@@ -30,6 +26,7 @@ namespace rego
       {"symbols", symbols(), &wf_pass_symbols},
       {"replace_argvals", replace_argvals(), &wf_pass_replace_argvals},
       {"lift_query", lift_query(), &wf_pass_lift_query},
+      {"expand_imports", expand_imports(), &wf_pass_expand_imports},
       {"constants", constants(), &wf_pass_constants},
       {"explicit_enums", explicit_enums(), &wf_pass_explicit_enums},
       {"body_locals", body_locals(builtins), &wf_pass_locals},
@@ -83,6 +80,7 @@ namespace rego
         {"symbols", symbols(), wf_pass_symbols},
         {"replace_argvals", replace_argvals(), wf_pass_replace_argvals},
         {"lift_query", lift_query(), wf_pass_lift_query},
+        {"expand_imports", expand_imports(), wf_pass_expand_imports},
         {"constants", constants(), wf_pass_constants},
         {"explicit_enums", explicit_enums(), wf_pass_explicit_enums},
         {"body_locals", body_locals(builtins), wf_pass_locals},
@@ -110,4 +108,9 @@ namespace rego
       });
     return d;
   }
+
+  void set_logging_enabled(bool enabled)
+  {
+    Logger::enabled = enabled;
+  }
 }
\ No newline at end of file
diff --git a/src/parse.cc b/src/parse.cc
index 7c78b1fa..6d9ad117 100644
--- a/src/parse.cc
+++ b/src/parse.cc
@@ -1,4 +1,4 @@
-#include "tokens.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/absolute_refs.cc b/src/passes/absolute_refs.cc
index aa6ed309..1201948b 100644
--- a/src/passes/absolute_refs.cc
+++ b/src/passes/absolute_refs.cc
@@ -1,27 +1,10 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-
-#include <set>
+#include "internal.hh"
 
 namespace
 {
   using namespace rego;
   using namespace wf::ops;
 
-  bool is_ref_to_type(const Node& var, const std::set<Token>& types)
-  {
-    Nodes defs = var->lookup();
-    if (defs.size() > 0)
-    {
-      return types.contains(defs[0]->type());
-    }
-    else
-    {
-      return false;
-    }
-  }
-
   Node build_ref(Node leaf)
   {
     if (leaf->type() == Data)
@@ -69,34 +52,6 @@ namespace rego
   PassDef absolute_refs()
   {
     return {
-      In(RefTerm, RuleRef) * T(Var)[Var]([](auto& n) {
-        return is_ref_to_type(*n.first, {Import});
-      }) >>
-        [](Match& _) {
-          // <import>
-          Nodes defs = _(Var)->lookup();
-          Node import = defs[0];
-          Node ref = import / Ref;
-          return ref->clone();
-        },
-
-      In(RefTerm, RuleRef) *
-          (T(Ref)
-           << ((T(RefHead) << T(Var)[Var](
-                  [](auto& n) { return is_ref_to_type(*n.first, {Import}); })) *
-               T(RefArgSeq)[RefArgSeq])) >>
-        [](Match& _) {
-          // <import>.dot <import>[brack]
-          Nodes defs = _(Var)->lookup();
-          Node import = defs[0];
-          Node ref = import / Ref;
-          Node refhead = (ref / RefHead)->clone();
-          Node refargseq = (ref / RefArgSeq)->clone();
-          refargseq->insert(
-            refargseq->end(), _(RefArgSeq)->begin(), _(RefArgSeq)->end());
-          return Ref << refhead << refargseq;
-        },
-
       In(RefTerm, RuleRef) * T(Var)[Var]([](auto& n) {
         return is_ref_to_type(*n.first, RuleTypes);
       }) >>
diff --git a/src/passes/arithmetic.cc b/src/passes/arithmetic.cc
index e2c4a744..4dfafc57 100644
--- a/src/passes/arithmetic.cc
+++ b/src/passes/arithmetic.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/assign.cc b/src/passes/assign.cc
index fcac7c60..9264f707 100644
--- a/src/passes/assign.cc
+++ b/src/passes/assign.cc
@@ -1,8 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-
-#include <sstream>
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/build_calls.cc b/src/passes/build_calls.cc
index 9ea90199..13106af4 100644
--- a/src/passes/build_calls.cc
+++ b/src/passes/build_calls.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/build_refs.cc b/src/passes/build_refs.cc
index 5ed736ce..81e8d32d 100644
--- a/src/passes/build_refs.cc
+++ b/src/passes/build_refs.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/comparison.cc b/src/passes/comparison.cc
index be089ae0..fc37525b 100644
--- a/src/passes/comparison.cc
+++ b/src/passes/comparison.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/compr.cc b/src/passes/compr.cc
index fd823dd2..606adf2c 100644
--- a/src/passes/compr.cc
+++ b/src/passes/compr.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/constants.cc b/src/passes/constants.cc
index 02e3e496..c702b9fc 100644
--- a/src/passes/constants.cc
+++ b/src/passes/constants.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-#include "tokens.h"
+#include "internal.hh"
 
 namespace rego
 {
@@ -172,6 +169,22 @@ namespace rego
                         << (LiteralNot << _(UnifyBody)))));
         },
 
+      In(ModuleSeq) * (T(Module)[Lhs] * T(Module)[Rhs])([](auto& n) {
+        Node lhs = n.first[0];
+        Node rhs = n.first[1];
+        auto lhs_package = flatten_ref((lhs / Package)->front());
+        auto rhs_package = flatten_ref((rhs / Package)->front());
+
+        return lhs_package == rhs_package;
+      }) >>
+        [](Match& _) {
+          Node pkg = _(Lhs) / Package;
+          Node policy = _(Lhs) / Policy;
+          Node rhs_policy = _(Rhs) / Policy;
+          policy->push_back({rhs_policy->begin(), rhs_policy->end()});
+          return Module << pkg << policy;
+        },
+
       // errors
     };
   }
diff --git a/src/passes/enumerate.cc b/src/passes/enumerate.cc
index 33d03993..7521a905 100644
--- a/src/passes/enumerate.cc
+++ b/src/passes/enumerate.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/expand_imports.cc b/src/passes/expand_imports.cc
new file mode 100644
index 00000000..5d47fb60
--- /dev/null
+++ b/src/passes/expand_imports.cc
@@ -0,0 +1,38 @@
+#include "internal.hh"
+
+namespace rego
+{
+  // Expands imports to their fully-qualified values.
+  PassDef expand_imports()
+  {
+    return {
+      In(RefTerm, RuleRef) * T(Var)[Var]([](auto& n) {
+        return is_ref_to_type(*n.first, {Import});
+      }) >>
+        [](Match& _) {
+          // <import>
+          Nodes defs = _(Var)->lookup();
+          Node import = defs[0];
+          Node ref = import / Ref;
+          return ref->clone();
+        },
+
+      In(RefTerm, RuleRef) *
+          (T(Ref)
+           << ((T(RefHead) << T(Var)[Var](
+                  [](auto& n) { return is_ref_to_type(*n.first, {Import}); })) *
+               T(RefArgSeq)[RefArgSeq])) >>
+        [](Match& _) {
+          // <import>.dot <import>[brack]
+          Nodes defs = _(Var)->lookup();
+          Node import = defs[0];
+          Node ref = import / Ref;
+          Node refhead = (ref / RefHead)->clone();
+          Node refargseq = (ref / RefArgSeq)->clone();
+          refargseq->insert(
+            refargseq->end(), _(RefArgSeq)->begin(), _(RefArgSeq)->end());
+          return Ref << refhead << refargseq;
+        },
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/passes/functions.cc b/src/passes/functions.cc
index 5f2c7702..dfb42d14 100644
--- a/src/passes/functions.cc
+++ b/src/passes/functions.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/if_else.cc b/src/passes/if_else.cc
index b69afa0e..cb487696 100644
--- a/src/passes/if_else.cc
+++ b/src/passes/if_else.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/imports.cc b/src/passes/imports.cc
index c5505722..1ffb93b1 100644
--- a/src/passes/imports.cc
+++ b/src/passes/imports.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/init.cc b/src/passes/init.cc
index 1fc810ae..90051387 100644
--- a/src/passes/init.cc
+++ b/src/passes/init.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/input_data.cc b/src/passes/input_data.cc
index 08d82947..4b38990f 100644
--- a/src/passes/input_data.cc
+++ b/src/passes/input_data.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/keywords.cc b/src/passes/keywords.cc
index 16f5ac69..27853fb1 100644
--- a/src/passes/keywords.cc
+++ b/src/passes/keywords.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/lift_query.cc b/src/passes/lift_query.cc
index 0139d575..b37498cb 100644
--- a/src/passes/lift_query.cc
+++ b/src/passes/lift_query.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/lift_refheads.cc b/src/passes/lift_refheads.cc
index c794cfe4..66552ee5 100644
--- a/src/passes/lift_refheads.cc
+++ b/src/passes/lift_refheads.cc
@@ -1,5 +1,4 @@
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
@@ -152,7 +151,7 @@ namespace rego
           else
           {
             Location loc = vararg->front()->location();
-            var = Var ^ "[" + std::string(loc.view()) + "]";
+            var = Var ^ ("[" + std::string(loc.view()) + "]");
           }
 
           rulehead->replace(ruleref, RuleRef << var);
diff --git a/src/passes/lift_to_rule.cc b/src/passes/lift_to_rule.cc
index 40ce79ed..078483db 100644
--- a/src/passes/lift_to_rule.cc
+++ b/src/passes/lift_to_rule.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/lists.cc b/src/passes/lists.cc
index e08cf7e2..548745a2 100644
--- a/src/passes/lists.cc
+++ b/src/passes/lists.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/locals.cc b/src/passes/locals.cc
index 23f7900b..2c53d6e6 100644
--- a/src/passes/locals.cc
+++ b/src/passes/locals.cc
@@ -1,9 +1,4 @@
-#include "builtins.h"
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-
-#include <map>
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/membership.cc b/src/passes/membership.cc
index 8c6f2d37..d9d89831 100644
--- a/src/passes/membership.cc
+++ b/src/passes/membership.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/merge_data.cc b/src/passes/merge_data.cc
index f308f717..0d9a21ac 100644
--- a/src/passes/merge_data.cc
+++ b/src/passes/merge_data.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/merge_modules.cc b/src/passes/merge_modules.cc
index e2781d6c..ee736f3f 100644
--- a/src/passes/merge_modules.cc
+++ b/src/passes/merge_modules.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/modules.cc b/src/passes/modules.cc
index ccd309d7..a229853e 100644
--- a/src/passes/modules.cc
+++ b/src/passes/modules.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
@@ -55,38 +53,6 @@ namespace rego
       In(Module) * (T(ImportSeq)[ImportSeq] * T(Import)[Import]) >>
         [](Match& _) { return ImportSeq << *_[ImportSeq] << _(Import); },
 
-      In(ModuleSeq) * (T(Module)[Lhs] * T(Module)[Rhs])([](auto& n) {
-        Node lhs = *n.first;
-        Node rhs = *(n.first + 1);
-        Node lhs_pkg = (lhs / Package / Group);
-        Node rhs_pkg = (rhs / Package / Group);
-        if (lhs_pkg->size() != rhs_pkg->size())
-        {
-          return false;
-        }
-
-        for (std::size_t i = 0; i < lhs_pkg->size(); ++i)
-        {
-          if (lhs_pkg->at(i)->location() != rhs_pkg->at(i)->location())
-          {
-            return false;
-          }
-        }
-
-        return true;
-      }) >>
-        [](Match& _) {
-          Node pkg = _(Lhs) / Package;
-          Node imports = _(Lhs) / ImportSeq;
-          Node policy = _(Lhs) / Policy;
-          Node rhs_imports = _(Rhs) / ImportSeq;
-          Node rhs_policy = _(Rhs) / Policy;
-          imports->insert(
-            imports->end(), rhs_imports->begin(), rhs_imports->end());
-          policy->insert(policy->end(), rhs_policy->begin(), rhs_policy->end());
-          return Module << pkg << imports << policy;
-        },
-
       T(Placeholder) >>
         [](Match& _) {
           Location temp = _.fresh({"_"});
diff --git a/src/passes/query.cc b/src/passes/query.cc
index 225e9a95..93dbb6ad 100644
--- a/src/passes/query.cc
+++ b/src/passes/query.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/replace_argvals.cc b/src/passes/replace_argvals.cc
index cd8a4e05..a28c11e6 100644
--- a/src/passes/replace_argvals.cc
+++ b/src/passes/replace_argvals.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-#include "tokens.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/rulebody.cc b/src/passes/rulebody.cc
index 84999fac..c8f713ae 100644
--- a/src/passes/rulebody.cc
+++ b/src/passes/rulebody.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/rules.cc b/src/passes/rules.cc
index b2e6f21f..21d5ed12 100644
--- a/src/passes/rules.cc
+++ b/src/passes/rules.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/simple_refs.cc b/src/passes/simple_refs.cc
index cff6fc2d..2ed84c77 100644
--- a/src/passes/simple_refs.cc
+++ b/src/passes/simple_refs.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/passes/skip_refs.cc b/src/passes/skip_refs.cc
index d746acc4..1629962f 100644
--- a/src/passes/skip_refs.cc
+++ b/src/passes/skip_refs.cc
@@ -1,7 +1,4 @@
-#include "helpers.h"
-#include "passes.h"
-
-#include <set>
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/skips.cc b/src/passes/skips.cc
index 0ae47caf..57ff1d99 100644
--- a/src/passes/skips.cc
+++ b/src/passes/skips.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/strings.cc b/src/passes/strings.cc
index bda439c7..71bd0daf 100644
--- a/src/passes/strings.cc
+++ b/src/passes/strings.cc
@@ -1,6 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/structure.cc b/src/passes/structure.cc
index 841f1c80..787c9fe6 100644
--- a/src/passes/structure.cc
+++ b/src/passes/structure.cc
@@ -1,7 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-#include "resolver.h"
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/symbols.cc b/src/passes/symbols.cc
index e52135ea..71908b10 100644
--- a/src/passes/symbols.cc
+++ b/src/passes/symbols.cc
@@ -1,9 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "passes.h"
-#include "resolver.h"
-
-#include <sstream>
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/passes/unify.cc b/src/passes/unify.cc
index 04cb634f..0db157b6 100644
--- a/src/passes/unify.cc
+++ b/src/passes/unify.cc
@@ -1,8 +1,4 @@
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "passes.h"
-#include "resolver.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/rego_c.cc b/src/rego_c.cc
index cb6c37a3..6f5c6ebe 100644
--- a/src/rego_c.cc
+++ b/src/rego_c.cc
@@ -1,8 +1,4 @@
-#include "rego_c.h"
-
-#include "helpers.h"
-#include "interpreter.h"
-#include "log.h"
+#include "internal.hh"
 
 namespace rego
 {
@@ -27,7 +23,7 @@ extern "C"
 
   void regoSetLoggingEnabled(regoBoolean enabled)
   {
-    rego::Logger::enabled = enabled;
+    rego::set_logging_enabled(enabled);
   }
 
   regoInterpreter* regoNew()
@@ -97,11 +93,11 @@ extern "C"
     }
   }
 
-  regoEnum regoAddInputJSONFile(regoInterpreter* rego, const char* path)
+  regoEnum regoSetInputJSONFile(regoInterpreter* rego, const char* path)
   {
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_input_json_file(path);
+      reinterpret_cast<rego::Interpreter*>(rego)->set_input_json_file(path);
       return REGO_OK;
     }
     catch (const std::exception& e)
@@ -111,11 +107,11 @@ extern "C"
     }
   }
 
-  regoEnum regoAddInputJSON(regoInterpreter* rego, const char* contents)
+  regoEnum regoSetInputJSON(regoInterpreter* rego, const char* contents)
   {
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_input_json(contents);
+      reinterpret_cast<rego::Interpreter*>(rego)->set_input_json(contents);
       return REGO_OK;
     }
     catch (const std::exception& e)
@@ -135,9 +131,18 @@ extern "C"
     return reinterpret_cast<rego::Interpreter*>(rego)->debug_enabled();
   }
 
-  void regoSetDebugPath(regoInterpreter* rego, const char* path)
+  regoEnum regoSetDebugPath(regoInterpreter* rego, const char* path)
   {
-    reinterpret_cast<rego::Interpreter*>(rego)->debug_path(path);
+    try
+    {
+      reinterpret_cast<rego::Interpreter*>(rego)->debug_path(path);
+      return REGO_OK;
+    }
+    catch (const std::exception& e)
+    {
+      rego::setError(rego, e.what());
+      return REGO_ERROR;
+    }
   }
 
   void regoSetWellFormedChecksEnabled(
@@ -153,11 +158,6 @@ extern "C"
       ->well_formed_checks_enabled();
   }
 
-  void regoSetExecutable(regoInterpreter* rego, const char* path)
-  {
-    reinterpret_cast<rego::Interpreter*>(rego)->executable(path);
-  }
-
   regoOutput* regoQuery(regoInterpreter* rego, const char* query_expr)
   {
     try
@@ -176,6 +176,19 @@ extern "C"
     }
   }
 
+  void regoSetStrictBuiltInErrors(regoInterpreter* rego, regoBoolean enabled)
+  {
+    reinterpret_cast<rego::Interpreter*>(rego)->builtins().strict_errors(
+      enabled);
+  }
+
+  regoBoolean regoGetStrictBuiltInErrors(regoInterpreter* rego)
+  {
+    return reinterpret_cast<rego::Interpreter*>(rego)
+      ->builtins()
+      .strict_errors();
+  }
+
   // Output functions
   regoBoolean regoOutputOk(regoOutput* output)
   {
@@ -321,6 +334,11 @@ extern "C"
       return REGO_NODE_ERROR_CODE;
     }
 
+    if (node->type() == rego::ErrorSeq)
+    {
+      return REGO_NODE_ERROR_SEQ;
+    }
+
     return REGO_NODE_INTERNAL;
   }
 
@@ -356,10 +374,14 @@ extern "C"
     return static_cast<regoSize>(size);
   }
 
-  regoNode* regoNodeGet(regoNode* node, regoSize index)
+  regoNode* regoNodeGet(regoNode* node_ptr, regoSize index)
   {
-    trieste::NodeDef* child =
-      reinterpret_cast<trieste::NodeDef*>(node)->at(index).get();
+    trieste::NodeDef* node = reinterpret_cast<trieste::NodeDef*>(node_ptr);
+    if (index >= node->size())
+    {
+      return nullptr;
+    }
+    trieste::NodeDef* child = node->at(index).get();
     return reinterpret_cast<regoNode*>(child);
   }
 
@@ -380,6 +402,7 @@ extern "C"
     }
 
     json.copy(buffer, size);
+    buffer[json.size()] = '\0';
     return REGO_OK;
   }
 }
\ No newline at end of file
diff --git a/src/resolver.cc b/src/resolver.cc
index af361022..006f12df 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -1,9 +1,4 @@
-#include "resolver.h"
-
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "unifier.h"
+#include "internal.hh"
 
 namespace
 {
@@ -800,31 +795,29 @@ namespace rego
     return expr_str(statement);
   }
 
-  Resolver::NodePrinter Resolver::expr_str(const Node& unifyexpr)
+  Resolver::NodePrinter Resolver::expr_str(const Node& node)
   {
-    return {
-      unifyexpr, [](std::ostream& os, const Node& unifyexpr) -> std::ostream& {
-        Node lhs = unifyexpr / Var;
-        Node rhs = unifyexpr / Val;
-        os << lhs->location().view() << " = "
-           << (rhs->type() == Function ? func_str(rhs) : arg_str(rhs));
-        return os;
-      }};
+    return {node, [](std::ostream& os, const Node& unifyexpr) -> std::ostream& {
+              Node lhs = unifyexpr / Var;
+              Node rhs = unifyexpr / Val;
+              os << lhs->location().view() << " = "
+                 << (rhs->type() == Function ? func_str(rhs) : arg_str(rhs));
+              return os;
+            }};
   }
 
-  Resolver::NodePrinter Resolver::term_str(const Node& term)
+  Resolver::NodePrinter Resolver::term_str(const Node& node)
   {
-    return {term, [](std::ostream& os, const Node& term) -> std::ostream& {
+    return {node, [](std::ostream& os, const Node& term) -> std::ostream& {
               os << term->type().str() << "(" << to_json(term) << ")";
               return os;
             }};
   }
 
-  Resolver::NodePrinter Resolver::with_str(const Node& unifyexprwith)
+  Resolver::NodePrinter Resolver::with_str(const Node& node)
   {
     return {
-      unifyexprwith,
-      [](std::ostream& os, const Node& unifyexprwith) -> std::ostream& {
+      node, [](std::ostream& os, const Node& unifyexprwith) -> std::ostream& {
         Node unifybody = unifyexprwith / UnifyBody;
         os << "{";
         std::string sep = "";
@@ -855,11 +848,10 @@ namespace rego
       }};
   }
 
-  Resolver::NodePrinter Resolver::compr_str(const Node& unifyexprcompr)
+  Resolver::NodePrinter Resolver::compr_str(const Node& node)
   {
     return {
-      unifyexprcompr,
-      [](std::ostream& os, const Node& unifyexprcompr) -> std::ostream& {
+      node, [](std::ostream& os, const Node& unifyexprcompr) -> std::ostream& {
         Node lhs = unifyexprcompr / Var;
         Node rhs = unifyexprcompr / Val;
         Node unifybody = unifyexprcompr / UnifyBody;
@@ -878,11 +870,10 @@ namespace rego
       }};
   }
 
-  Resolver::NodePrinter Resolver::enum_str(const Node& unifyexprenum)
+  Resolver::NodePrinter Resolver::enum_str(const Node& node)
   {
     return {
-      unifyexprenum,
-      [](std::ostream& os, const Node& unifyexprenum) -> std::ostream& {
+      node, [](std::ostream& os, const Node& unifyexprenum) -> std::ostream& {
         Node item = unifyexprenum / Item;
         Node itemseq = unifyexprenum / ItemSeq;
         Node unifybody = unifyexprenum / NestedBody / UnifyBody;
@@ -902,11 +893,10 @@ namespace rego
       }};
   }
 
-  Resolver::NodePrinter Resolver::not_str(const Node& unifyexprnot)
+  Resolver::NodePrinter Resolver::not_str(const Node& node)
   {
     return {
-      unifyexprnot,
-      [](std::ostream& os, const Node& unifyexprnot) -> std::ostream& {
+      node, [](std::ostream& os, const Node& unifyexprnot) -> std::ostream& {
         Node unifybody = unifyexprnot->front();
         os << "not {";
         std::string sep = "";
@@ -923,27 +913,26 @@ namespace rego
       }};
   }
 
-  Resolver::NodePrinter Resolver::func_str(const Node& function)
+  Resolver::NodePrinter Resolver::func_str(const Node& node)
   {
-    return {
-      function, [](std::ostream& os, const Node& function) -> std::ostream& {
-        Node name = function / JSONString;
-        Node args = function / ArgSeq;
-        os << name->location().view() << "(";
-        std::string sep = "";
-        for (const auto& child : *args)
-        {
-          os << sep << arg_str(child);
-          sep = ", ";
-        }
-        os << ")";
-        return os;
-      }};
+    return {node, [](std::ostream& os, const Node& function) -> std::ostream& {
+              Node name = function / JSONString;
+              Node args = function / ArgSeq;
+              os << name->location().view() << "(";
+              std::string sep = "";
+              for (const auto& child : *args)
+              {
+                os << sep << arg_str(child);
+                sep = ", ";
+              }
+              os << ")";
+              return os;
+            }};
   }
 
-  Resolver::NodePrinter Resolver::arg_str(const Node& arg)
+  Resolver::NodePrinter Resolver::arg_str(const Node& node)
   {
-    return {arg, [](std::ostream& os, const Node& arg) -> std::ostream& {
+    return {node, [](std::ostream& os, const Node& arg) -> std::ostream& {
               if (arg->type() == Var)
               {
                 os << arg->location().view();
@@ -982,13 +971,13 @@ namespace rego
             }};
   }
 
-  Resolver::NodePrinter Resolver::ref_str(const Node& ref)
+  Resolver::NodePrinter Resolver::ref_str(const Node& node)
   {
-    return {ref, [](std::ostream& os, const Node& node) -> std::ostream& {
-              Node ref = node;
-              if (node->type() == RuleRef || node->type() == RefTerm)
+    return {node, [](std::ostream& os, const Node& ref_) -> std::ostream& {
+              Node ref = ref_;
+              if (ref->type() == RuleRef || ref->type() == RefTerm)
               {
-                ref = node->front();
+                ref = ref->front();
                 if (ref->type() == Var)
                 {
                   os << ref->location().view();
@@ -1327,31 +1316,31 @@ namespace rego
     return buf.str();
   }
 
-  Resolver::NodePrinter Resolver::body_str(const Node& unifybody)
+  Resolver::NodePrinter Resolver::body_str(const Node& node)
   {
-    return {
-      unifybody, [](std::ostream& os, const Node& unifybody) -> std::ostream& {
-        os << "{" << std::endl;
-        for (auto& stmt : *unifybody)
-        {
-          if (stmt->type() == Local)
-          {
-            os << "  local " << (stmt / Var)->location().view() << std::endl;
-          }
-          else
-          {
-            os << "  " << stmt_str(stmt) << std::endl;
-          }
-        }
-        os << "}";
-        return os;
-      }};
+    return {node, [](std::ostream& os, const Node& unifybody) -> std::ostream& {
+              os << "{" << std::endl;
+              for (auto& stmt : *unifybody)
+              {
+                if (stmt->type() == Local)
+                {
+                  os << "  local " << (stmt / Var)->location().view()
+                     << std::endl;
+                }
+                else
+                {
+                  os << "  " << stmt_str(stmt) << std::endl;
+                }
+              }
+              os << "}";
+              return os;
+            }};
   }
 
-  Resolver::NodePrinter Resolver::rego_str(const Node& rego)
+  Resolver::NodePrinter Resolver::rego_str(const Node& node)
   {
     return {
-      rego, [](std::ostream& os, const Node& rego) -> std::ostream& {
+      node, [](std::ostream& os, const Node& rego) -> std::ostream& {
         std::deque<Node> queue;
         queue.push_back(rego / Data);
         os << std::endl << std::endl;
diff --git a/src/unifier.cc b/src/unifier.cc
index bb361f57..0647955b 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -1,17 +1,4 @@
-#include "unifier.h"
-
-#include "CLI/TypeTools.hpp"
-#include "args.h"
-#include "errors.h"
-#include "helpers.h"
-#include "log.h"
-#include "resolver.h"
-#include "tokens.h"
-
-#include <algorithm>
-#include <iostream>
-#include <sstream>
-#include <type_traits>
+#include "internal.hh"
 
 namespace rego
 {
@@ -781,7 +768,9 @@ namespace rego
         source_values.begin(),
         source_values.end(),
         std::back_inserter(values),
-        [var](auto& value) { return ValueDef::copy_to(value, var); });
+        [var](auto& source_value) {
+          return ValueDef::copy_to(source_value, var);
+        });
     }
     else if (value->type() == Scalar)
     {
diff --git a/src/value.cc b/src/value.cc
index c9f1a456..6672aa31 100644
--- a/src/value.cc
+++ b/src/value.cc
@@ -1,10 +1,4 @@
-#include "value.h"
-
-#include "errors.h"
-#include "helpers.h"
-#include "resolver.h"
-
-#include <sstream>
+#include "internal.hh"
 
 namespace
 {
diff --git a/src/value_map.cc b/src/value_map.cc
index 902d94e7..ed62adfb 100644
--- a/src/value_map.cc
+++ b/src/value_map.cc
@@ -1,6 +1,4 @@
-#include "value_map.h"
-
-#include "helpers.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/variable.cc b/src/variable.cc
index ea060061..94caa116 100644
--- a/src/variable.cc
+++ b/src/variable.cc
@@ -1,7 +1,4 @@
-#include "variable.h"
-
-#include "helpers.h"
-#include "log.h"
+#include "internal.hh"
 
 namespace rego
 {
diff --git a/src/wf.cc b/src/wf.cc
deleted file mode 100644
index a0a2d697..00000000
--- a/src/wf.cc
+++ /dev/null
@@ -1,4 +0,0 @@
-#include "wf.h"
-
-// this file exists to help the clangd vs-code exception figure out
-// how to find the headers that `wf.h` depends on.
diff --git a/tools/driver.cc b/tools/driver.cc
index 954bd5c7..f4a077b5 100644
--- a/tools/driver.cc
+++ b/tools/driver.cc
@@ -1,4 +1,4 @@
-#include <rego/rego.h>
+#include <rego/rego.hh>
 
 int main(int argc, char** argv)
 {
diff --git a/tools/main.cc b/tools/main.cc
index 90377344..46b076fa 100644
--- a/tools/main.cc
+++ b/tools/main.cc
@@ -1,5 +1,5 @@
 #include <CLI/CLI.hpp>
-#include <rego/rego.h>
+#include <rego/rego.hh>
 
 int main(int argc, char** argv)
 {
@@ -20,6 +20,9 @@ int main(int argc, char** argv)
   std::filesystem::path input_path;
   app.add_option("-i,--input", input_path, "Input JSON file");
 
+  bool wf_checks{false};
+  app.add_flag("-w,--wf", wf_checks, "Enable well-formedness checks");
+
   std::filesystem::path output;
   app.add_option("-a,--ast", output, "Output the AST");
 
@@ -35,10 +38,11 @@ int main(int argc, char** argv)
     return app.exit(e);
   }
 
-  rego::Logger::enabled = enable_logging;
+  rego::set_logging_enabled(enable_logging);
 
   auto interpreter = rego::Interpreter();
   interpreter.executable(argv[0]);
+  interpreter.well_formed_checks_enabled(wf_checks);
 
   if (!input_path.empty())
   {
@@ -49,7 +53,7 @@ int main(int argc, char** argv)
       return 1;
     }
 
-    interpreter.add_input_json_file(input_path);
+    interpreter.set_input_json_file(input_path);
   }
 
   for (auto& path : data_paths)
diff --git a/wrappers/rust/regorust/.gitignore b/wrappers/rust/regorust/.gitignore
new file mode 100644
index 00000000..d8996a7c
--- /dev/null
+++ b/wrappers/rust/regorust/.gitignore
@@ -0,0 +1,18 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# Rego
+rego-cpp
+rego
\ No newline at end of file
diff --git a/wrappers/rust/regorust/Cargo.toml b/wrappers/rust/regorust/Cargo.toml
new file mode 100644
index 00000000..6ff76933
--- /dev/null
+++ b/wrappers/rust/regorust/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "regorust"
+version = "0.3.4"
+edition = "2021"
+description = "Rust bindings for the rego-cpp Rego compiler and interpreter"
+license-file = "LICENSE"
+readme = "README.md"
+homepage = "https://github.com/microsoft/rego-cpp"
+repository = "https://github.com/microsoft/rego-cpp"
+
+[dependencies]
+
+[build-dependencies]
+bindgen = "*"
diff --git a/wrappers/rust/regorust/LICENSE b/wrappers/rust/regorust/LICENSE
new file mode 100644
index 00000000..9e841e7a
--- /dev/null
+++ b/wrappers/rust/regorust/LICENSE
@@ -0,0 +1,21 @@
+    MIT License
+
+    Copyright (c) Microsoft Corporation.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in all
+    copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+    SOFTWARE
diff --git a/wrappers/rust/regorust/README.md b/wrappers/rust/regorust/README.md
new file mode 100644
index 00000000..68679001
--- /dev/null
+++ b/wrappers/rust/regorust/README.md
@@ -0,0 +1,14 @@
+# `regorust`
+
+[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
+is the native query language of the Open Policy Agent project. If you want to
+learn more about Rego as a language, and its various use cases, we refer
+you to the language documentation above which OPA provides.
+
+This crate is a wrapper around `rego-cpp`, an open source cross-platform C++
+implementation of the Rego language compiler and runtime developed and maintained
+by Microsoft. You can learn more about that project
+[here](https://github.com/microsoft/rego-cpp). As much as possible in this
+wrapper we try to provide idiomatic Rust interfaces to the Rego query engine.
+We hope the project is of use to those wishing to leverage the power of Rego
+within a Rust context.
diff --git a/wrappers/rust/regorust/build.rs b/wrappers/rust/regorust/build.rs
new file mode 100644
index 00000000..f7c44fa9
--- /dev/null
+++ b/wrappers/rust/regorust/build.rs
@@ -0,0 +1,105 @@
+use std::env;
+use std::num::NonZeroUsize;
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() {
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    let regocpp_path = out_path.join("rego-cpp");
+    let num_procs = std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
+    let rego_git_repo = std::env::var("REGOCPP_C_WRAPPER_REPO")
+        .unwrap_or("https://github.com/microsoft/rego-cpp.git".to_string());
+    let rego_git_tag = std::env::var("REGOCPP_C_WRAPPER_TAG").unwrap_or("main".to_string());
+
+    if !regocpp_path.exists() {
+        Command::new("git")
+            .args(&["clone", &rego_git_repo])
+            .current_dir(&out_path)
+            .status()
+            .expect("failed to execute process");
+    } else {
+        Command::new("git")
+            .args(&["pull", "--rebase"])
+            .current_dir(&regocpp_path)
+            .status()
+            .expect("failed to execute process");
+    }
+
+    Command::new("git")
+        .args(&["checkout", &rego_git_tag])
+        .current_dir(&regocpp_path)
+        .status()
+        .expect("failed to execute process");
+
+    let regocpp_config = match std::env::var("PROFILE").as_ref() {
+        Ok(build_type) => {
+            if build_type == "debug" {
+                "RelWithDebInfo"
+            } else {
+                "Release"
+            }
+        }
+        Err(_) => "Release",
+    };
+
+    Command::new("cmake")
+        .args(&[
+            "-S",
+            ".",
+            "-B",
+            "build",
+            "-DCMAKE_BUILD_TYPE={}"
+                .replace("{}", regocpp_config)
+                .as_str(),
+            "-DCMAKE_INSTALL_PREFIX=rust",
+            "-DREGOCPP_COPY_EXAMPLES=ON",
+        ])
+        .current_dir(&regocpp_path)
+        .status()
+        .expect("failed to execute process");
+
+    let regocpp_build_path = regocpp_path.join("build");
+
+    Command::new("cmake")
+        .args(&[
+            "--build",
+            ".",
+            "--config",
+            regocpp_config,
+            "--parallel",
+            &num_procs.to_string(),
+            "--target",
+            "install",
+        ])
+        .current_dir(&regocpp_build_path)
+        .status()
+        .expect("failed to execute process");
+
+    let rego_path = regocpp_path.join("rust");
+    let libdir_path = rego_path.join("lib");
+    let headers_path = rego_path.join("include").join("rego").join("rego_c.h");
+    let headers_path_str = headers_path.to_str().expect("Path is not a valid string");
+
+    println!("cargo:rustc-link-search={}", libdir_path.to_str().unwrap());
+    println!("cargo:rustc-link-lib=static=re2");
+    println!("cargo:rustc-link-lib=static=snmallocshim-static");
+    println!("cargo:rustc-link-lib=static=rego");
+    if cfg!(windows) {
+        println!("cargo:rustc-link-arg=mincore.lib");
+    } else if (cfg!(target_os = "macos")) {
+        println!("cargo:rustc-link-lib=c++");
+    } else {
+        println!("cargo:rustc-link-lib=stdc++");
+    }
+    println!("cargo:rerun-if-changed={}", headers_path.to_str().unwrap());
+
+    let bindings = bindgen::Builder::default()
+        .header(headers_path_str)
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        .generate()
+        .expect("Unable to generate bindings");
+
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
diff --git a/wrappers/rust/regorust/src/lib.rs b/wrappers/rust/regorust/src/lib.rs
new file mode 100644
index 00000000..f087542b
--- /dev/null
+++ b/wrappers/rust/regorust/src/lib.rs
@@ -0,0 +1,1409 @@
+#![allow(non_camel_case_types)]
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+
+use std::ffi::{CStr, CString};
+use std::ops::Index;
+use std::path::Path;
+
+/// Interface for the Rego interpreter.
+///
+/// This wraps the Rego C API, and handles passing calls to
+/// the C API and converting the results to Rust types.
+///
+/// # Examples
+/// ```
+/// # use regorust::*;
+/// let rego = Interpreter::new();
+/// match rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4") {
+///   Ok(result) => {
+///     let x = result.binding("x").expect("cannot get x");
+///     let y = result.binding("y").expect("cannot get y");
+///     println!("x = {}", x.json().unwrap());
+///     println!("y = {}", y.json().unwrap());
+///     # assert_eq!(x.json().expect("cannot convert x to JSON"), "5");
+///     # assert_eq!(y.json().expect("cannot convert y to JSON"), "9.4");
+///   }
+///   Err(e) => {
+///     panic!("error: {}", e);
+///   }
+/// }
+/// ```
+///
+/// ```
+/// # use regorust::*;
+/// let input = r#"
+///   {
+///     "a": 10,
+///     "b": "20",
+///     "c": 30.0,
+///     "d": true
+///   }
+/// "#;
+/// let data0 = r#"
+///   {
+///     "one": {
+///       "bar": "Foo",
+///       "baz": 5,
+///       "be": true,
+///       "bop": 23.4
+///     },
+///     "two": {
+///       "bar": "Bar",
+///       "baz": 12.3,
+///       "be": false,
+///       "bop": 42
+///     }
+///   }
+/// "#;
+/// let data1 = r#"
+///   {
+///     "three": {
+///       "bar": "Baz",
+///       "baz": 15,
+///       "be": true,
+///       "bop": 4.23
+///     }
+///   }        
+/// "#;
+/// let module = r#"
+///   package objects
+///
+///   rect := {`width`: 2, "height": 4}
+///   cube := {"width": 3, `height`: 4, "depth": 5}
+///   a := 42
+///   b := false
+///   c := null
+///   d := {"a": a, "x": [b, c]}
+///   index := 1
+///   shapes := [rect, cube]
+///   names := ["prod", `smoke1`, "dev"]
+///   sites := [{"name": "prod"}, {"name": names[index]}, {"name": "dev"}]
+///   e := {
+///     a: "foo",
+///     "three": c,
+///     names[2]: b,
+///     "four": d,
+///   }
+///   f := e["dev"]                
+/// "#;
+/// let rego = Interpreter::new();
+/// rego.set_input_json(input);
+/// rego.add_data_json(data0);
+/// rego.add_data_json(data1);
+/// rego.add_module("objects", module);
+/// match rego.query("x=[data.one, input.b, data.objects.sites[1]]") {
+///   Ok(result) => {
+///     println!("{}", result.to_str().unwrap());
+///     let x = result.binding("x").expect("cannot get x");
+///     let data_one = x.index(0).unwrap();
+///     if let NodeValue::String(bar) = data_one
+///         .lookup("bar")
+///         .unwrap()
+///         .value()
+///         .unwrap()
+///     {
+///       println!("data.one.bar = {}", bar);
+///       # assert_eq!(bar, "Foo");
+///     }
+///   }
+///   Err(e) => {
+///    panic!("error: {}", e);
+///   }
+/// }
+/// ```
+#[derive(Debug)]
+pub struct Interpreter {
+    c_ptr: *mut regoInterpreter,
+}
+
+/// Interface for the Rego output.
+///
+/// Outputs can either be examined as strings, or inspected
+/// using Rego Nodes. It is also possible to extract bindings
+/// for specific variables.
+///
+/// # Examples
+/// ```
+/// # use regorust::*;
+/// # let rego = Interpreter::new();
+/// # match rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4") {
+/// #   Ok(result) => {
+/// match result.to_str() {
+///   Ok(output_str) => {
+///     println!("{}", output_str);
+///   }
+///   Err(err_str) => {
+///     println!("error: {}", err_str);
+///   }
+/// }
+/// #   }
+/// #   Err(e) => {
+/// #     panic!("error: {}", e);
+/// #   }
+/// # }
+/// ```
+///
+/// ```
+/// # use regorust::*;
+/// # let rego = Interpreter::new();
+/// # match rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4") {
+/// #   Ok(result) => {
+/// let x = result.binding("x").expect("cannot get x");
+/// println!("x = {}", x.json().unwrap());
+/// # assert_eq!(x.json().expect("cannot convert x to JSON"), "5");
+/// #   }
+/// #   Err(e) => {
+/// #     panic!("error: {}", e);
+/// #   }
+/// # }
+/// ```
+#[derive(Debug)]
+pub struct Output {
+    c_ptr: *mut regoOutput,
+}
+
+/// Enumeration of different kinds of Rego Nodes that can be returned as output from a query.
+///
+/// The main kinds of nodes are:
+/// * `Binding` - A variable binding. Has two children: a `Var` and a `Term`
+/// * `Var` - A variable name. Has no children.
+/// * `Term` - A term. Has one child: a `Scalar`, `Array`, `Set`, or `Object`
+/// * `Scalar` - A scalar value. Has one child: a `String`, `Int`, `Float`, `True`, `False`, or `Null`
+/// * `Array` - An array. Has zero or more `Term` children.
+/// * `Set` - A set. Has zero or more `Term` children.
+/// * `Object` - An object. Has zero or more `ObjectItem` children.
+/// * `ObjectItem` - An object item. Has two `Term` children.
+///
+/// Errors are also represented as nodes:
+/// * `Error` - An error. Has three children: an `ErrorMessage`, `ErrorAst`, and `ErrorCode`
+/// * `ErrorMessage` - An error message.
+/// * `ErrorAst` - A tree representing where in the input program the error occurred.
+/// * `ErrorCode` - An error code.
+/// * `ErrorSeq` - A sequence of errors. Has one or more `Error` children.
+#[derive(Clone, Debug, Default)]
+pub enum NodeKind {
+    #[default]
+    Undefined,
+    Binding,
+    Var,
+    Term,
+    Scalar,
+    Array,
+    Set,
+    Object,
+    ObjectItem,
+    Int,
+    Float,
+    String,
+    True,
+    False,
+    Null,
+    Error,
+    ErrorSeq,
+    ErrorMessage,
+    ErrorAst,
+    ErrorCode,
+    Internal,
+}
+
+/// Interface for Rego Node objects.
+///
+/// Rego Nodes are the basic building blocks of a Rego result. They
+/// exist in a tree structure. Each node has a kind, which is one of
+/// the variants of [`NodeKind`]. Each node also has zero or more
+/// children, which are also nodes.
+///
+/// # Examples
+/// ```
+/// use regorust::*;
+/// let rego = Interpreter::new();
+/// match rego.query(r#"x={"a": 10, "b": "20", "c": [30.0, 60], "d": true, "e": null}"#) {
+///   Ok(result) => {
+///     let x = result.binding("x").expect("cannot get x");
+///     println!("x = {}", x.json().unwrap());
+///     if let NodeValue::Int(a) = x
+///         .lookup("a")
+///         .unwrap()
+///         .value()
+///         .unwrap()
+///     {
+///       println!("x['a'] = {}", a);
+///       # assert_eq!(a, 10);
+///     }
+///
+///     if let NodeValue::String(b) = x
+///        .lookup("b")
+///        .unwrap()
+///        .value()
+///        .unwrap()
+///     {
+///       println!("x['b'] = {}", b);
+///       # assert_eq!(b, "20");
+///     }
+///
+///     let c = x.lookup("c").unwrap();
+///     if let NodeValue::Float(c0) = c.index(0)
+///       .unwrap()
+///       .value()
+///       .unwrap()
+///     {
+///       println!("x['c'][0] = {}", c0);
+///       # assert_eq!(c0, 30.0);
+///     }
+///
+///     if let NodeValue::Int(c1) = c.index(1)
+///       .unwrap()
+///       .value()
+///       .unwrap()
+///     {
+///       println!("x['c'][1] = {}", c1);
+///       # assert_eq!(c1, 60);
+///     }
+///
+///     if let NodeValue::Bool(d) = x
+///       .lookup("d")
+///       .unwrap()
+///       .value()
+///       .unwrap()
+///     {
+///       println!("x['d'] = {}", d);
+///       # assert!(d);
+///     }
+///
+///     if let NodeValue::Null = x
+///       .lookup("e")
+///       .unwrap()
+///       .value()
+///       .unwrap()
+///     {
+///       println!("x['e'] = null");
+///     }
+///   }
+///   Err(e) => {
+///     panic!("error: {}", e);
+///   }
+/// }
+/// ```
+#[derive(Debug)]
+pub struct Node {
+    c_ptr: *mut regoNode,
+    children: Vec<Node>,
+    /// The number of children of this node.
+    pub size: usize,
+
+    /// The kind of this node.
+    pub kind: NodeKind,
+}
+
+/// Represents the value of a Rego Node.
+///
+/// The value of a node is only valid for certain kinds of nodes,
+/// namely `Int`, `Float`, `String`, `True`, `False`, and `Null`.
+/// It will automatically unpack `Scalar` and `Term` nodes which
+/// contain these kinds of values.
+#[derive(PartialEq, Debug)]
+pub enum NodeValue {
+    Var(String),
+    Int(i64),
+    Float(f64),
+    String(String),
+    Bool(bool),
+    Null,
+}
+
+/// Sets whether verbose logging is enabled for the library
+///
+/// When enabled, the Rego library will output extensive information
+/// about its internal state to stdout. This is useful for debugging
+/// the library itself, but is not useful for debugging Rego policies.
+pub fn set_logging_enabled(enabled: bool) {
+    let c_enabled: regoBoolean = if enabled { 1 } else { 0 };
+    unsafe {
+        regoSetLoggingEnabled(c_enabled);
+    }
+}
+
+impl Interpreter {
+    /// Creates a new Rego interpreter.
+    pub fn new() -> Self {
+        let interpreter_ptr = unsafe { regoNew() };
+        Self {
+            c_ptr: interpreter_ptr,
+        }
+    }
+
+    /// Returns the error message for the last operation.
+    fn get_error(&self) -> &str {
+        let c_str = unsafe { regoGetError(self.c_ptr) };
+        let result: &CStr = unsafe { CStr::from_ptr(c_str) };
+        result.to_str().unwrap()
+    }
+
+    /// Adds a Rego module from a file.
+    ///
+    /// The file must be valid Rego, and is equivalent to adding
+    /// the module as a string using `add_module`.
+    ///
+    /// # Example module file
+    /// ```rego
+    /// package scalars
+    ///
+    /// greeting := "Hello"
+    /// max_height := 42
+    /// pi := 3.14159
+    /// allowed := true
+    /// location := null
+    /// ```
+    pub fn add_module_file(&self, path: &Path) -> Result<(), &str> {
+        let path_str = path.to_str().unwrap();
+        let path_cstr = CString::new(path_str).unwrap();
+        let path_ptr = path_cstr.as_ptr();
+        let result = unsafe { regoAddModuleFile(self.c_ptr, path_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Adds a Rego module from a string.
+    ///
+    /// # Example module string
+    /// ```rego
+    /// package scalars
+    ///
+    /// greeting := "Hello"
+    /// max_height := 42
+    /// pi := 3.14159
+    /// allowed := true
+    /// location := null
+    /// ```
+    ///
+    /// # Example
+    /// ```
+    /// # use regorust::*;
+    /// let module = r#"
+    ///   package scalars
+    ///
+    ///   greeting := "Hello"
+    ///   max_height := 42
+    ///   pi := 3.14159
+    ///   allowed := true
+    ///   location := null
+    /// "#;
+    /// let rego = Interpreter::new();
+    /// rego.add_module("scalars", module);
+    /// let result = rego.query("data.scalars.greeting").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), r#""Hello""#);
+    /// ```
+    pub fn add_module(&self, name: &str, source: &str) -> Result<(), &str> {
+        let name_cstr = CString::new(name).unwrap();
+        let name_ptr = name_cstr.as_ptr();
+        let source_cstr = CString::new(source).unwrap();
+        let source_ptr = source_cstr.as_ptr();
+        let result = unsafe { regoAddModule(self.c_ptr, name_ptr, source_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Adds a base document from a file.
+    ///
+    /// The file must be a valid JSON object, and is equivalent to adding
+    /// the document as a string using `add_data_json`.
+    ///
+    /// # Example document file
+    /// ```json
+    /// {
+    ///    "one": {
+    ///        "bar": "Foo",
+    ///        "baz": 5,
+    ///        "be": true,
+    ///        "bop": 23.4
+    ///    },
+    ///    "two": {
+    ///        "bar": "Bar",
+    ///      "baz": 12.3,
+    ///        "be": false,
+    ///        "bop": 42
+    ///    }
+    /// }
+    /// ```
+    pub fn add_data_json_file(&self, path: &Path) -> Result<(), &str> {
+        let path_str = path.to_str().unwrap();
+        let path_cstr = CString::new(path_str).unwrap();
+        let path_ptr = path_cstr.as_ptr();
+        let result = unsafe { regoAddDataJSONFile(self.c_ptr, path_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Adds a base document from a string.
+    ///
+    /// The string must contain a valid JSON object.
+    ///
+    /// # Example document file
+    /// ```json
+    /// {
+    ///    "one": {
+    ///        "bar": "Foo",
+    ///        "baz": 5,
+    ///        "be": true,
+    ///        "bop": 23.4
+    ///    },
+    ///    "two": {
+    ///        "bar": "Bar",
+    ///      "baz": 12.3,
+    ///        "be": false,
+    ///        "bop": 42
+    ///    }
+    /// }
+    /// ```
+    ///
+    /// # Example
+    /// ```
+    /// # use regorust::*;
+    /// let data = r#"
+    ///   {
+    ///     "one": {
+    ///       "bar": "Foo",
+    ///       "baz": 5,
+    ///       "be": true,
+    ///       "bop": 23.4
+    ///     },
+    ///     "two": {
+    ///       "bar": "Bar",
+    ///       "baz": 12.3,
+    ///       "be": false,
+    ///       "bop": 42
+    ///     }
+    ///   }
+    /// "#;
+    /// let rego = Interpreter::new();
+    /// rego.add_data_json(data);
+    /// let result = rego.query("data.one.bar").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), r#""Foo""#);
+    /// ```
+    pub fn add_data_json(&self, data: &str) -> Result<(), &str> {
+        let data_cstr = CString::new(data).unwrap();
+        let data_ptr = data_cstr.as_ptr();
+        let result = unsafe { regoAddDataJSON(self.c_ptr, data_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Sets the input JSON expression from a file.
+    ///
+    /// The input must be a single valid JSON value. It is equivalent to adding
+    /// the input file as a string using `add_input_json`.
+    ///
+    /// # Example input files
+    /// ```json
+    /// "Hello, rego"
+    /// ```
+    ///
+    /// ```json
+    /// 42
+    /// ```
+    ///
+    /// ```json
+    /// [1, 2, 3]
+    /// ```
+    ///
+    /// ```json
+    /// {"a": 1, "b": 2}
+    /// ```
+    pub fn set_input_json_file(&self, path: &Path) -> Result<(), &str> {
+        let path_str = path.to_str().unwrap();
+        let path_cstr = CString::new(path_str).unwrap();
+        let path_ptr = path_cstr.as_ptr();
+        let result = unsafe { regoSetInputJSONFile(self.c_ptr, path_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Sets the input JSON expression from a string.
+    ///
+    /// The input must be a single valid JSON value.
+    ///
+    /// # Example input files
+    ///
+    /// ```json
+    /// "Hello, rego"
+    /// ```
+    ///
+    /// ```json
+    /// 42
+    /// ```
+    ///
+    /// ```json
+    /// [1, 2, 3]
+    /// ```
+    ///
+    /// ```json
+    /// {"a": 1, "b": 2}
+    /// ```
+    ///
+    /// # Example
+    /// ```
+    /// # use regorust::*;
+    /// let input = r#"
+    ///   {
+    ///     "a": 10,
+    ///     "b": "20",
+    ///     "c": 30.0,
+    ///     "d": true
+    ///   }
+    /// "#;
+    /// let rego = Interpreter::new();
+    /// rego.set_input_json(input);
+    /// let result = rego.query("input.a").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), "10");
+    /// ```
+    pub fn set_input_json(&self, input: &str) -> Result<(), &str> {
+        let input_cstr = CString::new(input).unwrap();
+        let input_ptr = input_cstr.as_ptr();
+        let result = unsafe { regoSetInputJSON(self.c_ptr, input_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Sets whether the Rego interpreter is in debug mode.
+    ///
+    /// When debug mode is enabled, the Rego interpreter will output extensive
+    /// debugging information about the compliation process, including intermediary
+    /// ASTs and the generated bytecode. This is mostly useful for debugging the
+    /// Rego compiler itself, and is not useful for debugging Rego policies.
+    pub fn set_debug_enabled(&self, enabled: bool) {
+        let c_enabled: regoBoolean = if enabled { 1 } else { 0 };
+        unsafe {
+            regoSetDebugEnabled(self.c_ptr, c_enabled);
+        }
+    }
+
+    /// Returns whether the Rego interpreter is in debug mode.
+    pub fn get_debug_enabled(&self) -> bool {
+        let c_enabled = unsafe { regoGetDebugEnabled(self.c_ptr) };
+        c_enabled == 1
+    }
+
+    /// Sets the path to the directory where the Rego interpreter will write debug AST files.
+    ///
+    /// This is only useful when debug mode is enabled.
+    pub fn set_debug_path(&self, path: &Path) -> Result<(), &str> {
+        let path_str = path.to_str().unwrap();
+        let path_cstr = CString::new(path_str).unwrap();
+        let path_ptr = path_cstr.as_ptr();
+        let result = unsafe { regoSetDebugPath(self.c_ptr, path_ptr) };
+        if result == 0 {
+            Ok(())
+        } else {
+            Err(self.get_error())
+        }
+    }
+
+    /// Sets whether the Rego interpreter will perform well-formedness checks.
+    ///
+    /// When enabled, the Rego interpreter will perform well-formedness checks on
+    /// the AST during each pass of the compiler.
+    pub fn set_well_formed_checks_enabled(&self, enabled: bool) {
+        let c_enabled: regoBoolean = if enabled { 1 } else { 0 };
+        unsafe {
+            regoSetWellFormedChecksEnabled(self.c_ptr, c_enabled);
+        }
+    }
+
+    /// Returns whether the Rego interpreter will perform well-formedness checks.
+    pub fn get_well_formed_checks_enabled(&self) -> bool {
+        let c_enabled = unsafe { regoGetWellFormedChecksEnabled(self.c_ptr) };
+        c_enabled == 1
+    }
+
+    /// Sets whether the interpreter will forward errors thrown by the built-ins.
+    ///
+    /// By default, the Rego interpreter will catch errors thrown by the built-ins
+    /// and return them as an Undefined result. When this is enabled, the interpreter
+    /// will instead forward the error to the caller.
+    pub fn set_strict_built_in_errors(&self, enabled: bool) {
+        let c_enabled: regoBoolean = if enabled { 1 } else { 0 };
+        unsafe {
+            regoSetStrictBuiltInErrors(self.c_ptr, c_enabled);
+        }
+    }
+
+    /// Returns whether the interpreter will forward errors thrown by the built-ins.
+    pub fn get_strict_built_in_errors(&self) -> bool {
+        let c_enabled = unsafe { regoGetStrictBuiltInErrors(self.c_ptr) };
+        c_enabled == 1
+    }
+
+    /// This method performs a query against the current set of base and virtual documents.
+    ///
+    /// While the Rego interpreter can be used to perform simple queries, in most cases
+    /// users will want to load one or more base documents (using [`Self::add_data_json()`] or
+    /// [`Self::add_data_json_file()`]) and one or more Rego modules (using [`Self::add_module()`] or
+    /// [`Self::add_module_file()`]). Then, multiple queries can be performed by providing an input
+    /// (using [`Self::set_input_json()`] or [`Self::set_input_json_file()`]) and then calling this
+    /// method.
+    ///
+    /// # Example
+    /// ```
+    /// # use regorust::*;
+    /// let input0 = r#"{"a": 10}"#;
+    /// let input1 = r#"{"a": 4}"#;
+    /// let input2 = r#"{"a": 7}"#;
+    /// let multi = r#"
+    ///   package multi
+    ///   
+    ///   default a := 0
+    ///   
+    ///   a := val {
+    ///       input.a > 0
+    ///       input.a < 10
+    ///       input.a % 2 == 1
+    ///       val := input.a * 10
+    ///   } {
+    ///       input.a > 0
+    ///       input.a < 10
+    ///       input.a % 2 == 0
+    ///       val := input.a * 10 + 1
+    ///   }
+    ///   
+    ///   a := input.a / 10 {
+    ///       input.a >= 10
+    ///   }
+    /// "#;
+    /// let rego = Interpreter::new();
+    /// rego.add_module("multi", multi);
+    /// rego.set_input_json(input0);
+    /// let result = rego.query("data.multi.a").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), "1");
+    /// rego.set_input_json(input1);
+    /// let result = rego.query("data.multi.a").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), "41");
+    /// rego.set_input_json(input2);
+    /// let result = rego.query("data.multi.a").unwrap();
+    /// println!("{}", result.to_str().unwrap());
+    /// # assert_eq!(result.to_str().unwrap(), "70");
+    /// ```
+    pub fn query(&self, query: &str) -> Result<Output, &str> {
+        let query_cstr = CString::new(query).unwrap();
+        let query_ptr = query_cstr.as_ptr();
+        let output_ptr = unsafe { regoQuery(self.c_ptr, query_ptr) };
+        if output_ptr == std::ptr::null_mut() {
+            Err(self.get_error())
+        } else {
+            Ok(Output::new(output_ptr))
+        }
+    }
+}
+
+impl Drop for Interpreter {
+    fn drop(&mut self) {
+        unsafe {
+            regoFree(self.c_ptr);
+        }
+    }
+}
+
+impl PartialEq for Interpreter {
+    fn eq(&self, other: &Self) -> bool {
+        return self.c_ptr == other.c_ptr;
+    }
+}
+
+impl Drop for Output {
+    fn drop(&mut self) {
+        unsafe {
+            regoFreeOutput(self.c_ptr);
+        }
+    }
+}
+
+impl Output {
+    fn new(c_ptr: *mut regoOutput) -> Self {
+        Self { c_ptr }
+    }
+
+    /// Returns whether the output is ok.
+    ///
+    /// The output of a successful query will always be a Node. However, if there
+    /// was an error that arose with the Rego engine, then this Node will be of
+    /// kind [`NodeKind::ErrorSeq`] and contain one or more errors. This method
+    /// gives a quick way, without inspecting the result node, of finding whether
+    /// it is ok.
+    pub fn ok(&self) -> bool {
+        let c_ok = unsafe { regoOutputOk(self.c_ptr) };
+        c_ok == 1
+    }
+
+    /// Returns the output as a human readable string.
+    ///
+    /// If the result of [`Self::ok()`] is false, the result will be an string
+    /// containing error information.
+    pub fn to_str(&self) -> Result<&str, &str> {
+        let c_str = unsafe { regoOutputString(self.c_ptr) };
+        let result: &CStr = unsafe { CStr::from_ptr(c_str) };
+        let output_str = result.to_str().unwrap().trim_end();
+        if self.ok() {
+            Ok(output_str)
+        } else {
+            Err(output_str)
+        }
+    }
+
+    /// Returns the output node.
+    ///
+    /// The value of the [`Result`] enum will reflect the output of
+    /// [`Self::ok()`].
+    pub fn to_node(&self) -> Result<Node, Node> {
+        let node_ptr = unsafe { regoOutputNode(self.c_ptr) };
+        let output = Node::new(node_ptr);
+        if self.ok() {
+            Ok(output)
+        } else {
+            Err(output)
+        }
+    }
+
+    /// Attempts to return the binding for the given variable name.
+    ///
+    /// If the output is not OK or the variable is not bound, then
+    /// this will return an [`Result::Err`] with the output node.
+    /// Otherwise it will return the bound value for the variable.
+    pub fn binding(&self, name: &str) -> Result<Node, Node> {
+        if !self.ok() {
+            return self.to_node();
+        }
+
+        let name_cstr = CString::new(name).unwrap();
+        let name_ptr = name_cstr.as_ptr();
+        let node_ptr = unsafe { regoOutputBinding(self.c_ptr, name_ptr) };
+
+        if node_ptr == std::ptr::null_mut() {
+            self.to_node()
+        } else {
+            Ok(Node::new(node_ptr))
+        }
+    }
+}
+
+impl PartialEq for Output {
+    fn eq(&self, other: &Self) -> bool {
+        return self.c_ptr == other.c_ptr;
+    }
+}
+
+impl NodeKind {
+    fn new(c_kind: regoEnum) -> Self {
+        match c_kind {
+            REGO_NODE_UNDEFINED => NodeKind::Undefined,
+            REGO_NODE_BINDING => NodeKind::Binding,
+            REGO_NODE_VAR => NodeKind::Var,
+            REGO_NODE_TERM => NodeKind::Term,
+            REGO_NODE_SCALAR => NodeKind::Scalar,
+            REGO_NODE_ARRAY => NodeKind::Array,
+            REGO_NODE_SET => NodeKind::Set,
+            REGO_NODE_OBJECT => NodeKind::Object,
+            REGO_NODE_OBJECT_ITEM => NodeKind::ObjectItem,
+            REGO_NODE_INT => NodeKind::Int,
+            REGO_NODE_FLOAT => NodeKind::Float,
+            REGO_NODE_STRING => NodeKind::String,
+            REGO_NODE_TRUE => NodeKind::True,
+            REGO_NODE_FALSE => NodeKind::False,
+            REGO_NODE_NULL => NodeKind::Null,
+            REGO_NODE_ERROR => NodeKind::Error,
+            REGO_NODE_ERROR_SEQ => NodeKind::ErrorSeq,
+            REGO_NODE_ERROR_MESSAGE => NodeKind::ErrorMessage,
+            REGO_NODE_ERROR_AST => NodeKind::ErrorAst,
+            REGO_NODE_ERROR_CODE => NodeKind::ErrorCode,
+            REGO_NODE_INTERNAL => NodeKind::Internal,
+            _ => panic!("Unknown node kind"),
+        }
+    }
+}
+
+impl Node {
+    fn new(c_ptr: *mut regoNode) -> Self {
+        let num_children = unsafe { regoNodeSize(c_ptr) };
+        let mut children = Vec::with_capacity(num_children as usize);
+        for i in 0..num_children {
+            let child_ptr = unsafe { regoNodeGet(c_ptr, i) };
+            children.push(Node::new(child_ptr));
+        }
+        let kind = unsafe { regoNodeType(c_ptr) };
+        Self {
+            c_ptr,
+            children,
+            size: num_children as usize,
+            kind: NodeKind::new(kind),
+        }
+    }
+
+    /// Returns a human-readable string representation of the node kind.
+    pub fn kind_name(&self) -> &str {
+        let c_str = unsafe { regoNodeTypeName(self.c_ptr) };
+        let result: &CStr = unsafe { CStr::from_ptr(c_str) };
+        result.to_str().unwrap()
+    }
+
+    fn scalar_value(&self) -> Result<String, &str> {
+        let size = unsafe { regoNodeValueSize(self.c_ptr) };
+
+        let buf = vec![32 as u8; (size as usize) - 1];
+        let input_cstr = CString::new(buf).unwrap();
+        let input_ptr = CString::into_raw(input_cstr);
+        let err: regoEnum = unsafe { regoNodeValue(self.c_ptr, input_ptr, size) };
+        if err != REGO_OK {
+            return Err("Error getting node value");
+        } else {
+            let output_cstr: CString = unsafe { CString::from_raw(input_ptr) };
+            Ok(output_cstr.into_string().unwrap())
+        }
+    }
+
+    /// Returns the value of the node.
+    ///
+    /// If the node has a singular value (i.e is a `Scalar` or a `Term` containing a scalar)
+    /// then this will return that value. Otherwise it will return `[Option::None]`.
+    ///
+    /// # Examples
+    /// ```
+    /// # use regorust::*;
+    /// # let rego = Interpreter::new();
+    /// # match rego.query(r#"x=10; y="20"; z=true"#) {
+    /// #  Ok(result) => {
+    /// let x = result.binding("x").expect("cannot get x");
+    /// if let NodeValue::Int(x) = result
+    ///   .binding("x")
+    ///   .unwrap()
+    ///   .value()
+    ///   .unwrap()
+    /// {
+    ///   println!("x = {}", x);
+    ///   # assert_eq!(x, 10);
+    /// }
+    ///
+    /// if let NodeValue::String(y) = result
+    ///   .binding("y")
+    ///   .unwrap()
+    ///   .value()
+    ///   .unwrap()
+    /// {
+    ///   println!("y = {}", y);
+    ///   # assert_eq!(y, "20");
+    /// }
+    ///
+    /// if let NodeValue::Bool(z) = result
+    ///   .binding("z")
+    ///   .unwrap()
+    ///   .value()
+    ///   .unwrap()
+    /// {
+    ///   println!("z = {}", z);
+    ///   # assert!(z);
+    /// }
+    /// # }
+    /// # Err(e) => {
+    /// # panic!("error: {}", e);
+    /// # }
+    /// # }
+    /// ```
+    pub fn value(&self) -> Option<NodeValue> {
+        match self.kind {
+            NodeKind::Term => self.children[0].value(),
+            NodeKind::Scalar => self.children[0].value(),
+            NodeKind::Null => Some(NodeValue::Null),
+            NodeKind::True => Some(NodeValue::Bool(true)),
+            NodeKind::False => Some(NodeValue::Bool(false)),
+            NodeKind::Var => self.scalar_value().ok().map(|s| NodeValue::Var(s)),
+            NodeKind::Int => self
+                .scalar_value()
+                .ok()
+                .map(|s| NodeValue::Int(s.parse().unwrap())),
+            NodeKind::Float => self
+                .scalar_value()
+                .ok()
+                .map(|s| NodeValue::Float(s.parse().unwrap())),
+            NodeKind::String => self
+                .scalar_value()
+                .ok()
+                .map(|s| NodeValue::String(s[1..s.len() - 1].to_string())),
+            _ => None,
+        }
+    }
+
+    /// Returns the node as a JSON string.
+    ///
+    /// The result of this function will a valid JSON representation of the
+    /// node. If the node cannot be transformed into JSON this will return
+    /// an error.
+    ///
+    /// # Examples
+    /// ```
+    /// # use regorust::*;
+    /// # let rego = Interpreter::new();
+    /// # match rego.query(r#"x=10; y="20"; z=true"#) {
+    /// #  Ok(result) => {
+    /// let x = result.binding("x").expect("cannot get x");
+    /// println!("x = {}", x.json().unwrap());
+    /// # assert_eq!(x.json().unwrap(), "10");
+    /// let y = result.binding("y").expect("cannot get y");
+    /// println!("y = {}", y.json().unwrap());
+    /// # assert_eq!(y.json().unwrap(), r#""20""#);
+    /// let z = result.binding("z").expect("cannot get z");
+    /// println!("z = {}", z.json().unwrap());
+    /// # assert_eq!(z.json().unwrap(), "true");
+    /// # }
+    /// # Err(e) => {
+    /// # panic!("error: {}", e);
+    /// # }
+    /// # }
+    /// ```
+    pub fn json(&self) -> Result<String, &str> {
+        let size = unsafe { regoNodeJSONSize(self.c_ptr) };
+
+        let buf = vec![32 as u8; (size as usize) - 1];
+        let input_cstr = CString::new(buf).unwrap();
+        let input_ptr = CString::into_raw(input_cstr);
+        let err: regoEnum = unsafe { regoNodeJSON(self.c_ptr, input_ptr, size) };
+        if err != REGO_OK {
+            return Err("Error converting node to JSON");
+        } else {
+            let output_cstr = unsafe { CString::from_raw(input_ptr) };
+            let output = output_cstr.into_string().unwrap();
+            return Ok(output);
+        }
+    }
+
+    /// Returns the number of child nodes.
+    pub fn size(&self) -> usize {
+        let c_size = unsafe { regoNodeSize(self.c_ptr) };
+        c_size as usize
+    }
+
+    fn lookup_object(&self, key: &str) -> Result<&Node, &str> {
+        for child in self.children.iter() {
+            let key_json = child[0].json()?;
+            if key_json.starts_with('"') && key_json.ends_with('"') {
+                // allow querying of string keys without the quotes
+                let without_quotes = key_json[1..key_json.len() - 1].to_string();
+                if without_quotes == key {
+                    return Ok(&child[1]);
+                }
+            }
+
+            if key_json == key {
+                return Ok(&child[1]);
+            }
+        }
+
+        Err("key not found")
+    }
+
+    fn lookup_set(&self, item: &str) -> Result<&Node, &str> {
+        for child in self.children.iter() {
+            let item_json = child.json()?;
+            if item_json == item {
+                return Ok(&child);
+            }
+        }
+
+        Err("item not found")
+    }
+
+    /// Looks up a key in an object or a set.
+    ///
+    /// The key argument should be a JSON representation of the key, and can be
+    /// both simple types (e.g. strings, ints, bools) or complex types
+    /// (e.g. objects, arrays). The returned result will be value node of the
+    /// object item with a matching key, or conversely the node with the matching
+    /// value in the set. If no matching value is found, or if the node is not an
+    /// object or a set, an error will be returned.
+    ///
+    /// # Examples
+    /// ```
+    /// # use regorust::*;
+    /// # let rego = Interpreter::new();
+    /// let output = rego.query(r#"x={"a": 1}; y={["foo", false], 5}; z=[1, "bar"]"#).unwrap();
+    /// let x = output.binding("x").unwrap();
+    /// println!(r#"x["a"] = {}"#, x.lookup("a").unwrap().json().unwrap());
+    /// # assert_eq!(x.lookup("a").unwrap().json().unwrap(), "1");
+    /// let y = output.binding("y").unwrap();
+    /// println!(r#"y[["foo", false]] = {}"#, y.lookup(r#"["foo", false]"#).unwrap().json().unwrap());
+    /// # assert_eq!(y.lookup(r#"["foo", false]"#).unwrap().json().unwrap(), r#"["foo", false]"#);
+    /// let z = output.binding("z").unwrap();
+    /// match z.lookup("bar") {
+    ///   Ok(_) => panic!("bar should not be found"),
+    ///   Err(err) => println!("{}", err)
+    /// }
+    /// ```
+    pub fn lookup(&self, key: &str) -> Result<&Node, &str> {
+        match self.kind {
+            NodeKind::Term => self.children[0].lookup(key),
+            NodeKind::Object => self.lookup_object(key),
+            NodeKind::Set => self.lookup_set(key),
+            _ => Err("Must be an object or a set"),
+        }
+    }
+
+    /// Looks up an index in an array.
+    ///
+    /// If the index falls outside the range of the array, the result will be an error.
+    /// Similarly, this method will only work if the Node is of [`NodeKind::Array`].
+    ///
+    /// # Examples
+    /// ```
+    /// # use regorust::*;
+    /// # let rego = Interpreter::new();
+    /// let output = rego.query(r#"x=[1, 2, 3]; y={"0": "a"}"#).unwrap();
+    /// let x = output.binding("x").unwrap();
+    /// println!(r#"x[0] = {}"#, x.index(0).unwrap().json().unwrap());
+    /// # assert_eq!(x.index(0).unwrap().json().unwrap(), "1");
+    /// let y = output.binding("y").unwrap();
+    /// match y.index(0) {
+    ///   Ok(_) => panic!("0 should not be found"),
+    ///   Err(err) => println!("{}", err)
+    /// }
+    pub fn index(&self, index: usize) -> Result<&Node, &str> {
+        match self.kind {
+            NodeKind::Term => self.children[0].index(index),
+            NodeKind::Array => {
+                if index >= self.size() {
+                    return Err("Index out of bounds");
+                }
+
+                Ok(&self.children[index])
+            }
+            _ => Err("Must be an array"),
+        }
+    }
+
+    /// Returns the raw child node at a particular index.
+    ///
+    /// If the index falls outside the range of the array, the result will be an error.
+    pub fn at(&self, index: usize) -> Result<&Node, &str> {
+        if index >= self.size() {
+            return Err("Index out of bounds");
+        }
+
+        Ok(&self.children[index])
+    }
+
+    /// Returns an iterator over the child nodes.
+    pub fn iter(&self) -> std::slice::Iter<'_, Node> {
+        return self.children.iter();
+    }
+}
+
+impl Index<usize> for Node {
+    type Output = Node;
+
+    fn index(&self, index: usize) -> &Self::Output {
+        &self.children[index]
+    }
+}
+
+impl PartialEq for Node {
+    fn eq(&self, other: &Self) -> bool {
+        return self.c_ptr == other.c_ptr;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn query_math() {
+        let rego = Interpreter::new();
+        match rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4") {
+            Ok(result) => {
+                let x = result.binding("x").expect("cannot get x");
+                let y = result.binding("y").expect("cannot get y");
+                println!("x = {}", x.json().unwrap());
+                println!("y = {}", y.json().unwrap());
+                assert_eq!(x.json().expect("cannot convert x to JSON"), "5");
+                assert_eq!(y.json().expect("cannot convert y to JSON"), "9.4");
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+    }
+
+    #[test]
+    fn input_data() {
+        let input = r#"
+            {
+                "a": 10,
+                "b": "20",
+                "c": 30.0,
+                "d": true
+            }
+        "#;
+        let data0 = r#"
+            {
+                "one": {
+                    "bar": "Foo",
+                    "baz": 5,
+                    "be": true,
+                    "bop": 23.4
+                },
+                "two": {
+                    "bar": "Bar",
+                    "baz": 12.3,
+                    "be": false,
+                    "bop": 42
+                }
+            }
+        "#;
+        let data1 = r#"
+            {
+                "three": {
+                    "bar": "Baz",
+                    "baz": 15,
+                    "be": true,
+                    "bop": 4.23
+                }
+            }        
+        "#;
+        let module = r#"
+        package objects
+
+        rect := {`width`: 2, "height": 4}
+        cube := {"width": 3, `height`: 4, "depth": 5}
+        a := 42
+        b := false
+        c := null
+        d := {"a": a, "x": [b, c]}
+        index := 1
+        shapes := [rect, cube]
+        names := ["prod", `smoke1`, "dev"]
+        sites := [{"name": "prod"}, {"name": names[index]}, {"name": "dev"}]
+        e := {
+            a: "foo",
+            "three": c,
+            names[2]: b,
+            "four": d,
+        }
+        f := e["dev"]                
+        "#;
+        let rego = Interpreter::new();
+        rego.set_input_json(input).expect("cannot set input");
+        rego.add_data_json(data0)
+            .expect("cannot add data as string");
+        rego.add_data_json(data1).expect("cannot add data file");
+        rego.add_module("objects", module)
+            .expect("cannot add module");
+        match rego.query("x=[data.one, input.b, data.objects.sites[1]]") {
+            Ok(result) => {
+                let x = result.binding("x").expect("cannot get x");
+                let data_one = x.index(0).expect("cannot get data.one");
+                if let NodeValue::String(bar) = data_one
+                    .lookup("bar")
+                    .expect("bar key missing")
+                    .value()
+                    .expect("bar value missing")
+                {
+                    assert_eq!(bar, "Foo");
+                } else {
+                    panic!("bar is not a string");
+                }
+
+                if let NodeValue::Bool(be) = data_one
+                    .lookup("be")
+                    .expect("be key missing")
+                    .value()
+                    .expect("be value missing")
+                {
+                    assert_eq!(be, true);
+                } else {
+                    panic!("be is not a bool");
+                }
+
+                if let NodeValue::Int(baz) = data_one
+                    .lookup("baz")
+                    .expect("baz key missing")
+                    .value()
+                    .expect("baz value missing")
+                {
+                    assert_eq!(baz, 5);
+                } else {
+                    panic!("baz is not an int");
+                }
+
+                if let NodeValue::Float(bop) = data_one
+                    .lookup("bop")
+                    .expect("bop key missing")
+                    .value()
+                    .expect("bop value missing")
+                {
+                    assert_eq!(bop, 23.4);
+                } else {
+                    panic!("bop is not a float");
+                }
+
+                if let NodeValue::String(input_b) = x
+                    .index(1)
+                    .expect("cannot get input.b")
+                    .value()
+                    .expect("input.b value missing")
+                {
+                    assert_eq!(input_b, "20");
+                } else {
+                    panic!("input.b is not a string");
+                }
+
+                let data_objects_sites_1 = x.index(2).expect("cannot get data.objects.sites[1]");
+                if let NodeValue::String(name) = data_objects_sites_1
+                    .lookup("name")
+                    .expect("name key missing")
+                    .value()
+                    .expect("name value missing")
+                {
+                    assert_eq!(name, "smoke1");
+                } else {
+                    panic!("name is not a string");
+                }
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+    }
+
+    #[test]
+    fn node_access() {
+        let rego = Interpreter::new();
+        match rego.query(r#"x={"a": 10, "b": "20", "c": [30.0, 60], "d": true, "e": null}"#) {
+            Ok(result) => {
+                let x = result.binding("x").expect("cannot get x");
+                println!("x = {}", x.json().unwrap());
+                if let NodeValue::Int(a) = x.lookup("a").unwrap().value().unwrap() {
+                    assert_eq!(a, 10);
+                }
+
+                if let NodeValue::String(b) = x.lookup("b").unwrap().value().unwrap() {
+                    assert_eq!(b, "20");
+                }
+
+                let c = x.lookup("c").unwrap();
+                if let NodeValue::Float(c0) = c.index(0).unwrap().value().unwrap() {
+                    assert_eq!(c0, 30.0);
+                }
+
+                if let NodeValue::Int(c1) = c.index(1).unwrap().value().unwrap() {
+                    assert_eq!(c1, 60);
+                }
+
+                if let NodeValue::Bool(d) = x.lookup("d").unwrap().value().unwrap() {
+                    assert!(d);
+                }
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+    }
+
+    #[test]
+    fn multiple_inputs() {
+        let input0 = r#"{"a": 10}"#;
+        let input1 = r#"{"a": 4}"#;
+        let input2 = r#"{"a": 7}"#;
+        let module = r#"
+            package multi
+
+            default a := 0
+            
+            a := val {
+                input.a > 0
+                input.a < 10
+                input.a % 2 == 1
+                val := input.a * 10
+            } {
+                input.a > 0
+                input.a < 10
+                input.a % 2 == 0
+                val := input.a * 10 + 1
+            }
+            
+            a := input.a / 10 {
+                input.a >= 10
+            }        
+        "#;
+        let rego = Interpreter::new();
+        rego.add_module("multi", module).expect("cannot add module");
+        rego.set_input_json(input0).expect("cannot set input");
+        match rego.query("x = data.multi.a") {
+            Ok(output) => {
+                if let NodeValue::Int(a) = output
+                    .binding("x")
+                    .expect("cannot get x")
+                    .value()
+                    .expect("cannot get x value")
+                {
+                    assert_eq!(a, 1);
+                }
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+
+        rego.set_input_json(input1).expect("cannot set input");
+        match rego.query("x = data.multi.a") {
+            Ok(output) => {
+                if let NodeValue::Int(a) = output
+                    .binding("x")
+                    .expect("cannot get x")
+                    .value()
+                    .expect("cannot get x value")
+                {
+                    assert_eq!(a, 41);
+                }
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+
+        rego.set_input_json(input2).expect("cannot set input");
+        match rego.query("x = data.multi.a") {
+            Ok(output) => {
+                if let NodeValue::Int(a) = output
+                    .binding("x")
+                    .expect("cannot get x")
+                    .value()
+                    .expect("cannot get x value")
+                {
+                    assert_eq!(a, 70);
+                }
+            }
+            Err(e) => {
+                panic!("error: {}", e);
+            }
+        }
+    }
+}


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 962bc771..91b52081 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -15,7 +15,10 @@ target_link_libraries(yaml_trieste
   PRIVATE
   regocpp::rego)
 
-add_test(NAME rego_trieste COMMAND rego_trieste test -f WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_trieste>)
+if(REGOCPP_BUILD_TOOLS)
+  add_test(NAME rego_trieste COMMAND rego_trieste test -f WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_trieste>)
+endif()
+
 add_test(NAME yaml_trieste COMMAND yaml_trieste test -f WORKING_DIRECTORY $<TARGET_FILE_DIR:yaml_trieste>)
 add_test(NAME rego_test COMMAND rego_test regocpp.yaml WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
 add_test(NAME rego_test_aci COMMAND rego_test aci/aci.yaml WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
diff --git a/tests/main.cc b/tests/main.cc
index 21ec9bc4..03a0cdca 100644
--- a/tests/main.cc
+++ b/tests/main.cc
@@ -36,7 +36,7 @@ void load_testcase_dir(
   {
     if (std::filesystem::is_directory(file_or_dir))
     {
-      std::cout << std::endl << file_or_dir << std::endl;
+      std::cout << std::endl << file_or_dir.path() << std::endl;
       load_testcase_dir(file_or_dir, debug_path, testcases);
     }
     else if (std::filesystem::exists(file_or_dir))
@@ -46,7 +46,7 @@ void load_testcase_dir(
     }
     else
     {
-      std::cerr << "Not a file: " << file_or_dir << std::endl;
+      std::cerr << "Not a file: " << file_or_dir.path() << std::endl;
     }
   }
 }
@@ -68,6 +68,9 @@ int main(int argc, char** argv)
   bool enable_logging{false};
   app.add_flag("-l,--logging", enable_logging, "Enable logging");
 
+  bool wf_checks{false};
+  app.add_flag("-w,--wf", wf_checks, "Enable well-formedness checks (slow)");
+
   bool fail_first{false};
   app.add_flag(
     "-f,--fail-first", fail_first, "Stop after first test case failure");
@@ -87,7 +90,7 @@ int main(int argc, char** argv)
     return app.exit(e);
   }
 
-  rego::Logger::enabled = enable_logging;
+  rego::set_logging_enabled(enable_logging);
 
   std::cout << "Loading test cases:";
   TestCases all_testcases;
@@ -136,7 +139,7 @@ int main(int argc, char** argv)
       try
       {
         auto start = std::chrono::steady_clock::now();
-        auto result = testcase.run(argv[0], debug_path);
+        auto result = testcase.run(argv[0], debug_path, wf_checks);
         auto end = std::chrono::steady_clock::now();
         const std::chrono::duration<double> elapsed = end - start;
 
diff --git a/tests/parse.cc b/tests/parse.cc
index 60dce349..a7fa3c30 100644
--- a/tests/parse.cc
+++ b/tests/parse.cc
@@ -178,8 +178,8 @@ namespace rego_test
   enum class Collection
   {
     None,
-    Brace,
-    Square
+    Braces,
+    Squares
   };
 
   Parse parser()
@@ -216,12 +216,12 @@ namespace rego_test
             *in_value = false;
             m.term();
             m.push(Brace);
-            stack->push_back(Collection::Brace);
+            stack->push_back(Collection::Braces);
           },
 
         R"(\})" >>
           [stack, indent](auto& m) {
-            if (stack->back() == Collection::Brace)
+            if (stack->back() == Collection::Braces)
             {
               m.term();
               m.pop(Brace);
@@ -243,12 +243,12 @@ namespace rego_test
             indent->disable();
             m.term();
             m.push(Square);
-            stack->push_back(Collection::Square);
+            stack->push_back(Collection::Squares);
           },
 
         R"(\])" >>
           [stack, indent](auto& m) {
-            if (stack->back() == Collection::Square)
+            if (stack->back() == Collection::Squares)
             {
               m.term();
               m.pop(Square);
@@ -465,7 +465,7 @@ namespace rego_test
            {
              indent->disable();
              *in_value = false;
-             stack->push_back(Collection::Brace);
+             stack->push_back(Collection::Braces);
              m.term();
              m.push(Brace);
            }
@@ -473,7 +473,7 @@ namespace rego_test
 
        R"(\})" >>
          [indent, stack](auto& m) {
-           if (stack->back() == Collection::Brace)
+           if (stack->back() == Collection::Braces)
            {
              m.term();
              m.pop(Brace);
@@ -504,7 +504,7 @@ namespace rego_test
            if (indent->complete(m, "start"))
            {
              indent->disable();
-             stack->push_back(Collection::Square);
+             stack->push_back(Collection::Squares);
              m.term();
              m.push(Square);
            }
@@ -512,7 +512,7 @@ namespace rego_test
 
        R"(\])" >>
          [indent, stack](auto& m) {
-           if (stack->back() == Collection::Square)
+           if (stack->back() == Collection::Squares)
            {
              m.term();
              m.pop(Square);
@@ -568,7 +568,7 @@ namespace rego_test
 
         R"(\})" >>
           [stack, quote, indent](auto& m) {
-            if (*quote == Quote::None && stack->back() == Collection::Brace)
+            if (*quote == Quote::None && stack->back() == Collection::Braces)
             {
               m.term();
               m.pop(Brace);
@@ -587,7 +587,7 @@ namespace rego_test
 
         R"(\])" >>
           [stack, quote, indent](auto& m) {
-            if (*quote == Quote::None && stack->back() == Collection::Square)
+            if (*quote == Quote::None && stack->back() == Collection::Squares)
             {
               m.term();
               m.pop(Square);
diff --git a/tests/rego_test.h b/tests/rego_test.h
index 792d2873..cd5b5145 100644
--- a/tests/rego_test.h
+++ b/tests/rego_test.h
@@ -2,10 +2,7 @@
 
 // Trieste-based rewriter for Rego YAML test cases
 
-#include <rego/rego.h>
-#include <rego/wf.h>
-#include <trieste/driver.h>
-#include <trieste/token.h>
+#include <rego/rego.hh>
 
 namespace rego_test
 {
diff --git a/tests/test_case.cc b/tests/test_case.cc
index 79fdc474..3cd29489 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -461,10 +461,12 @@ namespace rego_test
 
   Result TestCase::run(
     const std::filesystem::path& executable_path,
-    const std::filesystem::path& debug_path) const
+    const std::filesystem::path& debug_path,
+    bool wf_checks) const
   {
     rego::Interpreter interpreter;
     interpreter.executable(executable_path);
+    interpreter.well_formed_checks_enabled(wf_checks);
     interpreter.builtins().strict_errors(m_strict_error);
     if (!debug_path.empty())
     {
@@ -480,11 +482,11 @@ namespace rego_test
     interpreter.add_data(m_data);
     if (m_input_term.size() > 0)
     {
-      interpreter.add_input_json(m_input_term);
+      interpreter.set_input_json(m_input_term);
     }
     else
     {
-      interpreter.add_input(m_input);
+      interpreter.set_input(m_input);
     }
 
     bool pass = true;
diff --git a/tests/test_case.h b/tests/test_case.h
index 7e183561..58e6ab13 100644
--- a/tests/test_case.h
+++ b/tests/test_case.h
@@ -1,9 +1,6 @@
 #pragma once
 
-#include <optional>
-#include <rego/rego.h>
-#include <string>
-#include <vector>
+#include <rego/rego.hh>
 
 namespace rego_test
 {
@@ -27,7 +24,8 @@ namespace rego_test
 
     Result run(
       const std::filesystem::path& executable_path,
-      const std::filesystem::path& debug_path) const;
+      const std::filesystem::path& debug_path,
+      bool wf_checks) const;
 
     /** name of the test case category. */
     const std::string& category() const;