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

Issue:
Python binding



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..c8adc1bb
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,15 @@
+#============================== Baseline config ==============================
+[flake8]
+inline-quotes = double
+import-order-style = google
+docstring-convention = google
+max-line-length = 120
+
+extend-ignore =
+    E203,
+    N812,
+    N817,
+
+#============================ Repo-specific config ===========================
+per-file-ignores =
+    wrappers/python/test/*:D103
diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
new file mode 100644
index 00000000..68ec5565
--- /dev/null
+++ b/.github/workflows/build_wheels.yml
@@ -0,0 +1,26 @@
+name: Build Python Wheels for Release
+
+on:
+  workflow_dispatch:
+
+jobs:
+  build_wheels:
+    name: Build wheels on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Build wheels
+        uses: pypa/cibuildwheel@v2.12.0
+        env:
+          CIBW_ARCHS_MACOS: x86_64 arm64
+        with:
+          package-dir: ${{github.workspace}}/wrappers/python/
+
+      - uses: actions/upload-artifact@v3
+        with:
+          path: ./wheelhouse/*.whl
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index d53f59db..1e1e7005 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -124,6 +124,39 @@ jobs:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
 
+  linux-wrappers-python:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python_version: ["3.7", "3.8", "3.9", "3.10"]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Use Python ${{matrix.python_version}}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{matrix.python_version}}
+
+      - name: Get dependencies
+        run: |
+          python -m pip install --upgrade pip
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: Python build
+        working-directory: ${{github.workspace}}/wrappers/python/
+        run: pip install -e .[dev]
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+
+      - name: Python test
+        working-directory: ${{github.workspace}}/wrappers/python/ 
+        run: pytest -vv
+
   windows:
     runs-on: windows-latest
 
@@ -186,6 +219,40 @@ jobs:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
 
+  windows-wrappers-python:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python_version: ["3.7", "3.8", "3.9", "3.10"]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Use Python ${{matrix.python_version}}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{matrix.python_version}}
+          architecture: x64
+
+      - name: Get dependencies
+        run: |
+          python -m pip install --upgrade pip
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: Python build
+        working-directory: ${{github.workspace}}/wrappers/python/
+        run: pip install -e .[dev]
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+
+      - name: Python test
+        working-directory: ${{github.workspace}}/wrappers/python/ 
+        run: pytest -vv
+
   macos:
     runs-on: macos-latest
 
@@ -217,7 +284,7 @@ jobs:
 
       - name: Get dependencies
         run: |
-          brew update && brew install ninja
+          brew update && brew upgrade && brew install ninja
 
       - name: Git config for fetching pull requests
         run: |
@@ -247,7 +314,7 @@ jobs:
 
       - name: Get dependencies
         run: |
-          brew update && brew install rust
+          brew update && brew upgrade && brew install rust
 
       - name: Git config for fetching pull requests
         run: |
@@ -258,4 +325,41 @@ jobs:
         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
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+
+  macos-wrappers-python:
+    runs-on: macos-latest
+    strategy:
+      matrix:
+        python_version: ["3.7", "3.8", "3.9", "3.10"]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Use Python ${{matrix.python_version}}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{matrix.python_version}}
+
+      - name: Get dependencies
+        run: |
+          brew update && brew upgrade && brew install openssl
+          python -m pip install --upgrade pip
+
+      - name: Git config for fetching pull requests
+        run: |
+          git config --global --add remote.origin.fetch +refs/pull/*/merge:refs/remotes/pull/*
+
+      - name: Python build
+        working-directory: ${{github.workspace}}/wrappers/python/
+        run: pip install -e .[dev]
+        env:
+          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
+          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          PYTHONVERBOSE: 3
+
+      - name: Python test
+        working-directory: ${{github.workspace}}/wrappers/python/ 
+        run: pytest -vv
+        env:
+          PYTHONVERBOSE: 3
diff --git a/.gitignore b/.gitignore
index fdb958bb..25a58da7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ _deps
 build
 build-*
 .vscode
-.cache
\ No newline at end of file
+.cache
+.env
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
index 5898ded5..09695817 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,18 @@
 # Changelog
 
+## 2023-09-14 - Version 0.3.6
+Adding a Python wrapper.
+
+**New Features**
+- `wrappers/python/` contains a Python package, `regopy`, which wraps the library
+  and exposes a Python API.
+
+**Bug Fixes**
+- Fixed a bug where `to_json()` was incorrectly sorting arrays when the `sort`
+  flag was set to `true`.
+- Removes a memory leak caused by a cyclic graph of Unifiers
+- Fixes various use-after-free violations caused by missing `clone()` invocations.
+
 ## 2023-09-12 - Version 0.3.5
 Adding C++ documentation.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8bac599f..a341701c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -68,13 +68,14 @@ option(REGOCPP_BUILD_DOCS "Specifies whether to build the documentation" OFF)
 option(REGOCPP_OPA_TESTS "Specifies whether to load and run the OPA built-ins tests" OFF)
 option(REGOCPP_SPECULATIVE_LOAD_HARDENING "Specifies whether to enable speculative load hardening, only supported with Clang" OFF)
 option(REGOCPP_COPY_EXAMPLES "Specifies whether to copy the examples to the install directory" OFF)
+set(REGOCPP_SANITIZE "" CACHE STRING "Argument to pass to sanitize (disabled by default)")
 
 # -------------------- Find packages --------------------------
 
 FetchContent_Declare(
   trieste
   GIT_REPOSITORY https://github.com/microsoft/trieste
-  GIT_TAG        5f75cf7aeb74890066ff249a82fa14dc031a9642
+  GIT_TAG        9d4ae8a75597849cde54d1927b2ed5a5ee9d425b
 )
 
 set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 
diff --git a/README.md b/README.md
index 8aa5ff9d..a29e5d9e 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,6 @@ environment.
 
 ### Linux
 
-> **Note**
-> At the moment, you must use `clang++` to build the project on Linux.
-
 Create a build directory and initialize the cmake project:
 
     mkdir build
diff --git a/VERSION b/VERSION
index 09e91570..53b61ecf 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.3.5
\ No newline at end of file
+0.3.6
\ No newline at end of file
diff --git a/include/rego/rego.hh b/include/rego/rego.hh
index 6e1b7296..9492e1b0 100644
--- a/include/rego/rego.hh
+++ b/include/rego/rego.hh
@@ -760,10 +760,10 @@ namespace rego
     | (Object <<= ObjectItem++)
     | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
     | (DataItem <<= Key * (Val >>= DataModule | Term))[Key]
-    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int))[Var]
-    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int))[Var]
-    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term))[Var]
+    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int) * Key)[Var]
+    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int) * Key)[Var]
+    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Key)[Var]
+    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Key)[Var]
     ;
   // clang-format on
 
@@ -1343,12 +1343,13 @@ namespace rego
   /** Converts a node to JSON.
    *
    * @param node The node to convert.
-   * @param sort Whether to sort the keys of a JSON object or the values in a
-   * set.
    * @param set_as_array Whether to represent a set using array notation.
+   * Otherwise, sets are denoted using "<>" delimiters.
    */
   std::string to_json(
-    const trieste::Node& node, bool sort = false, bool set_as_array = false);
+    const trieste::Node& node,
+    bool set_as_array = false,
+    bool sort_arrays = false);
 
   /**
    * The logging level.
diff --git a/src/builtins.cc b/src/builtins.cc
index e0d5f044..ae3cdf23 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -11,7 +11,7 @@ namespace
 
   Node json_marshal(const Nodes& args)
   {
-    return JSONString ^ to_json(args[0], false, false);
+    return JSONString ^ to_json(args[0], true);
   }
 
   Node print(const Nodes& args)
diff --git a/src/builtins/aggregates.cc b/src/builtins/aggregates.cc
index b66d43b1..3ba6c54e 100644
--- a/src/builtins/aggregates.cc
+++ b/src/builtins/aggregates.cc
@@ -76,12 +76,12 @@ namespace
       return collection;
     }
 
-    Node sorted = collection->clone();
-    std::sort(sorted->begin(), sorted->end(), [](const Node& a, const Node& b) {
+    Nodes items(collection->begin(), collection->end());
+    std::sort(items.begin(), items.end(), [](const Node& a, const Node& b) {
       return to_json(a) < to_json(b);
     });
 
-    return sorted;
+    return collection->type() << NodeRange{items.begin(), items.end()};
   }
 
   Node sum(const Nodes& args)
diff --git a/src/builtins/objects.cc b/src/builtins/objects.cc
index 5735dc62..9a0208b3 100644
--- a/src/builtins/objects.cc
+++ b/src/builtins/objects.cc
@@ -117,7 +117,7 @@ namespace
       return object;
     }
 
-    Node key = args[1];
+    Node key = args[1]->clone();
     if (key->type() != Array)
     {
       key = Array << key;
@@ -126,10 +126,10 @@ namespace
     auto maybe_value = get_key(object, key, 0);
     if (maybe_value.has_value())
     {
-      return maybe_value.value();
+      return maybe_value.value()->clone();
     }
 
-    return args[2];
+    return args[2]->clone();
   }
 
   Node keys(const Nodes& args)
@@ -144,7 +144,7 @@ namespace
     Node value = NodeDef::create(Set);
     for (auto& item : *object)
     {
-      value->push_back(item / Key);
+      value->push_back((item / Key)->clone());
     }
 
     return value;
diff --git a/src/encoding.cc b/src/encoding.cc
index ba317b6e..8e328e19 100644
--- a/src/encoding.cc
+++ b/src/encoding.cc
@@ -1,8 +1,104 @@
 #include "internal.hh"
 
+namespace
+{
+  using namespace rego;
+
+  struct NodeKey
+  {
+    std::string key;
+    Node node;
+    NodeKey(const Node& value)
+    {
+      node = value;
+      if (node->type() == Term)
+      {
+        node = node->front();
+      }
+
+      if (node->type() == Scalar)
+      {
+        node = node->front();
+      }
+
+      key = to_json(node, false);
+    }
+
+    bool operator<(const NodeKey& other) const
+    {
+      if (node->type() == other.node->type())
+      {
+        if (node->type() == Int)
+        {
+          return get_int(node) < get_int(other.node);
+        }
+
+        if (node->type() == Float)
+        {
+          return get_double(node) < get_double(other.node);
+        }
+
+        return key < other.key;
+      }
+      else
+      {
+        if (
+          (node->type() == Float && other.node->type() == Int) ||
+          (node->type() == Int && other.node->type() == Float))
+        {
+          return get_double(node) < get_double(other.node);
+        }
+
+        // type ordering is Null < Bool < Number < String
+        if (node->type() == Null)
+        {
+          return true;
+        }
+
+        if (other.node->type() == Null)
+        {
+          return false;
+        }
+
+        if (node->type() == False)
+        {
+          return true;
+        }
+
+        if (other.node->type() == False)
+        {
+          return false;
+        }
+
+        if (node->type() == True)
+        {
+          return true;
+        }
+
+        if (other.node->type() == True)
+        {
+          return false;
+        }
+
+        if (node->type() == Int || node->type() == Float)
+        {
+          return true;
+        }
+
+        if (other.node->type() == Int || other.node->type() == Float)
+        {
+          return false;
+        }
+
+        return key < other.key;
+      }
+    }
+  };
+}
+
 namespace rego
 {
-  std::string to_json(const Node& node, bool sort, bool set_as_array)
+  std::string to_json(const Node& node, bool set_as_array, bool sort_arrays)
   {
     std::ostringstream buf;
     if (node->type() == Int)
@@ -57,15 +153,28 @@ namespace rego
     else if (node->type() == Array || node->type() == DataArray)
     {
       std::vector<std::string> items;
-      for (const auto& child : *node)
+      if (sort_arrays)
       {
-        items.push_back(to_json(child, sort, set_as_array));
-      }
+        std::vector<NodeKey> keys;
+        for (const auto& child : *node)
+        {
+          keys.push_back(child);
+        }
 
-      if (sort)
+        std::sort(keys.begin(), keys.end());
+        std::transform(
+          keys.begin(), keys.end(), std::back_inserter(items), [&](auto& key) {
+            return to_json(key.node, set_as_array, sort_arrays);
+          });
+      }
+      else
       {
-        std::sort(items.begin(), items.end());
+        for (const auto& child : *node)
+        {
+          items.push_back(to_json(child, set_as_array, sort_arrays));
+        }
       }
+
       buf << "[";
       std::string sep = "";
       for (const auto& item : items)
@@ -77,16 +186,13 @@ namespace rego
     }
     else if (node->type() == Set || node->type() == DataSet)
     {
-      std::vector<std::string> items;
+      std::vector<NodeKey> node_keys;
       for (const auto& child : *node)
       {
-        items.push_back(to_json(child, sort, set_as_array));
+        node_keys.push_back(child);
       }
 
-      if (sort)
-      {
-        std::sort(items.begin(), items.end());
-      }
+      std::sort(node_keys.begin(), node_keys.end());
 
       if (set_as_array)
       {
@@ -97,9 +203,10 @@ namespace rego
         buf << "<";
       }
       std::string sep = "";
-      for (const auto& item : items)
+      for (const auto& node_key : node_keys)
       {
-        buf << sep << item;
+        std::string key_str = to_json(node_key.node, set_as_array, sort_arrays);
+        buf << sep << key_str;
         sep = ", ";
       }
 
@@ -115,16 +222,17 @@ namespace rego
     else if (node->type() == Object || node->type() == DataObject)
     {
       std::map<std::string, std::string> items;
+      std::vector<NodeKey> keys;
       for (const auto& child : *node)
       {
         auto key = child / Key;
         auto value = child / Val;
-        std::string key_str = to_json(key, sort, set_as_array);
+        std::string key_str = to_json(key, set_as_array, sort_arrays);
         if (!key_str.starts_with('"') || !key_str.ends_with('"'))
         {
           key_str = '"' + key_str + '"';
         }
-        items[key_str] = to_json(value, sort, set_as_array);
+        items.insert({key_str, to_json(value, set_as_array, sort_arrays)});
       }
 
       buf << "{";
@@ -141,12 +249,12 @@ namespace rego
       node->type() == Scalar || node->type() == Term ||
       node->type() == DataTerm)
     {
-      return to_json(node->front(), sort, set_as_array);
+      return to_json(node->front(), set_as_array, sort_arrays);
     }
     else if (node->type() == Binding)
     {
       buf << (node / Var)->location().view() << " = "
-          << to_json(node / Term, sort, set_as_array);
+          << to_json(node / Term, set_as_array, sort_arrays);
     }
     else if (node->type() == TermSet)
     {
@@ -154,7 +262,7 @@ namespace rego
       std::string sep = "";
       for (const auto& child : *node)
       {
-        buf << sep << to_json(child, sort, set_as_array);
+        buf << sep << to_json(child, set_as_array, sort_arrays);
         sep = ", ";
       }
       buf << "}";
diff --git a/src/internal.hh b/src/internal.hh
index f0598455..e5f97885 100644
--- a/src/internal.hh
+++ b/src/internal.hh
@@ -5,26 +5,6 @@
 
 #include "rego/rego.hh"
 
-#define LOG_ERROR(...) \
-  Logger::print(LogLevel::Error, Logger::indent, "Error: ", __VA_ARGS__)
-#define LOG_WARNING(...) \
-  Logger::print(LogLevel::Warning, Logger::indent, "Warning: ", __VA_ARGS__)
-#define LOG_INFO(...) Logger::print(LogLevel::Info, Logger::indent, __VA_ARGS__)
-#define LOG_DEBUG(...) \
-  Logger::print(LogLevel::Debug, Logger::indent, __VA_ARGS__)
-#define LOG_Trace(...) \
-  Logger::print(LogLevel::Trace, Logger::indent, __VA_ARGS__)
-#define LOG(...) Logger::print(LogLevel::Debug, Logger::indent, __VA_ARGS__)
-#define LOG_HEADER(message, header) \
-  Logger::print(LogLevel::Debug, Logger::indent, (header), (message), (header))
-#define LOG_VECTOR(vector) \
-  Logger::print_vector_inline(LogLevel::Debug, (vector))
-#define LOG_VECTOR_CUSTOM(vector, transform) \
-  Logger::print_vector_custom(LogLevel::Debug, (vector), (transform))
-#define LOG_MAP_VALUES(map) Logger::print_map_values(LogLevel::Debug, (map))
-#define LOG_INDENT() Logger::increase_print_indent()
-#define LOG_UNINDENT() Logger::decrease_print_indent()
-
 namespace rego
 {
   std::vector<PassCheck> passes(const BuiltIns& builtins);
@@ -422,12 +402,25 @@ namespace rego
     std::size_t m_id;
   };
 
+  enum class UnifierType
+  {
+    RuleBody,
+    RuleValue
+  };
+
+  struct UnifierKey
+  {
+    Location key;
+    UnifierType type;
+    bool operator<(const UnifierKey& other) const;
+  };
+
   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>>;
+  using UnifierCache = std::shared_ptr<std::map<UnifierKey, Unifier>>;
 
   class UnifierDef
   {
@@ -438,19 +431,20 @@ namespace rego
       CallStack call_stack,
       WithStack with_stack,
       const BuiltIns& builtins,
-      UnifierCache cache);
+      UnifierCache unifier_cache);
     Node unify();
     Nodes expressions() const;
     Nodes bindings() const;
     std::string str() const;
     std::string dependency_str() const;
     static Unifier create(
+      const UnifierKey& key,
       const Location& rule,
       const Node& rulebody,
       const CallStack& call_stack,
       const WithStack& with_stack,
       const BuiltIns& builtins,
-      const UnifierCache& cache);
+      const UnifierCache& unifier_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;
@@ -467,9 +461,19 @@ namespace rego
       std::size_t score;
     };
 
-    Unifier rule_unifier(const Location& rule, const Node& rulebody) const;
+    struct Statement
+    {
+      std::size_t id;
+      Node node;
+    };
+
+    static Resolver::NodePrinter stmt_str(const Statement& statement);
+    Unifier rule_unifier(
+      const UnifierKey& key, const Location& rule, const Node& rulebody) const;
     void init_from_body(
-      const Node& rulebody, std::vector<Node>& statements, std::size_t root);
+      const Node& rulebody,
+      std::vector<Statement>& 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);
@@ -480,7 +484,7 @@ namespace rego
     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 dependency_score(const Statement& stmt) const;
     std::size_t detect_cycles() const;
     bool has_cycle(std::size_t id) const;
 
@@ -499,7 +503,9 @@ namespace rego
     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 execute_statements(
+      std::vector<Statement>::iterator begin,
+      std::vector<Statement>::iterator end);
     void remove_invalid_values();
     void mark_invalid_values();
     Variable& get_variable(const Location& name);
@@ -519,16 +525,46 @@ namespace rego
 
     Location m_rule;
     std::map<Location, Variable> m_variables;
-    std::vector<Node> m_statements;
-    NodeMap<std::vector<Node>> m_nested_statements;
+    std::vector<Statement> m_statements;
+    std::map<std::size_t, std::vector<Statement>> m_nested_statements;
     CallStack m_call_stack;
     WithStack m_with_stack;
-    BuiltIns m_builtins;
+    const BuiltIns& m_builtins;
+    UnifierCache m_cache;
     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
+}
+
+#define LOG_ERROR(...) \
+  rego::Logger::print( \
+    rego::LogLevel::Error, rego::Logger::indent, "Error: ", __VA_ARGS__)
+#define LOG_WARN(...) \
+  rego::Logger::print( \
+    rego::LogLevel::Warn, rego::Logger::indent, "Warning: ", __VA_ARGS__)
+#define LOG_INFO(...) \
+  rego::Logger::print(rego::LogLevel::Info, rego::Logger::indent, __VA_ARGS__)
+#define LOG_DEBUG(...) \
+  rego::Logger::print(rego::LogLevel::Debug, rego::Logger::indent, __VA_ARGS__)
+#define LOG_TRACE(...) \
+  rego::Logger::print(rego::LogLevel::Trace, rego::Logger::indent, __VA_ARGS__)
+#define LOG(...) \
+  rego::Logger::print(rego::LogLevel::Debug, rego::Logger::indent, __VA_ARGS__)
+#define LOG_HEADER(message, header) \
+  rego::Logger::print( \
+    rego::LogLevel::Debug, \
+    rego::Logger::indent, \
+    (header), \
+    (message), \
+    (header))
+#define LOG_VECTOR(vector) \
+  rego::Logger::print_vector_inline(rego::LogLevel::Debug, (vector))
+#define LOG_VECTOR_CUSTOM(vector, transform) \
+  rego::Logger::print_vector_custom( \
+    rego::LogLevel::Debug, (vector), (transform))
+#define LOG_MAP_VALUES(map) \
+  rego::Logger::print_map_values(rego::LogLevel::Debug, (map))
+#define LOG_INDENT() rego::Logger::increase_print_indent()
+#define LOG_UNINDENT() rego::Logger::decrease_print_indent()
\ No newline at end of file
diff --git a/src/interpreter.cc b/src/interpreter.cc
index 0e95a8e8..4dd509d0 100644
--- a/src/interpreter.cc
+++ b/src/interpreter.cc
@@ -260,9 +260,18 @@ namespace rego
     }
     else
     {
-      for (auto result : *ast)
+      std::vector<std::string> results;
+      std::transform(
+        ast->begin(),
+        ast->end(),
+        std::back_inserter(results),
+        [](auto& result) { return rego::to_json(result, true); });
+      std::sort(results.begin(), results.end());
+      std::string sep = "";
+      for (const auto& result : results)
       {
-        output_buf << rego::to_json(result, false, true) << std::endl;
+        output_buf << sep << result;
+        sep = "\n";
       }
     }
 
diff --git a/src/passes/assign.cc b/src/passes/assign.cc
index 9264f707..9bc4d3b5 100644
--- a/src/passes/assign.cc
+++ b/src/passes/assign.cc
@@ -34,34 +34,23 @@ namespace
     return path.str();
   }
 
-  bool needs_rewrite(
-    const NodeRange& n,
-    const FunctionArity& func_arity,
-    const std::shared_ptr<NodeMap<bool>>& cache)
+  bool needs_rewrite(const NodeRange& n, const FunctionArity& func_arity)
   {
     Node ruleref = n.first[0];
     Node argseq = n.first[1];
-    if (cache->contains(ruleref))
-    {
-      return cache->at(ruleref);
-    }
 
     if (!is_in(ruleref, {UnifyBody}))
     {
-      cache->insert({ruleref, false});
       return false;
     }
 
     std::string path = concat(ruleref);
     if (!func_arity->contains(path))
     {
-      cache->insert({ruleref, false});
       return false;
     }
 
     bool result = func_arity->at(path) == argseq->size() - 1;
-
-    cache->insert({ruleref, result});
     return result;
   }
 }
@@ -75,7 +64,6 @@ namespace rego
   // Transforms unification expressions into AssignInfix nodes.
   PassDef assign(const BuiltIns& builtins)
   {
-    auto cache = std::make_shared<NodeMap<bool>>();
     FunctionArity func_arity =
       std::make_shared<std::map<std::string, std::size_t>>();
 
@@ -118,10 +106,10 @@ namespace rego
       In(Literal) *
           (T(Expr)
            << (T(ExprCall) << (T(RuleRef)[RuleRef] * T(ArgSeq)[ArgSeq])(
-                 [func_arity, cache](auto& n) {
+                 [func_arity](auto& n) {
                    // tests if this is a function call that has an extra
                    // argument that should be unified with the result.
-                   return needs_rewrite(n, func_arity, cache);
+                   return needs_rewrite(n, func_arity);
                  }))) >>
         [](Match& _) {
           // A convention in the Golang implementation of Rego is
@@ -145,8 +133,8 @@ namespace rego
                    (T(AssignArg)
                     << (T(ExprCall)
                         << (T(RuleRef)[RuleRef] *
-                            T(ArgSeq)[ArgSeq])([func_arity, cache](auto& n) {
-                             return needs_rewrite(n, func_arity, cache) &&
+                            T(ArgSeq)[ArgSeq])([func_arity](auto& n) {
+                             return needs_rewrite(n, func_arity) &&
                                is_in(*n.first, {UnifyBody});
                            })))))) >>
         [](Match& _) {
diff --git a/src/passes/functions.cc b/src/passes/functions.cc
index dfb42d14..eac40442 100644
--- a/src/passes/functions.cc
+++ b/src/passes/functions.cc
@@ -5,6 +5,17 @@ namespace
   using namespace rego;
   using namespace wf::ops;
 
+  int add_key(Node node)
+  {
+    if (node->back()->type() != Key)
+    {
+      node->push_back(Key ^ node->fresh({"rule"}));
+      return 1;
+    }
+
+    return 0;
+  }
+
   const auto inline VarOrTerm = T(Var) / T(Term);
   const auto inline RefArg = T(RefArgDot) / T(RefArgBrack);
 }
@@ -16,7 +27,7 @@ namespace rego
   // function that takes either <var> or <term> arguments.
   PassDef functions()
   {
-    return {
+    PassDef functions = {
       In(Input) * T(DataTerm)[DataTerm] >>
         [](Match& _) { return Term << *_[DataTerm]; },
 
@@ -272,5 +283,12 @@ namespace rego
             "Syntax error: module not allowed as object item value");
         },
     };
+
+    functions.post(RuleComp, add_key);
+    functions.post(RuleFunc, add_key);
+    functions.post(RuleSet, add_key);
+    functions.post(RuleObj, add_key);
+
+    return functions;
   }
 }
\ No newline at end of file
diff --git a/src/passes/init.cc b/src/passes/init.cc
index 90051387..56e7a60e 100644
--- a/src/passes/init.cc
+++ b/src/passes/init.cc
@@ -11,7 +11,31 @@ namespace
     std::set<Location> rhs_vars;
   };
 
-  using InitStatments = std::shared_ptr<NodeMap<InitInfo>>;
+  Node to_init(
+    const Node& lhs,
+    const std::set<Location>& lhs_locs,
+    const Node& rhs,
+    const std::set<Location>& rhs_locs)
+  {
+    Node lhs_vars = NodeDef::create(VarSeq);
+    for (auto& loc : lhs_locs)
+    {
+      lhs_vars << (Var ^ loc);
+    }
+
+    Node rhs_vars = NodeDef::create(VarSeq);
+    for (auto& loc : rhs_locs)
+    {
+      rhs_vars << (Var ^ loc);
+    }
+
+    if (rhs_vars->size() > 0 && lhs_vars->size() == 0)
+    {
+      return LiteralInit << rhs_vars << lhs_vars << (AssignInfix << rhs << lhs);
+    }
+
+    return LiteralInit << lhs_vars << rhs_vars << (AssignInfix << lhs << rhs);
+  }
 
   void vars_from(Node node, std::set<Location>& vars)
   {
@@ -58,12 +82,12 @@ namespace
     }
   }
 
-  void find_init_stmts(
-    Node unifybody, std::set<Location>& locals, InitStatments init_stmts)
+  void find_init_stmts(Node unifybody, std::set<Location>& locals)
   {
     // gather all locals
-    for (Node stmt : *unifybody)
+    for (std::size_t i = 0; i < unifybody->size(); ++i)
     {
+      Node stmt = unifybody->at(i);
       if (stmt->type() == Local)
       {
         locals.insert((stmt / Var)->location());
@@ -71,15 +95,15 @@ namespace
       else if (stmt->type() == LiteralEnum)
       {
         locals.erase((stmt / Item)->location());
-        find_init_stmts(stmt / UnifyBody, locals, init_stmts);
+        find_init_stmts(stmt / UnifyBody, locals);
       }
       else if (stmt->type() == LiteralWith)
       {
-        find_init_stmts(stmt / UnifyBody, locals, init_stmts);
+        find_init_stmts(stmt / UnifyBody, locals);
       }
       else if (stmt->type() == LiteralNot)
       {
-        find_init_stmts(stmt / UnifyBody, locals, init_stmts);
+        find_init_stmts(stmt / UnifyBody, locals);
       }
       else if (stmt->type() == Literal)
       {
@@ -126,12 +150,12 @@ namespace
           locals.erase(loc);
         }
 
-        init_stmts->insert({stmt, InitInfo{lhs_found, rhs_found}});
+        unifybody->replace_at(i, to_init(lhs, lhs_found, rhs, rhs_found));
       }
     }
   }
 
-  void register_init_stmts(const Node& rule, InitStatments init_stmts)
+  void register_init_stmts(const Node& rule)
   {
     Node body = rule / Body;
     Node val = rule / Val;
@@ -139,13 +163,13 @@ namespace
     if (body->type() == UnifyBody)
     {
       std::set<Location> locals;
-      find_init_stmts(body, locals, init_stmts);
+      find_init_stmts(body, locals);
     }
 
     if (val->type() == UnifyBody)
     {
       std::set<Location> locals;
-      find_init_stmts(val, locals, init_stmts);
+      find_init_stmts(val, locals);
     }
   }
 }
@@ -161,72 +185,37 @@ namespace rego
   // initialized. These allow later passes to correctly handle these statements.
   PassDef init()
   {
-    InitStatments init_stmts = std::make_shared<NodeMap<InitInfo>>();
+    PassDef init = {dir::once | dir::bottomup};
 
-    PassDef init = {
-      dir::once | dir::bottomup,
-      {
-        In(UnifyBody) *
-            (T(Literal)[Literal](
-               [init_stmts](auto& n) { return init_stmts->contains(*n.first); })
-             << (T(Expr)
-                 << (T(AssignInfix)
-                     << (T(AssignArg)[Lhs] * T(AssignArg)[Rhs])))) >>
-          [init_stmts](Match& _) {
-            InitInfo& init_info = init_stmts->at(_(Literal));
-
-            Node lhs_vars = NodeDef::create(VarSeq);
-            for (auto& loc : init_info.lhs_vars)
-            {
-              lhs_vars << (Var ^ loc);
-            }
-
-            Node rhs_vars = NodeDef::create(VarSeq);
-            for (auto& loc : init_info.rhs_vars)
-            {
-              rhs_vars << (Var ^ loc);
-            }
-
-            if (rhs_vars->size() > 0 && lhs_vars->size() == 0)
-            {
-              return LiteralInit << rhs_vars << lhs_vars
-                                 << (AssignInfix << _(Rhs) << _(Lhs));
-            }
-
-            return LiteralInit << lhs_vars << rhs_vars
-                               << (AssignInfix << _(Lhs) << _(Rhs));
-          },
-      }};
-
-    init.pre(RuleComp, [init_stmts](Node rule) {
-      register_init_stmts(rule, init_stmts);
+    init.pre(RuleComp, [](Node rule) {
+      register_init_stmts(rule);
       return 0;
     });
 
-    init.pre(RuleFunc, [init_stmts](Node rule) {
-      register_init_stmts(rule, init_stmts);
+    init.pre(RuleFunc, [](Node rule) {
+      register_init_stmts(rule);
       return 0;
     });
 
-    init.pre(RuleSet, [init_stmts](Node rule) {
-      register_init_stmts(rule, init_stmts);
+    init.pre(RuleSet, [](Node rule) {
+      register_init_stmts(rule);
       return 0;
     });
 
-    init.pre(RuleObj, [init_stmts](Node rule) {
-      register_init_stmts(rule, init_stmts);
+    init.pre(RuleObj, [](Node rule) {
+      register_init_stmts(rule);
       return 0;
     });
 
-    init.pre(NestedBody, [init_stmts](Node nested) {
+    init.pre(NestedBody, [](Node nested) {
       std::set<Location> locals;
-      find_init_stmts(nested / Val, locals, init_stmts);
+      find_init_stmts(nested / Val, locals);
       return 0;
     });
 
-    init.pre(ExprEvery, [init_stmts](Node exprevery) {
+    init.pre(ExprEvery, [](Node exprevery) {
       std::set<Location> locals;
-      find_init_stmts(exprevery / UnifyBody, locals, init_stmts);
+      find_init_stmts(exprevery / UnifyBody, locals);
       return 0;
     });
 
diff --git a/src/passes/lift_refheads.cc b/src/passes/lift_refheads.cc
index 66552ee5..2e3e8033 100644
--- a/src/passes/lift_refheads.cc
+++ b/src/passes/lift_refheads.cc
@@ -55,7 +55,7 @@ namespace
     return false;
   }
 
-  void prepend_refs(Node& node, Node& prefix_ref, RefHeads refheads)
+  void prepend_refs(const Node& node, Node& prefix_ref, RefHeads refheads)
   {
     if (node->type() == Term || node->type() == RuleRef)
     {
diff --git a/src/passes/locals.cc b/src/passes/locals.cc
index 2c53d6e6..a8f363f3 100644
--- a/src/passes/locals.cc
+++ b/src/passes/locals.cc
@@ -3,7 +3,7 @@
 namespace
 {
   using namespace rego;
-  using Scope = std::map<std::string, Node>;
+  using Scope = std::map<Location, bool>;
 
   void add_locals(
     Node unifybody, std::vector<Scope>& scopes, const BuiltIns& builtins);
@@ -21,12 +21,11 @@ namespace
     if (node->type() == Local)
     {
       Node var = node / Var;
-      std::string name = std::string(var->location().view());
       for (auto& scope : scopes)
       {
-        if (scope.contains(name))
+        if (scope.contains(var->location()))
         {
-          scope[name] = Undefined;
+          scope[var->location()] = false;
         }
       }
       return;
@@ -39,8 +38,7 @@ namespace
         return;
       }
 
-      std::string name = std::string(node->location().view());
-      if (name == "data")
+      if (node->location().view() == "data")
       {
         // reserved
         return;
@@ -48,7 +46,7 @@ namespace
 
       for (auto& scope : scopes)
       {
-        if (scope.contains(name))
+        if (scope.contains(node->location()))
         {
           return;
         }
@@ -60,7 +58,7 @@ namespace
         return;
       }
 
-      scopes.back().insert({name, Local << node->clone() << Undefined});
+      scopes.back().insert({node->location(), true});
     }
     else if (node->type() == UnifyBody)
     {
@@ -94,11 +92,11 @@ namespace
       find_free_vars(child, scopes, builtins);
     }
 
-    for (const auto& [name, local] : scopes.back())
+    for (const auto& [loc, add] : scopes.back())
     {
-      if (local->type() == Local)
+      if (add)
       {
-        unifybody->push_front(local);
+        unifybody->push_front(Local << (Var ^ loc) << Undefined);
       }
     }
 
diff --git a/src/rego_c.cc b/src/rego_c.cc
index ca916146..f553a41d 100644
--- a/src/rego_c.cc
+++ b/src/rego_c.cc
@@ -53,16 +53,20 @@ extern "C"
 
   regoInterpreter* regoNew()
   {
-    return reinterpret_cast<regoInterpreter*>(new rego::Interpreter());
+    auto ptr = reinterpret_cast<regoInterpreter*>(new rego::Interpreter());
+    LOG_DEBUG("regoNew: ", ptr);
+    return ptr;
   }
 
   void regoFree(regoInterpreter* rego)
   {
+    LOG_DEBUG("regoFree: ", rego);
     delete reinterpret_cast<rego::Interpreter*>(rego);
   }
 
   regoEnum regoAddModuleFile(regoInterpreter* rego, const char* path)
   {
+    LOG_DEBUG("regoAddModuleFile: ", path);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->add_module_file(path);
@@ -78,6 +82,7 @@ extern "C"
   regoEnum regoAddModule(
     regoInterpreter* rego, const char* name, const char* contents)
   {
+    LOG_DEBUG("regoAddModule: ", name);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->add_module(name, contents);
@@ -92,6 +97,7 @@ extern "C"
 
   regoEnum regoAddDataJSONFile(regoInterpreter* rego, const char* path)
   {
+    LOG_DEBUG("regoAddDataJSONFile: ", path);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->add_data_json_file(path);
@@ -106,6 +112,7 @@ extern "C"
 
   regoEnum regoAddDataJSON(regoInterpreter* rego, const char* contents)
   {
+    LOG_DEBUG("regoAddDataJSON: ", contents);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->add_data_json(contents);
@@ -120,6 +127,7 @@ extern "C"
 
   regoEnum regoSetInputJSONFile(regoInterpreter* rego, const char* path)
   {
+    LOG_DEBUG("regoSetInputJSONFile: ", path);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->set_input_json_file(path);
@@ -134,6 +142,7 @@ extern "C"
 
   regoEnum regoSetInputJSON(regoInterpreter* rego, const char* contents)
   {
+    LOG_DEBUG("regoSetInputJSON: ", contents);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->set_input_json(contents);
@@ -148,16 +157,19 @@ extern "C"
 
   void regoSetDebugEnabled(regoInterpreter* rego, regoBoolean enabled)
   {
+    LOG_DEBUG("regoSetDebugEnabled: ", enabled);
     reinterpret_cast<rego::Interpreter*>(rego)->debug_enabled(enabled);
   }
 
   regoBoolean regoGetDebugEnabled(regoInterpreter* rego)
   {
+    LOG_DEBUG("regoGetDebugEnabled");
     return reinterpret_cast<rego::Interpreter*>(rego)->debug_enabled();
   }
 
   regoEnum regoSetDebugPath(regoInterpreter* rego, const char* path)
   {
+    LOG_DEBUG("regoSetDebugPath: ", path);
     try
     {
       reinterpret_cast<rego::Interpreter*>(rego)->debug_path(path);
@@ -173,26 +185,30 @@ extern "C"
   void regoSetWellFormedChecksEnabled(
     regoInterpreter* rego, regoBoolean enabled)
   {
+    LOG_DEBUG("regoSetWellFormedChecksEnabled: ", enabled);
     reinterpret_cast<rego::Interpreter*>(rego)->well_formed_checks_enabled(
       enabled);
   }
 
   regoBoolean regoGetWellFormedChecksEnabled(regoInterpreter* rego)
   {
+    LOG_DEBUG("regoGetWellFormedChecksEnabled");
     return reinterpret_cast<rego::Interpreter*>(rego)
       ->well_formed_checks_enabled();
   }
 
   regoOutput* regoQuery(regoInterpreter* rego, const char* query_expr)
   {
+    LOG_DEBUG("regoQuery: ", query_expr);
     try
     {
       auto interpreter = reinterpret_cast<rego::Interpreter*>(rego);
       rego::regoOutput* output = new rego::regoOutput();
       output->node = interpreter->raw_query(query_expr);
       output->value = interpreter->output_to_string(output->node);
-
-      return reinterpret_cast<regoOutput*>(output);
+      auto ptr = reinterpret_cast<regoOutput*>(output);
+      LOG_DEBUG("regoQuery output: ", ptr);
+      return ptr;
     }
     catch (const std::exception& e)
     {
@@ -203,12 +219,14 @@ extern "C"
 
   void regoSetStrictBuiltInErrors(regoInterpreter* rego, regoBoolean enabled)
   {
+    LOG_DEBUG("regoSetStrictBuiltInErrors: ", enabled);
     reinterpret_cast<rego::Interpreter*>(rego)->builtins().strict_errors(
       enabled);
   }
 
   regoBoolean regoGetStrictBuiltInErrors(regoInterpreter* rego)
   {
+    LOG_DEBUG("regoGetStrictBuiltInErrors");
     return reinterpret_cast<rego::Interpreter*>(rego)
       ->builtins()
       .strict_errors();
@@ -217,18 +235,21 @@ extern "C"
   // Output functions
   regoBoolean regoOutputOk(regoOutput* output)
   {
+    LOG_DEBUG("regoOutputOk");
     return reinterpret_cast<rego::regoOutput*>(output)->node->type() !=
       rego::ErrorSeq;
   }
 
   regoNode* regoOutputNode(regoOutput* output)
   {
+    LOG_DEBUG("regoOutputNode");
     return reinterpret_cast<regoNode*>(
       reinterpret_cast<rego::regoOutput*>(output)->node.get());
   }
 
   regoNode* regoOutputBinding(regoOutput* output, const char* name)
   {
+    LOG_DEBUG("regoOutputBinding: ", name);
     auto output_ptr = reinterpret_cast<rego::regoOutput*>(output);
     auto node_ptr = reinterpret_cast<trieste::NodeDef*>(output_ptr->node.get());
     if (node_ptr->type() == rego::ErrorSeq)
@@ -251,17 +272,20 @@ extern "C"
 
   const char* regoOutputString(regoOutput* output)
   {
+    LOG_DEBUG("regoOutputString");
     return reinterpret_cast<rego::regoOutput*>(output)->value.c_str();
   }
 
   void regoFreeOutput(regoOutput* output)
   {
+    LOG_DEBUG("regoFreeOutput: ", output);
     delete reinterpret_cast<rego::regoOutput*>(output);
   }
 
   // Node functions
   regoEnum regoNodeType(regoNode* node_ptr)
   {
+    LOG_DEBUG("regoNodeType");
     auto node = reinterpret_cast<trieste::NodeDef*>(node_ptr);
 
     if (node->type() == rego::Binding)
@@ -369,11 +393,13 @@ extern "C"
 
   const char* regoNodeTypeName(regoNode* node)
   {
+    LOG_DEBUG("regoNodeTypeName");
     return reinterpret_cast<trieste::NodeDef*>(node)->type().str();
   }
 
   regoSize regoNodeValueSize(regoNode* node)
   {
+    LOG_DEBUG("regoNodeValueSize");
     std::size_t size =
       reinterpret_cast<trieste::NodeDef*>(node)->location().view().size();
     return static_cast<regoSize>(size + 1);
@@ -381,6 +407,7 @@ extern "C"
 
   regoEnum regoNodeValue(regoNode* node, char* buffer, regoSize size)
   {
+    LOG_DEBUG("regoNodeValue: ", buffer, "[", size, "]");
     std::string_view view =
       reinterpret_cast<trieste::NodeDef*>(node)->location().view();
     if (size < view.size() + 1)
@@ -395,12 +422,14 @@ extern "C"
 
   regoSize regoNodeSize(regoNode* node)
   {
+    LOG_DEBUG("regoNodeSize");
     std::size_t size = reinterpret_cast<trieste::NodeDef*>(node)->size();
     return static_cast<regoSize>(size);
   }
 
   regoNode* regoNodeGet(regoNode* node_ptr, regoSize index)
   {
+    LOG_DEBUG("regoNodeGet: ", index);
     trieste::NodeDef* node = reinterpret_cast<trieste::NodeDef*>(node_ptr);
     if (index >= node->size())
     {
@@ -412,6 +441,7 @@ extern "C"
 
   regoSize regoNodeJSONSize(regoNode* node)
   {
+    LOG_DEBUG("regoNodeJSONSize");
     auto node_ptr = reinterpret_cast<trieste::NodeDef*>(node);
     std::string json = rego::to_json(node_ptr->shared_from_this());
     return static_cast<regoSize>(json.size() + 1);
@@ -419,6 +449,7 @@ extern "C"
 
   regoEnum regoNodeJSON(regoNode* node, char* buffer, regoSize size)
   {
+    LOG_DEBUG("regoNodeJSON: ", buffer, "[", size, "]");
     auto node_ptr = reinterpret_cast<trieste::NodeDef*>(node);
     std::string json = rego::to_json(node_ptr->shared_from_this());
     if (size < json.size() + 1)
diff --git a/src/resolver.cc b/src/resolver.cc
index c1f7df81..b0d2757f 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -558,14 +558,14 @@ namespace rego
   {
     if (value->type() == Term || value->type() == TermSet)
     {
-      return value;
+      return value->clone();
     }
 
     if (
       value->type() == Array || value->type() == Set ||
       value->type() == Object || value->type() == Scalar)
     {
-      return Term << value;
+      return Term << value->clone();
     }
 
     if (
@@ -573,7 +573,7 @@ namespace rego
       value->type() == JSONString || value->type() == True ||
       value->type() == False || value->type() == Null)
     {
-      return Term << (Scalar << value);
+      return Term << (Scalar << value->clone());
     }
 
     return err(value, "Not a term");
@@ -1040,7 +1040,7 @@ namespace rego
       }
       else if (rulearg->type() == ArgVar)
       {
-        rulearg->at(1) = arg;
+        rulearg->replace_at(1, arg);
       }
     }
 
@@ -1113,7 +1113,7 @@ namespace rego
       if (!values.contains(repr))
       {
         values.insert(repr);
-        reduce->push_back(term);
+        reduce->push_back(term->clone());
       }
     }
 
@@ -1134,14 +1134,23 @@ namespace rego
     }
 
     Node rulebody = defs[0] / Val;
+    Location rulename{"query"};
+    UnifierCache cache = std::make_shared<std::map<UnifierKey, Unifier>>();
     auto unifier = UnifierDef::create(
-      {"query"},
+      UnifierKey{rulename, UnifierType::RuleBody},
+      rulename,
       rulebody,
       std::make_shared<std::vector<Location>>(),
       std::make_shared<std::vector<ValuesLookup>>(),
       builtins,
-      std::make_shared<NodeMap<Unifier>>());
+      cache);
     Node result = unifier->unify();
+
+    // now that unification is complete, we need to empty the
+    // Unifier cache. Since they all maintain a pointer to the
+    // cache, it will not be able to be freed otherwise.
+    cache->clear();
+
     if (result->type() == Error)
     {
       return Query << result;
@@ -1411,7 +1420,7 @@ namespace rego
 
     if (termset->type() == Term)
     {
-      terms->push_back(termset->front());
+      terms->push_back(termset->front()->clone());
       return;
     }
 
@@ -1429,7 +1438,7 @@ namespace rego
       }
       else if (term->type() == Term)
       {
-        terms->push_back(term->front());
+        terms->push_back(term->front()->clone());
       }
       else
       {
@@ -1443,8 +1452,8 @@ namespace rego
     if (termset->type() == Term)
     {
       Node array = termset->front();
-      items->push_back(array->front());
-      items->push_back(array->back());
+      items->push_back(array->front()->clone());
+      items->push_back(array->back()->clone());
       return;
     }
 
@@ -1463,8 +1472,8 @@ namespace rego
       else if (term->type() == Term)
       {
         Node array = term->front();
-        items->push_back(array->front());
-        items->push_back(array->back());
+        items->push_back(array->front()->clone());
+        items->push_back(array->back()->clone());
       }
       else
       {
@@ -1518,7 +1527,7 @@ namespace rego
 
     if (current->type() != Object)
     {
-      LOG(
+      LOG_WARN(
         "Conflict: cannot merge partials into non-object: ", term_str(current));
       Node replacement = NodeDef::create(Object);
       current->parent()->replace(current, replacement);
@@ -1541,12 +1550,13 @@ namespace rego
 
     if (existing == nullptr)
     {
-      current->push_back(ObjectItem << (Term << key) << (Term << value));
+      current->push_back(
+        ObjectItem << (Term << key) << (Term << value->clone()));
     }
     else
     {
       current->replace(
-        existing, ObjectItem << (Term << key) << (Term << value));
+        existing, ObjectItem << (Term << key) << (Term << value->clone()));
     }
   }
 }
\ No newline at end of file
diff --git a/src/unifier.cc b/src/unifier.cc
index 49e911f8..98f312f1 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -15,8 +15,8 @@ namespace rego
     m_call_stack(call_stack),
     m_with_stack(with_stack),
     m_builtins(builtins),
-    m_parent_type(rulebody->parent()->type()),
     m_cache(cache),
+    m_parent_type(rulebody->parent()->type()),
     m_negate(false)
   {
     LOG_HEADER("ASSEMBLING UNIFICATION", "---");
@@ -95,7 +95,7 @@ namespace rego
   }
 
   void UnifierDef::init_from_body(
-    const Node& rulebody, std::vector<Node>& statements, std::size_t root)
+    const Node& rulebody, std::vector<Statement>& statements, std::size_t root)
   {
     std::for_each(rulebody->begin(), rulebody->end(), [&](const auto& stmt) {
       if (stmt->type() == Local)
@@ -105,19 +105,18 @@ namespace rego
       }
       else if (stmt->type() == UnifyExpr)
       {
-        statements.push_back(stmt);
         std::size_t id = add_unifyexpr(stmt);
+        statements.push_back({id, stmt});
         m_dependency_graph[id].dependencies.insert(root);
       }
       else if (stmt->type() == UnifyExprWith)
       {
-        statements.push_back(stmt);
         std::size_t id = m_dependency_graph.size();
+        statements.push_back({id, stmt});
         std::string name = "with" + std::to_string(id);
         m_dependency_graph.push_back({name, {root}, 0});
-        m_expr_ids[stmt] = id;
-        m_nested_statements[stmt] = {};
-        init_from_body(stmt / UnifyBody, m_nested_statements[stmt], id);
+        m_nested_statements[id] = {};
+        init_from_body(stmt / UnifyBody, m_nested_statements[id], id);
 
         Node withseq = stmt / WithSeq;
         std::vector<std::size_t> dep_ids;
@@ -137,16 +136,15 @@ namespace rego
       }
       else if (stmt->type() == UnifyExprNot)
       {
-        statements.push_back(stmt);
         std::size_t id = m_dependency_graph.size();
+        statements.push_back({id, stmt});
         std::string name = "not" + std::to_string(id);
         m_dependency_graph.push_back({name, {root}, 0});
-        m_expr_ids[stmt] = id;
-        m_nested_statements[stmt] = {};
-        init_from_body(stmt->front(), m_nested_statements[stmt], root);
-        for (auto& nested : m_nested_statements[stmt])
+        m_nested_statements[id] = {};
+        init_from_body(stmt->front(), m_nested_statements[id], root);
+        for (auto& nested : m_nested_statements[id])
         {
-          m_dependency_graph[id].dependencies.insert(m_expr_ids[nested]);
+          m_dependency_graph[id].dependencies.insert(nested.id);
         }
       }
     });
@@ -181,7 +179,6 @@ namespace rego
         return m_variables.at(dep).id();
       });
     std::size_t expr_id = m_dependency_graph.size();
-    m_expr_ids[unifyexpr] = expr_id;
     std::string name = Resolver::expr_str(unifyexpr).str();
     m_dependency_graph.push_back(
       {name, std::set(dep_ids.begin(), dep_ids.end()), 0});
@@ -232,9 +229,9 @@ namespace rego
     }
   }
 
-  std::size_t UnifierDef::dependency_score(const Node& expr) const
+  std::size_t UnifierDef::dependency_score(const Statement& stmt) const
   {
-    return m_dependency_graph[m_expr_ids.at(expr)].score;
+    return m_dependency_graph[stmt.id].score;
   }
 
   std::size_t UnifierDef::dependency_score(const Variable& var) const
@@ -276,17 +273,18 @@ namespace rego
   }
 
   void UnifierDef::execute_statements(
-    Nodes::iterator begin, Nodes::iterator end)
+    std::vector<Statement>::iterator begin,
+    std::vector<Statement>::iterator end)
   {
     for (auto it = begin; it != end; ++it)
     {
-      Node stmt = *it;
-      if (stmt->type() == UnifyExpr)
+      Statement stmt = *it;
+      if (stmt.node->type() == UnifyExpr)
       {
-        LOG(Resolver::expr_str(stmt));
-        Node lhs = stmt / Var;
+        LOG(Resolver::expr_str(stmt.node));
+        Node lhs = stmt.node / Var;
         Variable& var = get_variable(lhs->location());
-        Values values = evaluate(lhs->location(), stmt / Val);
+        Values values = evaluate(lhs->location(), stmt.node / Val);
         if (values.size() == 0)
         {
           if (m_negate && var.is_unify())
@@ -333,22 +331,22 @@ namespace rego
           }
         }
       }
-      else if (stmt->type() == UnifyExprNot)
+      else if (stmt.node->type() == UnifyExprNot)
       {
-        LOG(Resolver::not_str(stmt));
+        LOG(Resolver::not_str(stmt.node));
         push_not();
         LOG_INDENT();
-        auto nested = m_nested_statements[stmt];
+        auto nested = m_nested_statements[stmt.id];
         execute_statements(nested.begin(), nested.end());
         LOG_UNINDENT();
         pop_not();
       }
-      else if (stmt->type() == UnifyExprWith)
+      else if (stmt.node->type() == UnifyExprWith)
       {
-        LOG(Resolver::with_str(stmt));
-        push_with(stmt / WithSeq);
+        LOG(Resolver::with_str(stmt.node));
+        push_with(stmt.node / WithSeq);
         LOG_INDENT();
-        auto nested = m_nested_statements[stmt];
+        auto nested = m_nested_statements[stmt.id];
         execute_statements(nested.begin(), nested.end());
         LOG_UNINDENT();
         pop_with();
@@ -470,7 +468,7 @@ namespace rego
     LOG_HEADER("Unification", "=====");
 
     LOG("exprs: ");
-    LOG_VECTOR_CUSTOM(m_statements, Resolver::stmt_str);
+    LOG_VECTOR_CUSTOM(m_statements, UnifierDef::stmt_str);
 
     LOG_INDENT();
 
@@ -795,8 +793,10 @@ namespace rego
           for (auto partial : partials)
           {
             Node partial_node = partial->node();
-            merged->insert(
-              merged->end(), partial_node->begin(), partial_node->end());
+            for (auto& node : *partial_node)
+            {
+              merged->push_back(node->clone());
+            }
           }
           values.push_back(ValueDef::create(var, merged, partials));
         }
@@ -1074,7 +1074,7 @@ namespace rego
           return std::nullopt;
         }
 
-        object_items->push_back(arg->node());
+        object_items->push_back(arg->node()->clone());
       }
       return ValueDef::create(
         var, Resolver::object(object_items, false), sources);
@@ -1090,7 +1090,7 @@ namespace rego
           return std::nullopt;
         }
 
-        array_members->push_back(arg->node());
+        array_members->push_back(arg->node()->clone());
       }
       return ValueDef::create(var, Resolver::array(array_members), sources);
     }
@@ -1105,7 +1105,7 @@ namespace rego
           return std::nullopt;
         }
 
-        set_members->push_back(arg->node());
+        set_members->push_back(arg->node()->clone());
       }
       return ValueDef::create(var, Resolver::set(set_members), sources);
     }
@@ -1172,7 +1172,7 @@ namespace rego
         for (std::size_t i = 0; i < container->size(); ++i)
         {
           Node index = Scalar << (Int ^ std::to_string(i));
-          Node tuple = Term << (Array << index << container->at(i));
+          Node tuple = Term << (Array << index << container->at(i)->clone());
           items.push_back(ValueDef::create(var, tuple));
         }
       }
@@ -1181,8 +1181,9 @@ namespace rego
       {
         for (const Node& object_item : *container)
         {
-          Node tuple = Term
-            << (Array << object_item / Key << object_item / Val);
+          Node key = (object_item / Key)->clone();
+          Node val = (object_item / Val)->clone();
+          Node tuple = Term << (Array << key << val);
           items.push_back(ValueDef::create(var, tuple));
         }
       }
@@ -1191,7 +1192,7 @@ namespace rego
       {
         for (const Node& value : *container)
         {
-          Node tuple = Term << (Array << value << value);
+          Node tuple = Term << (Array << value->clone() << value->clone());
           items.push_back(ValueDef::create(var, tuple));
         }
       }
@@ -1521,7 +1522,7 @@ namespace rego
           continue;
         }
 
-        object->push_back(ObjectItem << key << val);
+        object->push_back(ObjectItem << key->clone() << val->clone());
       }
     }
 
@@ -1590,6 +1591,7 @@ namespace rego
         return std::nullopt;
       }
 
+      Location rulekey = (rulecomp / Key)->location();
       Location rulename = (rulecomp / Var)->location();
       Node rulebody = rulecomp / Body;
       Node value = rulecomp / Val;
@@ -1604,7 +1606,10 @@ namespace rego
       {
         try
         {
-          body_result = rule_unifier(rulename, rulebody)->unify();
+          body_result =
+            rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
+              ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1626,7 +1631,8 @@ namespace rego
           LOG("Evaluating rule comp value");
           try
           {
-            Unifier unifier = rule_unifier(rulename, value);
+            Unifier unifier = rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node binding_val;
@@ -1712,6 +1718,7 @@ namespace rego
       return std::nullopt;
     }
 
+    Location rulekey = (rule / Key)->location();
     Location rulename = (rule / Var)->location();
     Node rulebody = rule / Body;
     Node body_result;
@@ -1724,7 +1731,10 @@ namespace rego
     {
       try
       {
-        body_result = rule_unifier(rulename, rulebody)->unify();
+        body_result =
+          rule_unifier(
+            UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
+            ->unify();
       }
       catch (const std::exception& e)
       {
@@ -1752,7 +1762,8 @@ namespace rego
       LOG("Evaluating rule func value");
       try
       {
-        Unifier unifier = rule_unifier(rulename, value);
+        Unifier unifier = rule_unifier(
+          UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
         unifier->unify();
         auto bindings = unifier->bindings();
         for (auto& binding : bindings)
@@ -1786,6 +1797,7 @@ namespace rego
         return std::nullopt;
       }
 
+      Location rulekey = (rule / Key)->location();
       Location rulename = (rule / Var)->location();
       Node rulebody = rule / Body;
       Node value = rule / Val;
@@ -1800,7 +1812,10 @@ namespace rego
       {
         try
         {
-          body_result = rule_unifier(rulename, rulebody)->unify();
+          body_result =
+            rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
+              ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1822,7 +1837,8 @@ namespace rego
           LOG("Evaluating rule set value");
           try
           {
-            Unifier unifier = rule_unifier(rulename, value);
+            Unifier unifier = rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node result;
@@ -1847,7 +1863,10 @@ namespace rego
       if (body_result->type() == True && value->type() == Term)
       {
         Node set = value->front();
-        argseq->insert(argseq->end(), set->begin(), set->end());
+        for (auto& item : *set)
+        {
+          argseq->push_back(item->clone());
+        }
       }
 
       if (value->type() == Error)
@@ -1881,6 +1900,7 @@ namespace rego
         return std::nullopt;
       }
 
+      Location rulekey = (rule / Key)->location();
       Location rulename = (rule / Var)->location();
       Node rulebody = rule / Body;
       Node value = rule / Val;
@@ -1895,7 +1915,10 @@ namespace rego
       {
         try
         {
-          body_result = rule_unifier(rulename, rulebody)->unify();
+          body_result =
+            rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
+              ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1917,7 +1940,8 @@ namespace rego
           LOG("Evaluating rule obj value");
           try
           {
-            Unifier unifier = rule_unifier(rulename, value);
+            Unifier unifier = rule_unifier(
+              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node result;
@@ -1944,8 +1968,8 @@ namespace rego
         Node obj = value->front();
         for (Node item : *obj)
         {
-          argseq->push_back(item / Key);
-          argseq->push_back(item / Val);
+          argseq->push_back((item / Key)->clone());
+          argseq->push_back((item / Val)->clone());
         }
       }
 
@@ -2014,6 +2038,7 @@ namespace rego
   }
 
   Unifier UnifierDef::create(
+    const UnifierKey& key,
     const Location& rule,
     const Node& rulebody,
     const CallStack& call_stack,
@@ -2021,26 +2046,26 @@ namespace rego
     const BuiltIns& builtins,
     const UnifierCache& cache)
   {
-    if (cache->contains(rulebody))
+    if (cache->contains(key))
     {
-      Unifier unifier = cache->at(rulebody);
+      Unifier unifier = cache->at(key);
       unifier->reset();
       return unifier;
     }
     else
     {
-      Unifier unifier = std::make_shared<UnifierDef>(
-        rule, rulebody, call_stack, with_stack, builtins, cache);
-      cache->insert({rulebody, unifier});
+      Unifier unifier = std::shared_ptr<UnifierDef>(new UnifierDef(
+        rule, rulebody, call_stack, with_stack, builtins, cache));
+      cache->insert({key, unifier});
       return unifier;
     }
   }
 
   Unifier UnifierDef::rule_unifier(
-    const Location& rule, const Node& rulebody) const
+    const UnifierKey& key, const Location& rule, const Node& rulebody) const
   {
     return create(
-      rule, rulebody, m_call_stack, m_with_stack, m_builtins, m_cache);
+      key, rule, rulebody, m_call_stack, m_with_stack, m_builtins, m_cache);
   }
 
   bool UnifierDef::is_variable(const Location& name) const
@@ -2075,4 +2100,34 @@ namespace rego
     LOG("Popping not: ", m_negate, " => ", !m_negate);
     m_negate = !m_negate;
   }
+
+  Resolver::NodePrinter UnifierDef::stmt_str(const Statement& stmt)
+  {
+    return Resolver::stmt_str(stmt.node);
+  }
+
+  bool UnifierKey::operator<(const UnifierKey& other) const
+  {
+    if (key < other.key)
+    {
+      return true;
+    }
+
+    if (key > other.key)
+    {
+      return false;
+    }
+
+    if (type < other.type)
+    {
+      return true;
+    }
+
+    if (type > other.type)
+    {
+      return false;
+    }
+
+    return false;
+  }
 }
\ No newline at end of file
diff --git a/src/value.cc b/src/value.cc
index 1f1e06b2..a51b3cf5 100644
--- a/src/value.cc
+++ b/src/value.cc
@@ -194,10 +194,10 @@ namespace rego
   {
     if (m_node->type() == Term || m_node->type() == Error)
     {
-      return m_node;
+      return m_node->clone();
     }
 
-    Node term = m_node;
+    Node term = m_node->clone();
     if (scalar_tokens.contains(term->type()))
     {
       return Term << (Scalar << term);
diff --git a/src/variable.cc b/src/variable.cc
index 61faa75c..e0f88d5b 100644
--- a/src/variable.cc
+++ b/src/variable.cc
@@ -182,7 +182,7 @@ namespace rego
     Node term = to_term();
     if (term->type() == Error)
     {
-      m_local->back() = term;
+      m_local->replace_at(1, term);
       return term;
     }
 
@@ -190,12 +190,12 @@ namespace rego
     {
       if (term->size() == 0)
       {
-        m_local->back() = Undefined;
+        m_local->replace_at(1, Undefined);
         return Undefined;
       }
     }
 
-    m_local->back() = term;
+    m_local->replace_at(1, term);
     return term;
   }
 
diff --git a/wrappers/python/.gitignore b/wrappers/python/.gitignore
new file mode 100644
index 00000000..bac992e5
--- /dev/null
+++ b/wrappers/python/.gitignore
@@ -0,0 +1,128 @@
+## Python ##
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+*.ilk
+*.pdb
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/wrappers/python/CMakeLists.txt b/wrappers/python/CMakeLists.txt
new file mode 100644
index 00000000..917c6dc3
--- /dev/null
+++ b/wrappers/python/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+cmake_minimum_required(VERSION 3.15)
+
+include(FetchContent)
+
+project( regopy VERSION 0.1.0 LANGUAGES C CXX )
+
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+if (DEFINED ENV{REGOCPP_C_WRAPPER_REPO})
+  set(REGOCPP_C_WRAPPER_REPO $ENV{REGOCPP_C_WRAPPER_REPO})
+else ()
+  set(REGOCPP_C_WRAPPER_REPO "https://github.com/microsoft/rego-cpp/")
+endif ()
+
+if (DEFINED ENV{REGOCPP_C_WRAPPER_TAG})
+  set(REGOCPP_C_WRAPPER_TAG $ENV{REGOCPP_C_WRAPPER_TAG})
+else ()
+  set(REGOCPP_C_WRAPPER_TAG "main")
+endif()
+
+FetchContent_Declare(
+  regocpp
+  GIT_REPOSITORY ${REGOCPP_C_WRAPPER_REPO}
+  GIT_TAG        ${REGOCPP_C_WRAPPER_TAG}
+)
+
+FetchContent_Declare(
+  pybind11
+  GIT_REPOSITORY https://github.com/pybind/pybind11/
+  GIT_TAG        v2.11.1
+)
+
+set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 
+set(SNMALLOC_ENABLE_DYNAMIC_LOADING ON)
+
+FetchContent_MakeAvailable(regocpp)
+FetchContent_MakeAvailable(pybind11)
+
+set(PYBIND11_CPP_STANDARD -std=c++20)
+pybind11_add_module(_regopy src/_regopy.cc)
+target_link_libraries(_regopy PRIVATE regocpp::rego)
+set_property(TARGET rego PROPERTY POSITION_INDEPENDENT_CODE ON)
+set_property(TARGET snmallocshim-static PROPERTY POSITION_INDEPENDENT_CODE ON)
+set_property(TARGET re2 PROPERTY POSITION_INDEPENDENT_CODE ON)
\ No newline at end of file
diff --git a/wrappers/python/README.md b/wrappers/python/README.md
new file mode 100644
index 00000000..33a28425
--- /dev/null
+++ b/wrappers/python/README.md
@@ -0,0 +1,142 @@
+# `regopy`
+
+[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 module 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 Python interfaces to the Rego query engine.
+We hope the project is of use to those wishing to leverage the power of Rego
+within a Python context.
+
+> **Warning**
+> While this project has progressed to the point that we support full Rego language
+> (see [Language Support](#language-support) below) we do not support all built-ins.
+> That said, we have verified compliance with the OPA Rego test suite. Even so, it
+> should still be considered experimental software and used with discretion.
+
+## Language Support
+
+At present we support v0.55.0 of Rego as defined by OPA, with the following grammar:
+
+```ebnf
+module          = package { import } policy
+package         = "package" ref
+import          = "import" ref [ "as" var ]
+policy          = { rule }
+rule            = [ "default" ] rule-head { rule-body }
+rule-head       = ( ref | var ) ( rule-head-set | rule-head-obj | rule-head-func | rule-head-comp )
+rule-head-comp  = [ assign-operator term ] [ "if" ]
+rule-head-obj   = "[" term "]" [ assign-operator term ] [ "if" ]
+rule-head-func  = "(" rule-args ")" [ assign-operator term ] [ "if" ]
+rule-head-set   = "contains" term [ "if" ] | "[" term "]"
+rule-args       = term { "," term }
+rule-body       = [ "else" [ assign-operator term ] [ "if" ] ] ( "{" query "}" ) | literal
+query           = literal { ( ";" | ( [CR] LF ) ) literal }
+literal         = ( some-decl | expr | "not" expr ) { with-modifier }
+with-modifier   = "with" term "as" term
+some-decl       = "some" term { "," term } { "in" expr }
+expr            = term | expr-call | expr-infix | expr-every | expr-parens | unary-expr
+expr-call       = var [ "." var ] "(" [ expr { "," expr } ] ")"
+expr-infix      = expr infix-operator expr
+expr-every      = "every" var { "," var } "in" ( term | expr-call | expr-infix ) "{" query "}"
+expr-parens     = "(" expr ")"
+unary-expr      = "-" expr
+membership      = term [ "," term ] "in" term
+term            = ref | var | scalar | array | object | set | membership | array-compr | object-compr | set-compr
+array-compr     = "[" term "|" query "]"
+set-compr       = "{" term "|" query "}"
+object-compr    = "{" object-item "|" query "}"
+infix-operator  = assign-operator | bool-operator | arith-operator | bin-operator
+bool-operator   = "==" | "!=" | "<" | ">" | ">=" | "<="
+arith-operator  = "+" | "-" | "*" | "/"
+bin-operator    = "&" | "|"
+assign-operator = ":=" | "="
+ref             = ( var | array | object | set | array-compr | object-compr | set-compr | expr-call ) { ref-arg }
+ref-arg         = ref-arg-dot | ref-arg-brack
+ref-arg-brack   = "[" ( scalar | var | array | object | set | "_" ) "]"
+ref-arg-dot     = "." var
+var             = ( ALPHA | "_" ) { ALPHA | DIGIT | "_" }
+scalar          = string | NUMBER | TRUE | FALSE | NULL
+string          = STRING | raw-string
+raw-string      = "`" { CHAR-"`" } "`"
+array           = "[" term { "," term } "]"
+object          = "{" object-item { "," object-item } "}"
+object-item     = ( scalar | ref | var ) ":" term
+set             = empty-set | non-empty-set
+non-empty-set   = "{" term { "," term } "}"
+empty-set       = "set(" ")"
+```
+
+Definitions:
+```
+[]     optional (zero or one instances)
+{}     repetition (zero or more instances)
+|      alternation (one of the instances)
+()     grouping (order of expansion)
+STRING JSON string
+NUMBER JSON number
+TRUE   JSON true
+FALSE  JSON false
+NULL   JSON null
+CHAR   Unicode character
+ALPHA  ASCII characters A-Z and a-z
+DIGIT  ASCII characters 0-9
+CR     Carriage Return
+LF     Line Feed
+```
+
+### Builtins
+
+At the moment only support a few builtins, but are actively working on adding
+all the standard builtins. The following builtins are currently supported:
+
+- `aggregates`
+- `arrays`
+- `bits`
+- `casts`
+- `numbers`
+- `objects`
+- `regex`
+- `semver`
+- `sets`
+- `strings`
+- `types`
+- `units`
+- miscellaneous
+    * `base64_encode`
+    * `base64_decode`
+    * `json.marshal`
+    * `opa.runtime`
+    * `print`
+    * `time.now_ns`
+
+### Compatibility with the OPA Rego Go implementation
+
+Our goal is to achieve and maintain full compatibility with the reference Go
+implementation. We have developed a test driver which runs the same tests
+and validates that we produce the same outputs. At this stage we pass all
+the non-builtin specific test suites, which we clone from the
+[OPA repository](https://github.com/open-policy-agent/opa/tree/main/test/cases/testdata).
+To build with the OPA tests available for testing, use one of the following presets:
+- `release-clang-opa`
+- `release-opa`
+
+At present, we are **NOT** passing the following test suites in full:
+- `base64*`
+- `crypto*`
+- `glob*`
+- `graphql`
+- `invalidkeyerror`
+- `json*`
+- `jwt*`
+- `net*`
+- `planner-ir`
+- `providers-aws`
+- `reachable`
+- `urlbuiltins`
+- `walkbuiltin`
diff --git a/wrappers/python/docs/make.bat b/wrappers/python/docs/make.bat
new file mode 100644
index 00000000..747ffb7b
--- /dev/null
+++ b/wrappers/python/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.https://www.sphinx-doc.org/
+	exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/wrappers/python/docs/source/api.rst b/wrappers/python/docs/source/api.rst
new file mode 100644
index 00000000..8514b2da
--- /dev/null
+++ b/wrappers/python/docs/source/api.rst
@@ -0,0 +1,34 @@
+.. _api:
+
+API
+===
+
+.. module:: regopy
+
+This part of the documentation covers all the interfaces of `regopy`.
+
+Interpreter
+-----------
+
+.. autoclass:: Interpreter
+    :members:
+
+Output
+------
+
+.. autoclass:: Output
+    :members:
+
+Node
+----
+
+.. autoclass:: Node
+    :members:
+
+.. autoenum:: NodeKind
+
+Logging
+-------
+
+.. autoenum:: LogLevel
+.. autofunction:: set_log_level
diff --git a/wrappers/python/docs/source/conf.py b/wrappers/python/docs/source/conf.py
new file mode 100644
index 00000000..49561629
--- /dev/null
+++ b/wrappers/python/docs/source/conf.py
@@ -0,0 +1,36 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'regopy'
+copyright = '2023, Microsoft'
+author = 'Microsoft'
+release = '0.3.6'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.napoleon",
+    "enum_tools.autoenum",
+    "sphinx_autodoc_typehints"
+]
+
+napoleon_use_param = True
+
+
+templates_path = ['_templates']
+exclude_patterns = []
+
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'alabaster'
+html_static_path = ['_static']
diff --git a/wrappers/python/docs/source/index.rst b/wrappers/python/docs/source/index.rst
new file mode 100644
index 00000000..f548a16a
--- /dev/null
+++ b/wrappers/python/docs/source/index.rst
@@ -0,0 +1,21 @@
+.. regopy documentation master file, created by
+   sphinx-quickstart on Wed Sep 13 23:04:59 2023.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to regopy's documentation!
+==================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   api
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/wrappers/python/pyproject.toml b/wrappers/python/pyproject.toml
new file mode 100644
index 00000000..d2c39abe
--- /dev/null
+++ b/wrappers/python/pyproject.toml
@@ -0,0 +1,7 @@
+[build-system]
+requires = [
+    "setuptools>=42",
+    "wheel",
+    "cmake>=3.15",
+]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/wrappers/python/setup.cfg b/wrappers/python/setup.cfg
new file mode 100644
index 00000000..5c8d488e
--- /dev/null
+++ b/wrappers/python/setup.cfg
@@ -0,0 +1,10 @@
+[flake8]
+# these error codes interfere with Black
+ignore = E203, E231, E501, W503, B950
+select = C,E,F,W,B,B9
+
+[tool:pytest]
+addopts = -ra --doctest-modules
+testpaths =
+    test
+    src/regopy
diff --git a/wrappers/python/setup.py b/wrappers/python/setup.py
new file mode 100644
index 00000000..322b12de
--- /dev/null
+++ b/wrappers/python/setup.py
@@ -0,0 +1,122 @@
+"""Setup script for scenepic."""
+
+import os
+import multiprocessing
+import platform
+import subprocess
+import sys
+
+from setuptools import Extension, find_packages, setup
+from setuptools.command.build_ext import build_ext
+
+REQUIRES_DEV = [
+    "flake8",
+    "flake8-bugbear",
+    "flake8-builtins",
+    "flake8-docstrings",
+    "flake8-import-order",
+    "flake8-quotes",
+    "pep8-naming",
+    "pytest",
+    "sphinx",
+    "sphinx-autodoc-typehints",
+    "sphinx-rtd-theme",
+    "enum-tools[sphinx]",
+    "twine",
+    "wheel"
+]
+
+with open("README.md", "r") as file:
+    LONG_DESCRIPTION = file.read()
+
+with open("../../VERSION", "r") as file:
+    VERSION = file.read()
+
+
+class CMakeExtension(Extension):
+    """Extension class for cmake."""
+
+    def __init__(self, name: str, source_dir=""):
+        """Initializer.
+
+        Args:
+            name (str): The extension name
+            source_dir (str, optional): The directory containing the source
+                                        files. Defaults to "".
+        """
+        Extension.__init__(self, name, sources=[])
+        self.source_dir = os.path.abspath(source_dir)
+
+
+class CMakeBuild(build_ext):
+    """Build driver for cmake."""
+
+    def run(self):
+        """Runs the build extension."""
+        for ext in self.extensions:
+            self.build_extension(ext)
+
+    def build_extension(self, ext: CMakeExtension):
+        """Builds the specified extension.
+
+        Args:
+            ext (CMakeExtension): the cmake extension.
+        """
+        cfg = "Debug" if self.debug else "Release"
+        extdir = os.path.abspath(os.path.dirname(
+            self.get_ext_fullpath(ext.name)))
+        cmake_args = ["-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + extdir,
+                      "-DCMAKE_BUILD_TYPE=" + cfg,
+                      "-DPYTHON_EXECUTABLE=" + sys.executable]
+
+        build_args = ["--config", cfg, "--target", "_regopy"]
+        if not os.path.exists(self.build_temp):
+            os.makedirs(self.build_temp)
+
+        num_workers = multiprocessing.cpu_count()
+        build_args += ["--parallel", str(num_workers)]
+
+        if platform.system() == "Windows":
+            cmake_args += ["-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir)]
+            if platform.architecture()[0] == "64bit":
+                cmake_args += ["-A", "x64"]
+            else:
+                cmake_args += ["-A", "Win32"]
+
+        subprocess.check_call(["cmake", ext.source_dir] +
+                              cmake_args, cwd=self.build_temp)
+        subprocess.check_call(["cmake", "--build", "."] +
+                              build_args, cwd=self.build_temp)
+
+
+setup(
+    name="regopy",
+    version=VERSION,
+    author="rego-cpp Team",
+    author_email="rego-cpp@microsoft.com",
+    description="Python interface for the OPA Rego Language and Runtime",
+    license="MIT",
+    long_description=LONG_DESCRIPTION,
+    long_description_content_type="text/markdown",
+    packages=find_packages("src"),
+    package_dir={"": "src"},
+    python_requires=">=3.6, <4",
+    ext_modules=[CMakeExtension("regopy._regopy")],
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "License :: OSI Approved :: MIT License"
+    ],
+    project_urls={
+        "Documentation": "https://microsoft.github.io/scenepic/",
+        "Bug Reports": "https://github.com/microsoft/scenepic/issues",
+        "Source": "https://github.com/microsoft/scenepic",
+    },
+    url="https://microsoft.github.io/scenepic/",
+    install_requires=[],
+    extras_require={
+        "dev": REQUIRES_DEV,
+    },
+    tests_require=["pytest"],
+    cmdclass=dict(build_ext=CMakeBuild),
+    zip_safe=False,
+)
diff --git a/wrappers/python/src/_regopy.cc b/wrappers/python/src/_regopy.cc
new file mode 100644
index 00000000..db84dbe4
--- /dev/null
+++ b/wrappers/python/src/_regopy.cc
@@ -0,0 +1,213 @@
+#include <pybind11/operators.h>
+#include <pybind11/pybind11.h>
+#include <rego/rego_c.h>
+
+using namespace pybind11::literals;
+
+namespace {
+  std::string get_value(regoNode* node)
+  {
+    regoSize size = regoNodeValueSize(node);
+    std::vector<char> value(size);
+    regoNodeValue(node, value.data(), size);
+    return std::string(value.begin(), value.end() - 1);
+  }
+
+  std::string get_json(regoNode* node)
+  {
+    regoSize size = regoNodeJSONSize(node);
+    std::vector<char> json(size);
+    regoNodeJSON(node, json.data(), size);
+    return std::string(json.begin(), json.end() - 1);
+  }
+}
+
+PYBIND11_MODULE(_regopy, m)
+{
+  m.attr("REGOCPP_VERSION") = REGOCPP_VERSION;
+  m.attr("REGOCPP_OPA_VERSION") = REGOCPP_OPA_VERSION;
+  m.attr("REGOCPP_GIT_HASH") = REGOCPP_GIT_HASH;
+  m.attr("REGOCPP_BUILD_NAME") = REGOCPP_BUILD_NAME;
+  m.attr("REGOCPP_BUILD_DATE") = REGOCPP_BUILD_DATE;
+  m.attr("REGOCPP_BUILD_TOOLCHAIN") = REGOCPP_BUILD_TOOLCHAIN;
+  m.attr("REGOCPP_PLATFORM") = REGOCPP_PLATFORM;
+
+  // error codes
+  m.attr("REGO_OK") = REGO_OK;
+  m.attr("REGO_ERROR") = REGO_ERROR;
+
+  // term node types
+  m.attr("REGO_NODE_BINDING") = REGO_NODE_BINDING;
+  m.attr("REGO_NODE_VAR") = REGO_NODE_VAR;
+  m.attr("REGO_NODE_TERM") = REGO_NODE_TERM;
+  m.attr("REGO_NODE_SCALAR") = REGO_NODE_SCALAR;
+  m.attr("REGO_NODE_ARRAY") = REGO_NODE_ARRAY;
+  m.attr("REGO_NODE_SET") = REGO_NODE_SET;
+  m.attr("REGO_NODE_OBJECT") = REGO_NODE_OBJECT;
+  m.attr("REGO_NODE_OBJECT_ITEM") = REGO_NODE_OBJECT_ITEM;
+  m.attr("REGO_NODE_INT") = REGO_NODE_INT;
+  m.attr("REGO_NODE_FLOAT") = REGO_NODE_FLOAT;
+  m.attr("REGO_NODE_STRING") = REGO_NODE_STRING;
+  m.attr("REGO_NODE_TRUE") = REGO_NODE_TRUE;
+  m.attr("REGO_NODE_FALSE") = REGO_NODE_FALSE;
+  m.attr("REGO_NODE_NULL") = REGO_NODE_NULL;
+  m.attr("REGO_NODE_UNDEFINED") = REGO_NODE_UNDEFINED;
+
+  m.attr("REGO_NODE_ERROR") = REGO_NODE_ERROR;
+  m.attr("REGO_NODE_ERROR_MESSAGE") = REGO_NODE_ERROR_MESSAGE;
+  m.attr("REGO_NODE_ERROR_AST") = REGO_NODE_ERROR_AST;
+  m.attr("REGO_NODE_ERROR_CODE") = REGO_NODE_ERROR_CODE;
+  m.attr("REGO_NODE_ERROR_SEQ") = REGO_NODE_ERROR_SEQ;
+
+  m.attr("REGO_NODE_INTERNAL") = REGO_NODE_INTERNAL;
+
+  // log levels
+  m.attr("REGO_LOG_LEVEL_NONE") = REGO_LOG_LEVEL_NONE;
+  m.attr("REGO_LOG_LEVEL_ERROR") = REGO_LOG_LEVEL_ERROR;
+  m.attr("REGO_LOG_LEVEL_WARN") = REGO_LOG_LEVEL_WARN;
+  m.attr("REGO_LOG_LEVEL_INFO") = REGO_LOG_LEVEL_INFO;
+  m.attr("REGO_LOG_LEVEL_DEBUG") = REGO_LOG_LEVEL_DEBUG;
+  m.attr("REGO_LOG_LEVEL_TRACE") = REGO_LOG_LEVEL_TRACE;
+
+  // Interpreter functions
+  m.def(
+    "regoSetLogLevel",
+    &regoSetLogLevel,
+    "Sets the level of logging.",
+    "level"_a);
+  m.def("regoNew", &regoNew, "Returns a pointer to a new rego instance.");
+  m.def("regoFree", &regoFree, "Deletes a rego instance.");
+  m.def(
+    "regoAddModuleFile",
+    &regoAddModuleFile,
+    "Adds a module file to the rego instance.",
+    "rego"_a,
+    "filename"_a);
+  m.def(
+    "regoAddModule",
+    &regoAddModule,
+    "Adds a module to the rego instance.",
+    "rego"_a,
+    "name"_a,
+    "contents"_a);
+  m.def(
+    "regoAddDataJSONFile",
+    &regoAddDataJSONFile,
+    "Adds a base document file to the rego instance.",
+    "rego"_a,
+    "filename"_a);
+  m.def(
+    "regoAddDataJSON",
+    &regoAddDataJSON,
+    "Adds a base document to the rego instance.",
+    "rego"_a,
+    "contents"_a);
+  m.def(
+    "regoSetInputJSONFile",
+    &regoSetInputJSONFile,
+    "Sets the input document file for the rego instance.",
+    "rego"_a,
+    "filename"_a);
+  m.def(
+    "regoSetInputJSON",
+    &regoSetInputJSON,
+    "Sets the input document for the rego instance.",
+    "rego"_a,
+    "contents"_a);
+  m.def(
+    "regoSetDebugEnabled",
+    &regoSetDebugEnabled,
+    "Sets the debug enabled flag for the rego instance.",
+    "rego"_a,
+    "enabled"_a);
+  m.def(
+    "regoGetDebugEnabled",
+    &regoGetDebugEnabled,
+    "Gets the debug enabled flag for the rego instance.",
+    "rego"_a);
+  m.def(
+    "regoSetDebugPath",
+    &regoSetDebugPath,
+    "Sets the debug path for the rego instance.",
+    "rego"_a,
+    "path"_a);
+  m.def(
+    "regoSetWellFormedChecksEnabled",
+    &regoSetWellFormedChecksEnabled,
+    "Sets the well formed checks enabled flag for the rego instance.",
+    "rego"_a,
+    "enabled"_a);
+  m.def(
+    "regoGetWellFormedChecksEnabled",
+    &regoGetWellFormedChecksEnabled,
+    "Gets the well formed checks enabled flag for the rego instance.",
+    "rego"_a);
+  m.def(
+    "regoQuery",
+    &regoQuery,
+    "Queries the rego instance.",
+    "rego"_a,
+    "query_expr"_a);
+  m.def(
+    "regoSetStrictBuiltInErrors",
+    &regoSetStrictBuiltInErrors,
+    "Sets the strict built-in errors flag for the rego instance.",
+    "rego"_a,
+    "enabled"_a);
+  m.def(
+    "regoGetStrictBuiltInErrors",
+    &regoGetStrictBuiltInErrors,
+    "Gets the strict built-in errors flag for the rego instance.",
+    "rego"_a);
+  m.def(
+    "regoGetError",
+    &regoGetError,
+    "Gets the error from the rego instance.",
+    "rego"_a);
+
+  // Output functions
+  m.def(
+    "regoOutputOk",
+    &regoOutputOk,
+    "Returns true if the output is ok.",
+    "output"_a);
+  m.def(
+    "regoOutputNode", &regoOutputNode, "Returns the output node.", "output"_a);
+  m.def(
+    "regoOutputBinding",
+    &regoOutputBinding,
+    "Returns the output binding.",
+    "output"_a,
+    "name"_a);
+  m.def(
+    "regoOutputString",
+    &regoOutputString,
+    "Returns the output string.",
+    "output"_a);
+  m.def("regoFreeOutput", &regoFreeOutput, "Frees the output.", "output"_a);
+
+  // Node functions
+  m.def("regoNodeType", &regoNodeType, "Returns the node type.", "node"_a);
+  m.def(
+    "regoNodeTypeName",
+    &regoNodeTypeName,
+    "Returns a human-readable node type name.",
+    "node"_a);
+  m.def(
+    "regoNodeValue",
+    &get_value,
+    "Gets the node value as a string.",
+    "node"_a);
+  m.def("regoNodeSize", &regoNodeSize, "Returns the node size.", "node"_a);
+  m.def(
+    "regoNodeGet",
+    &regoNodeGet,
+    "Returns the node at the specified index.",
+    "node"_a,
+    "index"_a);
+  m.def(
+    "regoNodeJSON",
+    &get_json,
+    "Gets the node's JSON representation.",
+    "node"_a);
+}
\ No newline at end of file
diff --git a/wrappers/python/src/regopy/__init__.py b/wrappers/python/src/regopy/__init__.py
new file mode 100644
index 00000000..a3bea70c
--- /dev/null
+++ b/wrappers/python/src/regopy/__init__.py
@@ -0,0 +1,44 @@
+"""regopy - Python wrapper for rego-cpp."""
+
+from enum import IntEnum
+
+from ._regopy import (
+    REGO_LOG_LEVEL_DEBUG,
+    REGO_LOG_LEVEL_ERROR,
+    REGO_LOG_LEVEL_INFO,
+    REGO_LOG_LEVEL_NONE,
+    REGO_LOG_LEVEL_TRACE,
+    REGO_LOG_LEVEL_WARN,
+    REGOCPP_VERSION,
+    regoSetLogLevel
+)
+from .interpreter import Interpreter, RegoError
+from .node import Node, NodeKind
+from .output import Output
+
+__version__ = REGOCPP_VERSION
+
+__all__ = [
+    "Interpreter", "RegoError",
+    "Output",
+    "Node", "NodeKind",
+    "regoNew", "regoFree",
+]
+
+
+class LogLevel(IntEnum):
+    NONE = REGO_LOG_LEVEL_NONE
+    ERROR = REGO_LOG_LEVEL_ERROR
+    WARN = REGO_LOG_LEVEL_WARN
+    INFO = REGO_LOG_LEVEL_INFO
+    DEBUG = REGO_LOG_LEVEL_DEBUG
+    TRACE = REGO_LOG_LEVEL_TRACE
+
+
+def set_log_level(level: LogLevel):
+    """Sets the log level.
+
+    Args:
+        level (LogLevel): The log level.
+    """
+    regoSetLogLevel(level)
diff --git a/wrappers/python/src/regopy/interpreter.py b/wrappers/python/src/regopy/interpreter.py
new file mode 100644
index 00000000..89010dea
--- /dev/null
+++ b/wrappers/python/src/regopy/interpreter.py
@@ -0,0 +1,370 @@
+"""Module providing a Pythonic interface to the rego-cpp interpreter."""
+
+import json
+from typing import Any
+
+from ._regopy import (
+    REGO_ERROR,
+    regoAddDataJSON,
+    regoAddModule,
+    regoFree,
+    regoGetDebugEnabled,
+    regoGetError,
+    regoGetStrictBuiltInErrors,
+    regoGetWellFormedChecksEnabled,
+    regoNew,
+    regoQuery,
+    regoSetDebugEnabled,
+    regoSetDebugPath,
+    regoSetInputJSON,
+    regoSetStrictBuiltInErrors,
+    regoSetWellFormedChecksEnabled
+)
+from .output import Output
+
+
+class RegoError(Exception):
+    """Exception raised when an error occurs in the rego-cpp interpreter."""
+
+    def __init__(self, message: str):
+        """Initializer.
+
+        Args:
+            message (str): The error message.
+        """
+        Exception.__init__(self, message)
+
+
+class Interpreter:
+    """Pythonic interface to the rego-cpp interpreter.
+
+    This wraps the Rego C API, and handles passing calls to
+    the C API and converting the results to Python types.
+
+    Examples:
+        >>> from regopy import Interpreter
+        >>> rego = Interpreter()
+        >>> print(rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4"))
+        x = 5
+        y = 9.4
+
+        >>> input = {
+        ...     "a": 10,
+        ...     "b": "20",
+        ...     "c": 30.0,
+        ...     "d": True
+        ... }
+        >>> data0 = {
+        ...     "one": {
+        ...         "bar": "Foo",
+        ...         "baz": 5,
+        ...         "be": True,
+        ...         "bop": 23.4
+        ...     },
+        ...     "two": {
+        ...         "bar": "Bar",
+        ...         "baz": 12.3,
+        ...         "be": False,
+        ...         "bop": 42
+        ...     }
+        ... }
+        >>> data1 = {
+        ...     "three": {
+        ...         "bar": "Baz",
+        ...         "baz": 15,
+        ...         "be": True,
+        ...         "bop": 4.23
+        ...     }
+        ... }
+        >>> module = '''
+        ...     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"]
+        ... '''
+        >>> rego.set_input(input)
+        >>> rego.add_data(data0)
+        >>> rego.add_data(data1)
+        >>> rego.add_module("objects", module)
+        >>> print(rego.query("x=[data.one, input.b, data.objects.sites[1]]"))
+        x = [{"bar":"Foo", "baz":5, "be":true, "bop":23.4}, "20", {"name":"smoke1"}]
+    """
+
+    def __init__(self):
+        """Initializer."""
+        self._impl = regoNew()
+
+    def __del__(self):
+        """Destructor."""
+        regoFree(self._impl)
+
+    def add_module(self, name: str, source: str):
+        """Adds a module (i.e. a virtual document) to the interpreter.
+
+        Args:
+            name (str): The module name.
+            source (str): The module source.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        Example:
+            >>> from regopy import Interpreter
+            >>> module = '''
+            ...   package scalars
+            ...
+            ...   greeting := "Hello"
+            ...   max_height := 42
+            ...   pi := 3.14159
+            ...   allowed := true
+            ...   location := null
+            ... '''
+            >>> rego = Interpreter()
+            >>> rego.add_module("scalars", module)
+            >>> output = rego.query("data.scalars.greeting")
+            >>> print(output)
+            "Hello"
+        """
+        err = regoAddModule(self._impl, name, source)
+        if err == REGO_ERROR:
+            raise RegoError(regoGetError(self._impl))
+
+    def add_data_json(self, json: str):
+        """Adds data (i.e. a base document) to the interpreter.
+
+        The data should be provided as single JSON-encoded object.
+
+        Args:
+            json (str): The data JSON.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        Example:
+            >>> from regopy import Interpreter
+            >>> rego = Interpreter()
+            >>> data = '''
+            ...   {
+            ...     "one": {
+            ...       "bar": "Foo",
+            ...       "baz": 5,
+            ...       "be": true,
+            ...       "bop": 23.4
+            ...     },
+            ...     "two": {
+            ...       "bar": "Bar",
+            ...       "baz": 12.3,
+            ...       "be": false,
+            ...       "bop": 42
+            ...     }
+            ...   }
+            ... '''
+            >>> rego.add_data_json(data)
+            >>> output = rego.query("data.one.bar")
+            >>> print(output)
+            "Foo"
+        """
+        err = regoAddDataJSON(self._impl, json)
+        if err == REGO_ERROR:
+            raise RegoError(regoGetError(self._impl))
+
+    def add_data(self, data: Any):
+        """Adds data (i.e. a base document) to the interpreter.
+
+        The data should be provided as single, JSON-encodable Python object.
+
+        Args:
+            data (Any): The data.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        See Also:
+            :func:`~regopy.Interpreter.add_data_json`
+        """
+        self.add_data_json(json.dumps(data))
+
+    def set_input_json(self, json: str):
+        """Sets the input document of the interpreter.
+
+        This can be called several times during the lifetime of the interpreter.
+        The input should be provided as single JSON-encoded value.
+
+        Args:
+            json (str): The input JSON.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        Example:
+            >>> from regopy import Interpreter
+            >>> input = '''
+            ...   {
+            ...     "a": 10,
+            ...     "b": "20",
+            ...     "c": 30.0,
+            ...     "d": true
+            ...   }
+            ... '''
+            >>> rego = Interpreter()
+            >>> rego.set_input_json(input)
+            >>> output = rego.query("input.a")
+            >>> print(output)
+            10
+        """
+        err = regoSetInputJSON(self._impl, json)
+        if err == REGO_ERROR:
+            raise RegoError(regoGetError(self._impl))
+
+    def set_input(self, value: Any):
+        """Sets the input document of the interpreter.
+
+        This can be called several times during the lifetime of the interpreter.
+        The value must be JSON encodable.
+
+        Args:
+            value (Any): The input value.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        See Also:
+            :func:`~regopy.Interpreter.set_input_json`
+        """
+        self.set_input_json(json.dumps(value))
+
+    @property
+    def debug_enabled(self) -> bool:
+        """Whether debug mode is enabled.
+
+        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 may not be as useful for debugging Rego policies.
+        """
+        return regoGetDebugEnabled(self._impl)
+
+    @debug_enabled.setter
+    def debug_enabled(self, value: bool):
+        regoSetDebugEnabled(self._impl, value)
+
+    def set_debug_path(self, path: str):
+        """Sets the path to the directory where this will write debug AST files.
+
+        This is only useful when debug mode is enabled.
+
+        Args:
+            path (str): The path to the directory where this will write debug AST files.
+        """
+        err = regoSetDebugPath(self._impl, path)
+        if err == REGO_ERROR:
+            raise RegoError(regoGetError(self._impl))
+
+    @property
+    def well_formed_checks_enabled(self) -> bool:
+        """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.
+        """
+        return regoGetWellFormedChecksEnabled(self._impl)
+
+    @well_formed_checks_enabled.setter
+    def well_formed_checks_enabled(self, value: bool):
+        regoSetWellFormedChecksEnabled(self._impl, value)
+
+    @property
+    def strict_built_in_errors(self) -> bool:
+        """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.
+        """
+        return regoGetStrictBuiltInErrors(self._impl)
+
+    @strict_built_in_errors.setter
+    def strict_built_in_errors(self, value: bool):
+        regoSetStrictBuiltInErrors(self._impl, value)
+
+    def query(self, query: str) -> Output:
+        """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 :func:`~regopy.Interpreter.add_data` or
+        :func:`~regopy.Interpreter.add_data_json`) and one or more Rego modules
+        (using :func:`~regopy.Interpreter.add_module` or :func:`~regopy.Interpreter.add_module_file`).
+        Then, multiple queries can be performed by providing an input
+        (using :func:`~regopy.Interpreter.set_input` or :func:`~regopy.Interpreter.set_input_json`)
+        and then calling this method.
+
+        Args:
+            query (str): The query.
+
+        Returns:
+            Output: The query result.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        Examples:
+            >>> from regopy import Interpreter
+            >>> input0 = {"a": 10}
+            >>> input1 = {"a": 4}
+            >>> input2 = {"a": 7}
+            >>> multi = '''
+            ...   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
+            ...   }
+            ... '''
+            >>> rego = Interpreter()
+            >>> rego.add_module("multi", multi)
+            >>> rego.set_input(input0)
+            >>> output = rego.query("data.multi.a")
+            >>> print(output)
+            1
+            >>> rego.set_input(input1)
+            >>> output = rego.query("data.multi.a")
+            >>> print(output)
+            41
+            >>> rego.set_input(input2)
+            >>> output = rego.query("data.multi.a")
+            >>> print(output)
+            70
+        """
+        impl = regoQuery(self._impl, query)
+        if impl is None:
+            raise RegoError(regoGetError(self._impl))
+
+        return Output(impl)
diff --git a/wrappers/python/src/regopy/node.py b/wrappers/python/src/regopy/node.py
new file mode 100644
index 00000000..c296dad3
--- /dev/null
+++ b/wrappers/python/src/regopy/node.py
@@ -0,0 +1,275 @@
+"""Module providing an interface to the rego-cpp Node object."""
+
+from enum import IntEnum
+from typing import Union
+
+from ._regopy import (
+    REGO_NODE_ARRAY,
+    REGO_NODE_BINDING,
+    REGO_NODE_ERROR,
+    REGO_NODE_ERROR_AST,
+    REGO_NODE_ERROR_CODE,
+    REGO_NODE_ERROR_MESSAGE,
+    REGO_NODE_ERROR_SEQ,
+    REGO_NODE_FALSE,
+    REGO_NODE_FLOAT,
+    REGO_NODE_INT,
+    REGO_NODE_INTERNAL,
+    REGO_NODE_NULL,
+    REGO_NODE_OBJECT,
+    REGO_NODE_OBJECT_ITEM,
+    REGO_NODE_SCALAR,
+    REGO_NODE_SET,
+    REGO_NODE_STRING,
+    REGO_NODE_TERM,
+    REGO_NODE_TRUE,
+    REGO_NODE_UNDEFINED,
+    REGO_NODE_VAR,
+    regoNodeGet,
+    regoNodeJSON,
+    regoNodeSize,
+    regoNodeType,
+    regoNodeTypeName,
+    regoNodeValue,
+)
+
+
+class NodeKind(IntEnum):
+    """Enumeration of node types."""
+
+    Binding = REGO_NODE_BINDING
+    Var = REGO_NODE_VAR
+    Term = REGO_NODE_TERM
+    Scalar = REGO_NODE_SCALAR
+    Array = REGO_NODE_ARRAY
+    Set = REGO_NODE_SET
+    Object = REGO_NODE_OBJECT
+    ObjectItem = REGO_NODE_OBJECT_ITEM
+    Int = REGO_NODE_INT
+    Float = REGO_NODE_FLOAT
+    String = REGO_NODE_STRING
+    Boolean = REGO_NODE_TRUE
+    Null = REGO_NODE_NULL
+    Undefined = REGO_NODE_UNDEFINED
+    Error = REGO_NODE_ERROR
+    ErrorMessage = REGO_NODE_ERROR_MESSAGE
+    ErrorAst = REGO_NODE_ERROR_AST
+    ErrorCode = REGO_NODE_ERROR_CODE
+    ErrorSeq = REGO_NODE_ERROR_SEQ
+    Internal = REGO_NODE_INTERNAL
+
+
+class Node:
+    """Interface for the Rego Node.
+
+    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:
+
+    >>> from regopy import Interpreter
+    >>> rego = Interpreter()
+    >>> output = rego.query('x={"a": 10, "b": "20", "c": [30.0, 60], "d": true, "e": null}')
+    >>> x = output.binding("x")
+    >>> print("x =", x.json())
+    x = {"a":10, "b":"20", "c":[30, 60], "d":true, "e":null}
+
+    >>> print("x['a'] =", x["a"].value)
+    x['a'] = 10
+
+    >>> print("x['b'] =", x["b"].value)
+    x['b'] = "20"
+
+    >>> print("x['c'][0] =", x["c"][0].value)
+    x['c'][0] = 30.0
+
+    >>> print("x['c'][1] =", x["c"][1].value)
+    x['c'][1] = 60
+
+    >>> print("x['d'] =", x["d"].value)
+    x['d'] = True
+
+    >>> print("x['e'] =", x["e"].value)
+    x['e'] = None
+    """
+
+    ValueKinds = [NodeKind.Int, NodeKind.Float, NodeKind.String, NodeKind.Boolean, NodeKind.Null]
+
+    def __init__(self, impl):
+        """Creates a new node."""
+        self._impl = impl
+        kind = regoNodeType(self._impl)
+        if kind in (REGO_NODE_TRUE, REGO_NODE_FALSE):
+            self._kind = NodeKind.Boolean
+        else:
+            self._kind = NodeKind(regoNodeType(self._impl))
+
+        self._children = [Node(regoNodeGet(self._impl, i)) for i in range(len(self))]
+
+    @property
+    def kind(self) -> NodeKind:
+        """Returns the node type."""
+        return self._kind
+
+    @property
+    def kind_name(self) -> str:
+        """Returns a human-readable string representation of the node kind."""
+        return regoNodeTypeName(self._impl)
+
+    @property
+    def value(self) -> Union[str, int, float, bool, None]:
+        """Returns the node value.
+
+        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 raise a RegoError.
+
+        Returns:
+            Union[str, int, float, bool, None]: The node value.
+
+        Raises:
+            RegoError: If the node does not have a singular value.
+
+        Examples:
+            >>> from regopy import Interpreter
+            >>> rego = Interpreter()
+            >>> output = rego.query('x=10; y="20"; z=true')
+            >>> x = output.binding("x")
+            >>> print("x =", x.value)
+            x = 10
+
+            >>> y = output.binding("y")
+            >>> print("y =", y.value)
+            y = "20"
+
+            >>> z = output.binding("z")
+            >>> print("z =", z.value)
+            z = True
+        """
+        if self._kind == NodeKind.Term or self._kind == NodeKind.Scalar:
+            return self.at(0).value
+
+        if self._kind not in Node.ValueKinds:
+            raise ValueError("Node does not have a value")
+
+        if hasattr(self, "_value"):
+            return self._value
+
+        value = regoNodeValue(self._impl)
+        if self._kind == NodeKind.Int:
+            self._value = int(value)
+        elif self._kind == NodeKind.Float:
+            self._value = float(value)
+        elif self._kind == NodeKind.Boolean:
+            self._value = value == "true"
+        elif self._kind == NodeKind.Null:
+            self._value = None
+        elif self._kind == NodeKind.String:
+            self._value = value
+        else:
+            raise ValueError("Node does not have a value")
+
+        return self._value
+
+    def __len__(self) -> int:
+        """Returns the number of child nodes."""
+        return regoNodeSize(self._impl)
+
+    def index(self, index: int) -> "Node":
+        """Returns the node at an index of an array.
+        
+        Returns:
+            Node: The child node.
+        
+        Raises:
+            IndexError: If the index is out of bounds.
+            TypeError: If the node is not an array.
+        """
+        if self._kind == NodeKind.Term:
+            return self.at(0).index(index)
+
+        if self._kind == NodeKind.Array:
+            if index < 0 or index >= len(self):
+                raise IndexError("index out of bounds")
+
+            return self.at(index)
+
+        raise TypeError("index is only valid for Array nodes")
+
+    def lookup(self, key: str) -> "Node":
+        """Returns the child node for the given key.
+
+        If this is of kind Object or Set, then the key must be a string.
+
+        Returns:
+            Node: The child node.
+        
+        Raises:
+            KeyError: If the key is not found.
+            TypeError: If the node does not support lookup
+        """
+        if self._kind == NodeKind.Term:
+            return self.at(0).lookup(key)
+
+        if self._kind == NodeKind.Object:
+            for child in self._children:
+                assert child.kind == NodeKind.ObjectItem
+                child_key = child.at(0).value
+                if child_key == key:
+                    return child.at(1)
+
+                if child_key.startswith('"') and child_key.endswith('"'):
+                    child_key = child_key[1:-1]
+
+                if child_key == key:
+                    return child.at(1)
+
+            raise KeyError(f"Key {key} not found")
+
+        if self._kind == NodeKind.Set:
+            for child in self._children:
+                if child.value == key:
+                    return child
+
+            return None
+
+        raise TypeError("lookup is only valid for OBJECT and SET nodes")
+
+    def at(self, index: int) -> "Node":
+        """Returns the child node at the given index."""
+        return self._children[index]
+
+    def __iter__(self):
+        """Returns an iterator over the child nodes."""
+        return iter(self._children)
+
+    def __getitem__(self, key: Union[int, str]) -> "Node":
+        """Returns the child node for the given key.
+
+        If this is of kind OBJECT or SET, then the key must be a string.
+        Otherwise, the key must be an integer. If the key is out of bounds,
+        then this will raise a RegoError.
+
+        Returns:
+            Node: The child node.
+
+        Raises:
+            RegoError: If the key is out of bounds.
+            TypeError: If the key is not an integer or a string.
+        """
+        if isinstance(key, int):
+            return self.index(key)
+
+        if isinstance(key, str):
+            return self.lookup(key)
+
+        raise TypeError("key must be an integer or a string")
+
+    def json(self) -> str:
+        """Returns the node as a JSON string."""
+        return regoNodeJSON(self._impl)
+
+    def __str__(self) -> str:
+        """Returns the node as a JSON string."""
+        return self.json()
diff --git a/wrappers/python/src/regopy/output.py b/wrappers/python/src/regopy/output.py
new file mode 100644
index 00000000..25866a56
--- /dev/null
+++ b/wrappers/python/src/regopy/output.py
@@ -0,0 +1,84 @@
+"""Module providing an interface to a rego-cpp output object."""
+
+from ._regopy import (
+    regoFreeOutput,
+    regoOutputBinding,
+    regoOutputNode,
+    regoOutputOk,
+    regoOutputString,
+)
+from .node import Node
+
+
+class Output:
+    """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:
+        >>> from regopy import Interpreter
+        >>> rego = Interpreter()
+        >>> output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4")
+        >>> print(output)
+        x = 5
+        y = 9.4
+
+        >>> x = output.binding("x")
+        >>> print(x.json())
+        5
+    """
+
+    def __init__(self, impl):
+        """Initializer.
+
+        The only way to create an output is to query a rego interpreter.
+        As such, this initializer should not be called directly.
+        """
+        self._impl = impl
+
+    def __del__(self):
+        """Destructor."""
+        regoFreeOutput(self._impl)
+
+    def __str__(self) -> str:
+        """Returns the output as a human readable string.
+
+        If the result of :func:`~regopy.Output.ok` is false, the result will be an string
+        containing error information.
+
+        Returns:
+            str: A string representation of the output.
+        """
+        return regoOutputString(self._impl)
+
+    def node(self) -> Node:
+        """Returns the root node of the output."""
+        return Node(regoOutputNode(self._impl))
+
+    def binding(self, name: str) -> Node:
+        """Attempts to return the binding for the given variable name.
+
+        Returns:
+            Node: The binding for the given variable name.
+
+        Raises:
+            ValueError: If the variable is not bound.
+        """
+        impl = regoOutputBinding(self._impl, name)
+        if impl:
+            return Node(impl)
+
+        raise ValueError(f"Variable {name} is not bound.")
+
+    def ok(self) -> bool:
+        """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.
+        """
+        regoOutputOk(self._impl)
diff --git a/wrappers/rust/regorust/Cargo.toml b/wrappers/rust/regorust/Cargo.toml
index b7560a4e..3abf02f2 100644
--- a/wrappers/rust/regorust/Cargo.toml
+++ b/wrappers/rust/regorust/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "regorust"
-version = "0.3.5"
+version = "0.3.6"
 edition = "2021"
 description = "Rust bindings for the rego-cpp Rego compiler and interpreter"
 license = "MIT"
diff --git a/wrappers/rust/regorust/src/lib.rs b/wrappers/rust/regorust/src/lib.rs
index cbb9eec6..350a92c1 100644
--- a/wrappers/rust/regorust/src/lib.rs
+++ b/wrappers/rust/regorust/src/lib.rs
@@ -378,17 +378,6 @@ impl Interpreter {
 
     /// 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::*;
@@ -458,24 +447,6 @@ impl Interpreter {
     ///
     /// 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::*;


Test patch:
diff --git a/tests/test_case.cc b/tests/test_case.cc
index 1252e0ad..60d4b5b9 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -211,7 +211,7 @@ namespace rego_test
 
       std::string key = std::string((binding / rego::Var)->location().view());
       std::string value =
-        rego::to_json((binding / rego::Term), m_sort_bindings, true);
+        rego::to_json((binding / rego::Term), true, m_sort_bindings);
       bindings[key] = value;
     }
 
diff --git a/wrappers/python/test/test_interpreter.py b/wrappers/python/test/test_interpreter.py
new file mode 100644
index 00000000..726a7c8a
--- /dev/null
+++ b/wrappers/python/test/test_interpreter.py
@@ -0,0 +1,121 @@
+"""Tests for the Python wrapper of the Rego interpreter."""
+
+from regopy import Interpreter
+
+
+def test_query_math():
+    rego = Interpreter()
+    output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4")
+    assert output is not None
+    assert str(output) == "x = 5\ny = 9.4"
+    assert output.binding("x").json() == "5"
+    assert output.binding("y").json() == "9.4"
+
+
+def test_input_data():
+    input0 = {
+        "a": 10,
+        "b": "20",
+        "c": 30.0,
+        "d": True
+    }
+    data0 = {
+        "one": {
+            "bar": "Foo",
+            "baz": 5,
+            "be": True,
+            "bop": 23.4
+        },
+        "two": {
+            "bar": "Bar",
+            "baz": 12.3,
+            "be": False,
+            "bop": 42
+        }
+    }
+    data1 = {
+        "three": {
+            "bar": "Baz",
+            "baz": 15,
+            "be": True,
+            "bop": 4.23
+        }
+    }
+    module = """
+    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"]
+    """
+    rego = Interpreter()
+    rego.set_input(input0)
+    rego.add_data(data0)
+    rego.add_data(data1)
+    rego.add_module("objects", module)
+    output = rego.query("x=[data.one, input.b, data.objects.sites[1]]")
+    assert output is not None
+    x = output.binding("x")
+    data_one = x[0]
+    assert data_one["bar"].value == '"Foo"'
+    assert data_one["be"].value is True
+    assert data_one["baz"].value == 5
+    assert data_one["bop"].value == 23.4
+    assert x[1].value == '"20"'
+    data_objects_sites_1 = x[2]
+    assert data_objects_sites_1["name"].value == '"smoke1"'
+
+
+def test_multiple_inputs():
+    input0 = {"a": 10}
+    input1 = {"a": 4}
+    input2 = {"a": 7}
+    module = """
+    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
+    }
+    """
+    rego = Interpreter()
+    rego.add_module("multi", module)
+    rego.set_input(input0)
+    output = rego.query("x = data.multi.a")
+    assert str(output) == "x = 1"
+    assert output.binding("x").value == 1
+    rego.set_input(input1)
+    output = rego.query("x = data.multi.a")
+    assert output is not None
+    assert output.binding("x").value == 41
+    rego.set_input(input2)
+    output = rego.query("x = data.multi.a")
+    assert output is not None
+    assert output.binding("x").value == 70
diff --git a/wrappers/python/test/test_node.py b/wrappers/python/test/test_node.py
new file mode 100644
index 00000000..dc1a81a3
--- /dev/null
+++ b/wrappers/python/test/test_node.py
@@ -0,0 +1,19 @@
+"""Tests for the Python wrapper for Rego Nodes."""
+
+from regopy import Interpreter
+
+
+def test_node_access():
+    rego = Interpreter()
+    result = rego.query('x={"a": 10, "b": "20", "c": [30.0, 60], "d": true, "e": null}')
+    assert result is not None
+    x = result.binding("x")
+    assert x is not None
+    assert x.json() == '{"a":10, "b":"20", "c":[30, 60], "d":true, "e":null}'
+
+    assert x["a"].value == 10
+    assert x["b"].value == '"20"'
+    assert x["c"][0].value == 30.0
+    assert x["c"][1].value == 60
+    assert x["d"].value
+    assert x["e"].value is None