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

Issue:
Richer output from `rego` tool
At the moment, the query `rego "5; x=3"` results in the following output:

    rego 0.4.0 (main:9cb7b03, Tue, 4 Jun 2024 02:05:01 +0100)[Clang 14.0.0] on linux
    5
    x = 3

It would be nice to have this be a JSON object (i.e. structured) and to follow the style of output that comes from the `opa eval "5; x=3"`:

```json
{
  "result": [
    {
      "expressions": [
        {
          "value": 5,
          "text": "5",
          "location": {
            "row": 1,
            "col": 1
          }
        },
        {
          "value": true,
          "text": "x=3",
          "location": {
            "row": 1,
            "col": 4
          }
        }
      ],
      "bindings": {
        "x": 3
      }
    }
  ]
}
```

That is to say:
- An expression object that lists the resulting term values for each literal in the query, in order
- An (optional) bindings object which has a lookup table for the bindings if there were any

One suggested implementation path would be:
1. Update the `result()` pass (and its accompanying `wf_result` WF definition to contain `Result <<= Expressions * Bindings`, `Expressions <<= Term++[1]` and `Bindings << Binding++`.
2. Update the `to_json()` rewriter to handle `Result` nodes and product the JSON object format above as closely as we can at this point

Common prefix of two rules is not evaluated
```
package test
a.b.c = 1
a.b.d = 2
y = a.b
```
does not produce any value for data.test.y

Regorus also does not produce correct output for this example if the order of the rules are changed: https://github.com/microsoft/regorus/issues/162


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
index 6ff806c3..8d62e742 100644
--- a/.github/workflows/build_wheels.yml
+++ b/.github/workflows/build_wheels.yml
@@ -23,8 +23,8 @@ jobs:
         env:
           CIBW_BUILD: ${{ matrix.python }}-win*
           CIBW_ENVIRONMENT: >
-            REGOCPP_C_WRAPPER_REPO=https://github.com/${{github.repository}}
-            REGOCPP_C_WRAPPER_TAG=${{github.sha}}
+            REGOCPP_REPO=https://github.com/${{github.repository}}
+            REGOCPP_TAG=${{github.sha}}
         with:
           package-dir: ${{github.workspace}}/wrappers/python/
 
@@ -55,8 +55,8 @@ jobs:
             MACOSX_DEPLOYMENT_TARGET=10.15
             CC=gcc-11
             CXX=g++-11
-            REGOCPP_C_WRAPPER_REPO=https://github.com/${{github.repository}}
-            REGOCPP_C_WRAPPER_TAG=${{github.sha}}
+            REGOCPP_REPO=https://github.com/${{github.repository}}
+            REGOCPP_TAG=${{github.sha}}
         with:
           package-dir: ${{github.workspace}}/wrappers/python/
 
@@ -100,8 +100,8 @@ jobs:
           CIBW_ARCHS_MACOS: arm64
           CIBW_ENVIRONMENT_MACOS : >
             MACOSX_DEPLOYMENT_TARGET=10.15
-            REGOCPP_C_WRAPPER_REPO=https://github.com/${{github.repository}}
-            REGOCPP_C_WRAPPER_TAG=${{github.sha}}
+            REGOCPP_REPO=https://github.com/${{github.repository}}
+            REGOCPP_TAG=${{github.sha}}
         with:
           package-dir: ${{github.workspace}}/wrappers/python/
 
@@ -129,8 +129,8 @@ jobs:
           CIBW_BUILD: ${{ matrix.python }}-manylinux*
           CIBW_BEFORE_ALL: yum makecache && yum -y install devtoolset-10-libatomic-devel
           CIBW_ENVIRONMENT: >
-            REGOCPP_C_WRAPPER_REPO=https://github.com/${{github.repository}}
-            REGOCPP_C_WRAPPER_TAG=${{github.sha}}
+            REGOCPP_REPO=https://github.com/${{github.repository}}
+            REGOCPP_TAG=${{github.sha}}
         with:
           package-dir: ${{github.workspace}}/wrappers/python/
 
@@ -157,8 +157,8 @@ jobs:
         env:
           CIBW_BUILD: ${{ matrix.python }}-musllinux*
           CIBW_ENVIRONMENT: >
-            REGOCPP_C_WRAPPER_REPO=https://github.com/${{github.repository}}
-            REGOCPP_C_WRAPPER_TAG=${{github.sha}}
+            REGOCPP_REPO=https://github.com/${{github.repository}}
+            REGOCPP_TAG=${{github.sha}}
         with:
           package-dir: ${{github.workspace}}/wrappers/python/
 
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index 41940a88..7c1f50c9 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -99,8 +99,8 @@ jobs:
         working-directory: ${{github.workspace}}/examples/c
         run: cmake -B ${{github.workspace}}/examples/c/build --preset release-clang
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
       - name: CMake build
         working-directory: ${{github.workspace}}/examples/c/build
@@ -129,8 +129,8 @@ jobs:
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
         run: cargo test -vv
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
   linux-wrappers-python:
     runs-on: ubuntu-latest
@@ -158,8 +158,8 @@ jobs:
         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}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
       - name: Python test
         working-directory: ${{github.workspace}}/wrappers/python/ 
@@ -199,8 +199,8 @@ jobs:
         working-directory: ${{github.workspace}}/examples/c
         run: cmake -B ${{github.workspace}}/examples/c/build --preset release
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
       - name: CMake build
         working-directory: ${{github.workspace}}/examples/c/build
         run: cmake --build . --config Release --target INSTALL
@@ -224,8 +224,8 @@ jobs:
         working-directory: ${{github.workspace}}/wrappers/rust/regorust
         run: cargo test -vv
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
   windows-wrappers-python:
     runs-on: ubuntu-latest
@@ -254,8 +254,8 @@ jobs:
         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}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
       - name: Python test
         working-directory: ${{github.workspace}}/wrappers/python/ 
@@ -302,8 +302,8 @@ jobs:
         working-directory: ${{github.workspace}}/examples/c
         run: cmake -B ${{github.workspace}}/examples/c/build --preset release-clang
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
       - name: CMake build
         working-directory: ${{github.workspace}}/examples/c/build
@@ -332,8 +332,8 @@ jobs:
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
         run: cargo test -vv
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
   macos-wrappers-python:
     runs-on: macos-latest
@@ -361,8 +361,8 @@ jobs:
         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}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
           CXX: g++-11
           CC: gcc-11
 
@@ -411,8 +411,8 @@ jobs:
         working-directory: ${{github.workspace}}/examples/c
         run: cmake -B ${{github.workspace}}/examples/c/build --preset release-clang
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
       - name: CMake build
         working-directory: ${{github.workspace}}/examples/c/build
@@ -441,8 +441,8 @@ jobs:
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
         run: cargo test -vv
         env:
-          REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
-          REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
 
   applesilicon-wrappers-python:
     runs-on: macos-latest-xlarge
@@ -470,8 +470,8 @@ jobs:
         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}}
+          REGOCPP_REPO: https://github.com/${{github.repository}}
+          REGOCPP_TAG: ${{github.sha}}
           CXX: g++-11
           CC: gcc-11
 
diff --git a/CHANGELOG b/CHANGELOG
index f185e804..c7388483 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,25 @@
 # Changelog
 
+## 2024-06-12 - Update to rego v0.65.0
+Update to Rego v0.65.0
+
+**New Features**
+- Updated the supported version of Rego to v0.65.0.
+- `json.marshal_with_options`
+- `numbers.range_step`
+- `rego.v1` import and behaviour, plus a set of tests in `regov1.yaml`
+- The interpreter now returns more than one `Result` when appropriate, and separates out the `Bindings` and `Terms`.
+- The output of `rego` is now JSON instead of plain text
+- Added `regoOutputSize`, `regoExpressions`, `regoBindingsAtIndex`, and `regoExpressionsAtIndex` to the C API
+- Updated the Python and Rust wrappers to use the new API
+
+**Breaking Changes**
+- Output of `rego` tool has changed. Any downstream tooling that was parsing that will need to be updated to parse the JSON.
+- `rego::wf_result` has changed. Any tooling that was using the output node directly will need to be updated
+- the `add_*` methods on `Interpreter`, as they now perform parsing, now produce either a `nullptr` if successful or an error `Node`.
+  Downstream consumers that were checking the boolean return value will need to be updated. Behavior in the C API is unchanged, but
+  there will now be detailed parse/compile errors when an individual module etc. fails to compile.
+
 ## 2024-06-04 - Version 0.4.0
 Major refactor of the code to use the new version of Trieste.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 84d4fe50..78d2a152 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,7 +15,7 @@ list( GET REGOCPP_VERSION_LIST 1 REGOCPP_VERSION_MINOR )
 
 list( GET REGOCPP_VERSION_LIST 2 REGOCPP_VERSION_REVISION )
 
-set ( REGOCPP_OPA_VERSION 0.55.0 )
+set ( REGOCPP_OPA_VERSION 0.65.0 )
 
 set( REGOCPP_VERSION ${REGOCPP_VERSION_MAJOR}.${REGOCPP_VERSION_MINOR}.${REGOCPP_VERSION_REVISION} )
 
@@ -68,6 +68,7 @@ option(REGOCPP_SPECULATIVE_LOAD_HARDENING "Specifies whether to enable speculati
 option(REGOCPP_COPY_EXAMPLES "Specifies whether to copy the examples to the install directory" OFF)
 option(REGOCPP_USE_CXX17 "Specifies whether to use C++17" OFF)
 option(REGOCPP_ACTION_METRICS "Specifies whether to metricate Trieste Actions" OFF)
+option(REGOCPP_CLEAN_INSTALL "Whether the install directory should be cleaned before install" OFF)
 set(REGOCPP_SANITIZE "" CACHE STRING "Argument to pass to sanitize (disabled by default)")
 
 if(REGOCPP_USE_CXX17)
@@ -102,7 +103,6 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
 set(TRIESTE_USE_CXX17 ${REGOCPP_USE_CXX17})
 set(TRIESTE_BUILD_SAMPLES OFF)
 set(TRIESTE_BUILD_PARSERS ON)
-set(TRIESTE_CLEAN_INSTALL ON)
 
 if (REGOCPP_SPECULATIVE_LOAD_HARDENING)
   if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
@@ -153,7 +153,7 @@ endif()
 
 if(REGOCPP_OPA_TESTS)
   if(NOT IS_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/opa)
-    execute_process(COMMAND ${GIT_EXECUTABLE} clone --depth=1 --branch v0.55.0 https://github.com/open-policy-agent/opa/ 
+    execute_process(COMMAND ${GIT_EXECUTABLE} clone --depth=1 --branch v${REGOCPP_OPA_VERSION} https://github.com/open-policy-agent/opa/ 
                     WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                     OUTPUT_QUIET)
   endif()
@@ -187,6 +187,15 @@ endif()
 
 # -------------------- Install ------------------------------------
 
+if(REGOCPP_CLEAN_INSTALL)
+  message("${CMAKE_INSTALL_PREFIX} will be recursively cleaned before install")
+  # Clear all existing files and folders from the install directory
+  install(CODE [[
+    file(REMOVE_RECURSE ${CMAKE_INSTALL_PREFIX}/.)
+    ]])
+endif()
+
+
 set(INSTALL_CONFIGDIR cmake)
 set(INSTALL_LIBDIR lib)
 set(INSTALL_INCLUDEDIR include)
@@ -231,7 +240,7 @@ endif()
 
 if (REGOCPP_BUILD_TESTS)
   install(TARGETS rego_test rego_test_c_api RUNTIME)
-  install(FILES tests/regocpp.yaml tests/bigint.yaml tests/bugs.yaml DESTINATION tests)
+  install(FILES tests/regocpp.yaml tests/bigint.yaml tests/bugs.yaml tests/regov1.yaml DESTINATION tests)
   install(DIRECTORY tests/aci DESTINATION tests)
 endif()
 
diff --git a/README.md b/README.md
index 5893462d..7593a406 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ library from different langauages.
 
 ## Language Support
 
-At present we support v0.55.0 of Rego as defined by OPA, with the following grammar:
+At present we support v0.65.0 of Rego as defined by OPA, with the following grammar:
 
 ```ebnf
 module          = package { import } policy
@@ -108,7 +108,7 @@ set-compr       = "{" term "|" query "}"
 object-compr    = "{" object-item "|" query "}"
 infix-operator  = assign-operator | bool-operator | arith-operator | bin-operator
 bool-operator   = "==" | "!=" | "<" | ">" | ">=" | "<="
-arith-operator  = "+" | "-" | "*" | "/"
+arith-operator  = "+" | "-" | "*" | "/" | "%"
 bin-operator    = "&" | "|"
 assign-operator = ":=" | "="
 ref             = ( var | array | object | set | array-compr | object-compr | set-compr | expr-call ) { ref-arg }
@@ -127,6 +127,9 @@ non-empty-set   = "{" term { "," term } "}"
 empty-set       = "set(" ")"
 ```
 
+> [!NOTE]
+> This grammar corresponds to Rego with `rego.v1` enabled (See [OPA v1.0](https://www.openpolicyagent.org/docs/latest/opa-1) for more info).
+
 Definitions:
 ```
 []     optional (zero or one instances)
diff --git a/examples/c/CMakeLists.txt b/examples/c/CMakeLists.txt
index 6844830e..3e269271 100644
--- a/examples/c/CMakeLists.txt
+++ b/examples/c/CMakeLists.txt
@@ -9,22 +9,22 @@ project( regoc 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})
+if (DEFINED ENV{REGOCPP_REPO})
+  set(REGOCPP_REPO $ENV{REGOCPP_REPO})
 else ()
-  set(REGOCPP_C_WRAPPER_REPO "https://github.com/microsoft/rego-cpp/")
+  set(REGOCPP_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})
+if (DEFINED ENV{REGOCPP_TAG})
+  set(REGOCPP_TAG $ENV{REGOCPP_TAG})
 else ()
-  set(REGOCPP_C_WRAPPER_TAG "main")
+  set(REGOCPP_TAG "main")
 endif()
 
 FetchContent_Declare(
   regocpp
-  GIT_REPOSITORY ${REGOCPP_C_WRAPPER_REPO}
-  GIT_TAG        ${REGOCPP_C_WRAPPER_TAG}
+  GIT_REPOSITORY ${REGOCPP_REPO}
+  GIT_TAG        ${REGOCPP_TAG}
 )
 
 FetchContent_Declare(
diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt
index c72f2e57..e0729ab2 100644
--- a/examples/cpp/CMakeLists.txt
+++ b/examples/cpp/CMakeLists.txt
@@ -9,22 +9,22 @@ project( regoc 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})
+if (DEFINED ENV{REGOCPP_REPO})
+  set(REGOCPP_REPO $ENV{REGOCPP_REPO})
 else ()
-  set(REGOCPP_C_WRAPPER_REPO "https://github.com/microsoft/rego-cpp/")
+  set(REGOCPP_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})
+if (DEFINED ENV{REGOCPP_TAG})
+  set(REGOCPP_TAG $ENV{REGOCPP_TAG})
 else ()
-  set(REGOCPP_C_WRAPPER_TAG "main")
+  set(REGOCPP_TAG "main")
 endif()
 
 FetchContent_Declare(
   regocpp
-  GIT_REPOSITORY ${REGOCPP_C_WRAPPER_REPO}
-  GIT_TAG        ${REGOCPP_C_WRAPPER_TAG}
+  GIT_REPOSITORY ${REGOCPP_REPO}
+  GIT_TAG        ${REGOCPP_TAG}
 )
 
 set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 
diff --git a/include/rego/rego.hh b/include/rego/rego.hh
index 1b5f0104..3e517fa4 100644
--- a/include/rego/rego.hh
+++ b/include/rego/rego.hh
@@ -93,6 +93,7 @@ namespace rego
   inline const auto Import =
     TokenDef("rego-import", flag::lookdown | flag::lookup | flag::shadowing);
   inline const auto Placeholder = TokenDef("rego-placeholder");
+  inline const auto Version = TokenDef("rego-version", flag::print);
 
   // intermediate tokens
   inline const auto RuleBodySeq = TokenDef("rego-rulebodyseq");
@@ -110,7 +111,10 @@ namespace rego
   inline const auto Undefined = TokenDef("rego-undefined");
 
   // other tokens
+  inline const auto Results = TokenDef("rego-results", flag::symtab);
   inline const auto Result = TokenDef("rego-result", flag::symtab);
+  inline const auto Bindings = TokenDef("rego-bindings");
+  inline const auto Terms = TokenDef("rego-terms");
   inline const auto Binding = TokenDef("rego-binding", flag::lookdown);
   inline const auto ErrorCode = TokenDef("rego-errorcode");
   inline const auto ErrorSeq = TokenDef("rego-errorseq");
@@ -126,7 +130,7 @@ namespace rego
   // clang-format off
   inline const auto wf =
       (Top <<= Query | Module)
-    | (Module <<= Package * ImportSeq * Policy)
+    | (Module <<= Package * Version * ImportSeq * Policy)
     | (ImportSeq <<= Import++)
     | (Import <<= Ref * Var)
     | (Package <<= Ref)
@@ -141,7 +145,7 @@ namespace rego
     | (RuleArgs <<= Term++)
     | (RuleBodySeq <<= (Else | Query)++)
     | (Else <<= Expr * Query)
-    | (Query <<= Literal++[1])
+    | (Query <<= Literal++)
     | (Literal <<= (Expr >>= SomeDecl | Expr | NotExpr) * WithSeq)
     | (WithSeq <<= With++)
     | (With <<= Term * Expr)
@@ -168,7 +172,7 @@ namespace rego
     | (Ref <<= RefHead * RefArgSeq)
     | (RefHead <<= Var | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr | ExprCall)
     | (RefArgSeq <<= (RefArgDot | RefArgBrack)++)
-    | (RefArgBrack <<= ExprInfix | Scalar | Var | Array | Object | Set | Placeholder)
+    | (RefArgBrack <<= Expr | Placeholder)
     | (RefArgDot <<= Var)
     | (Scalar <<= String | Int | Float | True | False | Null)
     | (String <<= JSONString | RawString)
@@ -181,9 +185,12 @@ namespace rego
 
   // clang-format off
   inline const auto wf_result =
-    (Top <<= Result)
-    | (Result <<= (Binding | Term)++)
-    | (Binding <<= Var * Term)[Var]
+    (Top <<= Results | Undefined)
+    | (Results <<= Result++[1])
+    | (Result <<= Terms * Bindings)
+    | (Terms <<= Term++)
+    | (Bindings <<= Binding++)
+    | (Binding <<= (Key >>= Var) * (Val >>= Term))[Key]
     | (Term <<= Scalar | Array | Object | Set)
     | (Array <<= Term++)
     | (Set <<= Term++)
@@ -401,6 +408,15 @@ namespace rego
    */
   Node unwrap_arg(const Nodes& args, const UnwrapOpt& options);
 
+  /**
+   * Attempts to unwrap an argument to a specified type.
+   *
+   * @param term The term to unwrap.
+   * @param type The acceptable type.
+   * @return An unwrap result.
+   */
+  UnwrapResult unwrap(const Node& term, const Token& type);
+
   /**
    * Attempts to unwrap an argument to one of the specified types.
    *
@@ -454,6 +470,15 @@ namespace rego
    */
   bool get_bool(const Node& node);
 
+  /**
+   * Tries to get an item within an object using its key.
+   *
+   * @param node The object to search.
+   * @param key The key to search for.
+   */
+  std::optional<Node> try_get_item(
+    const Node& node, const std::string_view& key);
+
   /**
    * Converts the value to a scalar node.
    *
@@ -630,14 +655,25 @@ namespace rego
      */
     bool is_builtin(const Location& name) const;
 
+    /**
+     * Determines whether the provided builtin name is deprecated in the
+     * provided version.
+     *
+     * @param version The version to check.
+     * @param name The name to check.
+     */
+    bool is_deprecated(const Location& version, const Location& name) const;
+
     /**
      * Calls the built-in with the provided name and arguments.
      *
      * @param name The name of the built-in to call.
      * @param args The arguments to pass to the built-in.
+     * @param version The Rego version.
      * @return The result of the built-in call.
      */
-    Node call(const Location& name, const Nodes& args) const;
+    Node call(
+      const Location& name, const Location& version, const Nodes& args) const;
 
     /**
      * Registers a built-in.
@@ -707,9 +743,12 @@ namespace rego
   const std::string EvalTypeError = "eval_type_error";
   const std::string EvalBuiltInError = "eval_builtin_error";
   const std::string RegoTypeError = "rego_type_error";
+  const std::string RegoParseError = "rego_parse_error";
+  const std::string RegoCompileError = "rego_compile_error";
   const std::string EvalConflictError = "eval_conflict_error";
   const std::string WellFormedError = "wellformed_error";
   const std::string RuntimeError = "runtime_error";
+  const std::string DefaultVersion = "v0";
 
   /**
    * Generates an error node.
@@ -816,8 +855,9 @@ namespace rego
      * the file.
      *
      * @param path The path to the module file.
+     * @returns either an error node or a nullptr if the module is valid.
      */
-    bool add_module_file(const std::filesystem::path& path);
+    Node add_module_file(const std::filesystem::path& path);
 
     /**
      * Adds a module (i.e. virtual document) to the interpreter.
@@ -826,8 +866,9 @@ namespace rego
      *
      * @param name The name of the module.
      * @param contents The contents of the module.
+     * @returns either an error node or a nullptr if the module is valid.
      */
-    bool add_module(const std::string& name, const std::string& contents);
+    Node add_module(const std::string& name, const std::string& contents);
 
     /**
      * Adds a base document to the interpreter.
@@ -836,8 +877,9 @@ namespace rego
      * of the file.
      *
      * @param module The module to add.
+     * @returns either an error node or a nullptr if the JSON is valid.
      */
-    bool add_data_json_file(const std::filesystem::path& path);
+    Node add_data_json_file(const std::filesystem::path& path);
 
     /**
      * Adds a base document to the interpreter.
@@ -846,8 +888,9 @@ namespace rego
      * parsed and added to the interpreter's data sequence.
      *
      * @param json The contents of the document.
+     * @returns either an error node or a nullptr if the JSON is valid.
      */
-    bool add_data_json(const std::string& json);
+    Node add_data_json(const std::string& json);
 
     /**
      * Adds a base document to the interpreter.
@@ -857,7 +900,7 @@ namespace rego
      *
      * @param node The contents of the document.
      */
-    bool add_data(const Node& node);
+    Node add_data(const Node& node);
 
     /**
      * Sets the input document to the interpreter.
@@ -866,8 +909,10 @@ namespace rego
      * of the file.
      *
      * @param path The path to the input file.
+     * @returns either an error node or a nullptr if the input document is
+     * valid.
      */
-    bool set_input_json_file(const std::filesystem::path& path);
+    Node set_input_json_file(const std::filesystem::path& path);
 
     /**
      * Sets the input term of the interpreter.
@@ -875,8 +920,9 @@ namespace rego
      * The string must contain a single valid Rego data term.
      *
      * @param term The contents of the term.
+     * @returns either an error node or a nullptr if the input term is valid.
      */
-    bool set_input_term(const std::string& term);
+    Node set_input_term(const std::string& term);
 
     /**
      * Sets the input document to the interpreter.
@@ -886,7 +932,7 @@ namespace rego
      *
      * @param node The contents of the document.
      */
-    bool set_input(const Node& node);
+    Node set_input(const Node& node);
 
     /**
      * Executes a query against the interpreter.
@@ -945,7 +991,7 @@ namespace rego
      * This object can be used to register custom built-ins created using
      * BuiltInDef::create.
      */
-    BuiltIns builtins() const;
+    BuiltInsDef& builtins() const;
 
   private:
     friend const char* ::regoGetError(regoInterpreter* rego);
diff --git a/include/rego/rego_c.h b/include/rego/rego_c.h
index e6035150..1b10ab4c 100644
--- a/include/rego/rego_c.h
+++ b/include/rego/rego_c.h
@@ -37,6 +37,10 @@ typedef unsigned int regoSize;
 #define REGO_NODE_FALSE 1012
 #define REGO_NODE_NULL 1013
 #define REGO_NODE_UNDEFINED 1014
+#define REGO_NODE_TERMS 1015
+#define REGO_NODE_BINDINGS 1016
+#define REGO_NODE_RESULTS 1017
+#define REGO_NODE_RESULT 1018
 
 #define REGO_NODE_ERROR 1800
 #define REGO_NODE_ERROR_MESSAGE 1801
@@ -331,11 +335,38 @@ extern "C"
    */
   regoBoolean regoOutputOk(regoOutput* output);
 
+  /**
+   * Returns the number of results in the output.
+   *
+   * Each query can potentially generate multiple results. This function
+   * returns the number of results in the output.
+   */
+  regoSize regoOutputSize(regoOutput* output);
+
+  /**
+   * Returns a node containing a list of terms resulting from the query at
+   * the specified index.
+   *
+   * @param output The output.
+   * @param index The result index.
+   * @return The output node.
+   */
+  regoNode* regoOutputExpressionsAtIndex(regoOutput* output, regoSize index);
+
+  /**
+   * Returns a node containing a list of terms resulting from the query
+   * at the default index.
+   *
+   * @param output The output.
+   * @return The output node.
+   */
+  regoNode* regoOutputExpressions(regoOutput* output);
+
   /**
    * Returns the node containing the output of the query.
    *
-   * This will either be a node which contains sequence of terms and/or
-   * bindings, or an error sequence.
+   * This will either be a node which contains one or more results,
+   * or an error sequence.
    *
    * @param output The output.
    * @return The output node.
@@ -348,6 +379,19 @@ extern "C"
    * If the variable is not bound, then this function will return NULL.
    *
    * @param output The output.
+   * @param index The result index.
+   * @param name The variable name.
+   * @return The bound value (or NULL if the variable was not bound)
+   */
+  regoNode* regoOutputBindingAtIndex(
+    regoOutput* output, regoSize index, const char* name);
+
+  /**
+   * Returns the bound value for a given variable name at the first index.
+   *
+   * If the variable is not bound, then this function will return NULL.
+   *
+   * @param output The output.
    * @param name The variable name.
    * @return The bound value (or NULL if the variable was not bound)
    */
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e39bcb22..ab794dde 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -21,6 +21,7 @@ reader.cc
 unify.cc
 unify/strings.cc
 unify/merge_data.cc
+unify/varrefheads.cc
 unify/lift_refheads.cc
 unify/symbols.cc
 unify/replace_argvals.cc
diff --git a/src/builtins.cc b/src/builtins.cc
index 0bded41f..b2c3d94b 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -62,13 +62,50 @@ namespace rego
     return contains(m_builtins, name);
   }
 
-  Node BuiltInsDef::call(const Location& name, const Nodes& args) const
+  bool BuiltInsDef::is_deprecated(
+    const Location& version, const Location& name) const
+  {
+    if (version.view() == "v0")
+    {
+      return false;
+    }
+
+    std::vector<std::string> deprecated = {
+      "any",
+      "all",
+      "re_match",
+      "net.cidr_overlap",
+      "set_diff",
+      "cast_array",
+      "cast_set",
+      "cast_string",
+      "cast_boolean",
+      "cast_null",
+      "cast_object"};
+
+    return std::find_if(
+             deprecated.begin(),
+             deprecated.end(),
+             [name](const std::string& n) { return n == name.view(); }) !=
+      deprecated.end();
+  }
+
+  Node BuiltInsDef::call(
+    const Location& name, const Location& version, const Nodes& args) const
   {
     if (!is_builtin(name))
     {
       return err(args[0], "unknown builtin");
     }
 
+    if (is_deprecated(version, name))
+    {
+      std::ostringstream err_buff;
+      err_buff << "deprecated built-in function calls in expression: "
+               << name.view();
+      return err(args[0], err_buff.str(), RegoTypeError);
+    }
+
     auto& builtin = m_builtins.at(name);
     if (builtin->arity != AnyArity && builtin->arity != args.size())
     {
diff --git a/src/builtins/casts.cc b/src/builtins/casts.cc
index 55a8f0db..0f1a2571 100644
--- a/src/builtins/casts.cc
+++ b/src/builtins/casts.cc
@@ -25,7 +25,30 @@ namespace
       return items;
     }
 
-    return Resolver::set(items);
+    return Resolver::set(items, false);
+  }
+
+  Node cast_set_ex(const Nodes& args)
+  {
+    auto items = unwrap_arg(
+      args, UnwrapOpt(0).types({Array, Set, Object}).exclude_got(true));
+    if (items->type() == Error)
+    {
+      return items;
+    }
+
+    if (items == Object)
+    {
+      Node vals = NodeDef::create(ArgSeq);
+      for (auto& item : *items)
+      {
+        vals << item / Val;
+      }
+
+      items = vals;
+    }
+
+    return Resolver::set(items, false);
   }
 
   Node cast_object(const Nodes& args)
@@ -131,6 +154,7 @@ namespace rego
         BuiltInDef::create(Location("cast_boolean"), 1, cast_boolean),
         BuiltInDef::create(Location("cast_null"), 1, cast_null),
         BuiltInDef::create(Location("cast_set"), 1, cast_set),
+        BuiltInDef::create(Location("cast_set_ex"), 1, cast_set_ex),
         BuiltInDef::create(Location("cast_string"), 1, cast_string),
         BuiltInDef::create(Location("cast_object"), 1, cast_object),
         BuiltInDef::create(Location("to_number"), 1, to_number),
diff --git a/src/builtins/encoding.cc b/src/builtins/encoding.cc
index d4aa8403..19fba1c1 100644
--- a/src/builtins/encoding.cc
+++ b/src/builtins/encoding.cc
@@ -169,24 +169,86 @@ namespace
 
   Node json_marshal(const Nodes& args)
   {
-    Node term = args[0];
-    if (term != Term)
+    Node x = Resolver::to_term(args[0]);
+    if (x->type() == Error)
     {
-      if (term->in({Int, Float, True, False, Null, JSONString}))
+      return x;
+    }
+
+    auto result = to_json().wf_check_enabled(true).rewrite(Top << x);
+    if (!result.ok)
+    {
+      logging::Error log;
+      result.print_errors(log);
+      return err(args[0], "failed to marshal JSON", EvalBuiltInError);
+    }
+
+    std::string json = json::to_string(result.ast);
+    return JSONString ^ ('"' + json::escape(json) + '"');
+  }
+
+  Node json_marshal_with_options(const Nodes& args)
+  {
+    Node x = Resolver::to_term(args[0]);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    Node opts = unwrap_arg(args, UnwrapOpt(1).type(Object));
+    if (opts == Error)
+    {
+      return opts;
+    }
+
+    std::optional<bool> maybe_pretty = std::nullopt;
+    std::string indent = "\t";
+    std::string prefix = "";
+
+    for (auto& item : *opts)
+    {
+      auto maybe_key = unwrap(item / Key, JSONString);
+      if (!maybe_key.success)
       {
-        term = Term << (Scalar << term);
+        return err(item, "key must be a string", EvalBuiltInError);
       }
-      else if (term->in({Array, Object, Set, Scalar}))
+
+      std::string key = get_string(maybe_key.node);
+      if (key == "pretty")
       {
-        term = Term << term;
+        auto maybe_bool = unwrap(item / Val, {True, False});
+        if (!maybe_bool.success)
+        {
+          return err(item, "value must be a boolean", EvalBuiltInError);
+        }
+        maybe_pretty = get_bool(maybe_bool.node);
+      }
+      else if (key == "indent")
+      {
+        indent = get_string(item / Val);
+        if (!maybe_pretty.has_value())
+        {
+          maybe_pretty = true;
+        }
+      }
+      else if (key == "prefix")
+      {
+        prefix = get_string(item / Val);
+        if (!maybe_pretty.has_value())
+        {
+          maybe_pretty = true;
+        }
       }
       else
       {
-        return err(args[0], "failed to marshal JSON", EvalBuiltInError);
+        std::string message = "object contained unknown key \"" + key + '"';
+        return err(item, message, EvalTypeError);
       }
     }
 
-    auto result = to_json().wf_check_enabled(true).rewrite(Top << term);
+    bool pretty = maybe_pretty.value_or(false);
+
+    auto result = to_json().wf_check_enabled(true).rewrite(Top << x);
     if (!result.ok)
     {
       logging::Error log;
@@ -194,7 +256,24 @@ namespace
       return err(args[0], "failed to marshal JSON", EvalBuiltInError);
     }
 
-    std::string json = json::to_string(result.ast);
+    std::string json = json::to_string(result.ast, pretty, false, indent);
+
+    if (pretty && !prefix.empty())
+    {
+      std::ostringstream prefixed;
+      prefixed << prefix;
+      size_t start = 0;
+      size_t end = json.find('\n');
+      while (end != std::string::npos)
+      {
+        prefixed << json.substr(start, end - start) << '\n' << prefix;
+        start = end + 1;
+        end = json.find('\n', start);
+      }
+      prefixed << json.substr(start);
+      json = prefixed.str();
+    }
+
     return JSONString ^ ('"' + json::escape(json) + '"');
   }
 
@@ -222,7 +301,7 @@ namespace
 
   Node json_is_valid(const Nodes& args)
   {
-    auto maybe_x = unwrap(args[0], {JSONString});
+    auto maybe_x = unwrap(args[0], JSONString);
     if (!maybe_x.success)
     {
       return False ^ "false";
@@ -289,7 +368,7 @@ namespace
 
   Node yaml_is_valid(const Nodes& args)
   {
-    auto maybe_x = unwrap(args[0], {JSONString});
+    auto maybe_x = unwrap(args[0], JSONString);
     if (!maybe_x.success)
     {
       return False ^ "false";
@@ -344,7 +423,7 @@ namespace
     std::ostringstream encoded;
     for (std::size_t i = 0; i < object->size(); ++i)
     {
-      auto maybe_key = unwrap(object->at(i) / Key, {JSONString});
+      auto maybe_key = unwrap(object->at(i) / Key, JSONString);
       if (!maybe_key.success)
       {
         return err(maybe_key.node, "key must be a string", EvalBuiltInError);
@@ -378,7 +457,7 @@ namespace
         Node array = maybe_value.node;
         for (std::size_t j = 0; j < array->size(); ++j)
         {
-          maybe_value = unwrap(array->at(j), {JSONString});
+          maybe_value = unwrap(array->at(j), JSONString);
           if (!maybe_value.success)
           {
             return err(
@@ -531,6 +610,8 @@ namespace rego
         BuiltInDef::create(Location("hex.decode"), 1, hex_decode),
         BuiltInDef::create(Location("hex.encode"), 1, hex_encode),
         BuiltInDef::create(Location("json.marshal"), 1, json_marshal),
+        BuiltInDef::create(
+          Location("json.marshal_with_options"), 2, json_marshal_with_options),
         BuiltInDef::create(Location("json.unmarshal"), 1, json_unmarshal),
         BuiltInDef::create(Location("json.is_valid"), 1, json_is_valid),
         BuiltInDef::create(Location("urlquery.decode"), 1, urlquery_decode),
diff --git a/src/builtins/graph.cc b/src/builtins/graph.cc
index f3b06686..732a9501 100644
--- a/src/builtins/graph.cc
+++ b/src/builtins/graph.cc
@@ -118,7 +118,7 @@ namespace
       argseq->push_back(nodes[std::string(key)]);
     }
 
-    return Resolver::set(argseq);
+    return Resolver::set(argseq, false);
   }
 
   class Path
@@ -277,7 +277,7 @@ namespace
       argseq->push_back(path.to_node(nodes));
     }
 
-    return Resolver::set(argseq);
+    return Resolver::set(argseq, false);
   }
 }
 
diff --git a/src/builtins/numbers.cc b/src/builtins/numbers.cc
index e68c5745..b8c85fdd 100644
--- a/src/builtins/numbers.cc
+++ b/src/builtins/numbers.cc
@@ -138,6 +138,77 @@ namespace
     return array;
   }
 
+  Node numbers_range_step(const Nodes& args)
+  {
+    Node lhs_number = unwrap_arg(
+      args,
+      UnwrapOpt(0).type(Int).func("numbers.range_step").specify_number(true));
+    if (lhs_number->type() == Error)
+    {
+      return lhs_number;
+    }
+
+    Node rhs_number = unwrap_arg(
+      args,
+      UnwrapOpt(1).type(Int).func("numbers.range_step").specify_number(true));
+    if (rhs_number->type() == Error)
+    {
+      return rhs_number;
+    }
+
+    Node step_number = unwrap_arg(
+      args,
+      UnwrapOpt(2).type(Int).func("numbers.range_step").specify_number(true));
+    if (step_number->type() == Error)
+    {
+      return step_number;
+    }
+
+    BigInt lhs = get_int(lhs_number);
+    BigInt rhs = get_int(rhs_number);
+    BigInt step = get_int(step_number);
+
+    if (step < BigInt({"1"}))
+    {
+      return err(
+        step_number,
+        "numbers.range_step: step must be a positive number above zero",
+        EvalBuiltInError);
+    }
+
+    Node array = Array ^ args[0];
+    if (lhs < rhs)
+    {
+      BigInt curr = lhs;
+      while (curr < rhs)
+      {
+        array->push_back(Term << (Scalar << (Int ^ curr.loc())));
+        curr = curr + step;
+      }
+
+      if (curr == rhs)
+      {
+        array->push_back(Term << (Scalar << (Int ^ curr.loc())));
+      }
+    }
+    else
+    {
+      BigInt curr = lhs;
+      while (curr > rhs)
+      {
+        array->push_back(Term << (Scalar << (Int ^ curr.loc())));
+        curr = curr - step;
+      }
+
+      if (curr == rhs)
+      {
+        array->push_back(Term << (Scalar << (Int ^ curr.loc())));
+      }
+    }
+
+    return array;
+  }
+
   Node rand_intn(const Nodes& args)
   {
     Node seed_string_node =
@@ -175,6 +246,8 @@ namespace rego
         BuiltInDef::create(Location("floor"), 1, floor),
         BuiltInDef::create(Location("round"), 1, round),
         BuiltInDef::create(Location("numbers.range"), 2, numbers_range),
+        BuiltInDef::create(
+          Location("numbers.range_step"), 3, numbers_range_step),
         BuiltInDef::create(Location("rand.intn"), 2, rand_intn)};
     }
   }
diff --git a/src/builtins/objects.cc b/src/builtins/objects.cc
index 4d1e3755..e623662d 100644
--- a/src/builtins/objects.cc
+++ b/src/builtins/objects.cc
@@ -94,7 +94,7 @@ namespace
     }
     else if (collection->type() == Array)
     {
-      auto maybe_key = unwrap(keys->at(index), {Int});
+      auto maybe_key = unwrap(keys->at(index), Int);
       if (!maybe_key.success)
       {
         return std::nullopt;
diff --git a/src/builtins/regex.cc b/src/builtins/regex.cc
index 0534be24..5b29bd9d 100644
--- a/src/builtins/regex.cc
+++ b/src/builtins/regex.cc
@@ -247,7 +247,7 @@ namespace
 
     Node array = NodeDef::create(Array);
     std::smatch match;
-    for (std::size_t i = 0; i < number; ++i)
+    for (std::size_t i = 0; i < number && !value.empty(); ++i)
     {
       std::regex_search(value, match, re);
       if (match.empty())
@@ -264,22 +264,24 @@ namespace
 
   Node find_all_string_submatch_n(const Nodes& args)
   {
-    Node pattern_node =
-      unwrap_arg(args, UnwrapOpt(0).type(JSONString).func("regex.find_n"));
+    Node pattern_node = unwrap_arg(
+      args,
+      UnwrapOpt(0).type(JSONString).func("regex.find_all_string_submatch_n"));
     if (pattern_node->type() == Error)
     {
       return pattern_node;
     }
 
-    Node value_node =
-      unwrap_arg(args, UnwrapOpt(1).type(JSONString).func("regex.find_n"));
+    Node value_node = unwrap_arg(
+      args,
+      UnwrapOpt(1).type(JSONString).func("regex.find_all_string_submatch_n"));
     if (value_node->type() == Error)
     {
       return value_node;
     }
 
-    Node number_node =
-      unwrap_arg(args, UnwrapOpt(2).type({Int}).func("regex.find_n"));
+    Node number_node = unwrap_arg(
+      args, UnwrapOpt(2).type({Int}).func("regex.find_all_string_submatch_n"));
     if (number_node->type() == Error)
     {
       return number_node;
@@ -301,7 +303,7 @@ namespace
 
     Node array = NodeDef::create(Array);
     std::smatch match;
-    for (std::size_t i = 0; i < number; ++i)
+    for (std::size_t i = 0; i < number && !value.empty(); ++i)
     {
       std::regex_search(value, match, re);
       if (match.empty())
@@ -356,7 +358,7 @@ namespace
     while (true)
     {
       std::regex_search(value, match, re);
-      if (match.empty())
+      if (match.empty() || value.empty())
       {
         array->push_back(Resolver::scalar(value));
         break;
diff --git a/src/builtins/semver.cc b/src/builtins/semver.cc
index 4b08bf66..95d18858 100644
--- a/src/builtins/semver.cc
+++ b/src/builtins/semver.cc
@@ -171,7 +171,7 @@ namespace
 
   Node is_valid(const Nodes& args)
   {
-    auto vsn = unwrap(args[0], {JSONString});
+    auto vsn = unwrap(args[0], JSONString);
     if (!vsn.success)
     {
       return False ^ "false";
diff --git a/src/builtins/strings.cc b/src/builtins/strings.cc
index 4b9e34e0..1068c9d9 100644
--- a/src/builtins/strings.cc
+++ b/src/builtins/strings.cc
@@ -13,7 +13,7 @@ namespace
   {
     for (auto term : *collection)
     {
-      auto maybe_item = unwrap(term, {JSONString});
+      auto maybe_item = unwrap(term, JSONString);
       if (!maybe_item.success)
       {
         return maybe_item.node;
diff --git a/src/encoding.cc b/src/encoding.cc
index 76d4e5b7..6db8c6a2 100644
--- a/src/encoding.cc
+++ b/src/encoding.cc
@@ -191,7 +191,8 @@ namespace rego
         });
       buf << "]";
     }
-    else if (node->type() == Set || node->type() == DataSet)
+    else if (
+      node->type() == Set || node->type() == DataSet || node == DynamicSet)
     {
       std::vector<NodeKey> node_keys;
       for (const auto& child : *node)
@@ -230,7 +231,9 @@ namespace rego
         buf << ">";
       }
     }
-    else if (node->type() == Object || node->type() == DataObject)
+    else if (
+      node == Object || node == DataObject || node == DynamicObject ||
+      node == Bindings)
     {
       std::map<std::string, std::string> items;
       for (const auto& child : *node)
@@ -263,10 +266,46 @@ namespace rego
     {
       return to_key(node->front(), set_as_array, sort_arrays);
     }
-    else if (node->type() == Binding)
+    else if (node == Result)
+    {
+      Node terms = node / Terms;
+      Node bindings = node / Bindings;
+      buf << "{";
+      if (!terms->empty())
+      {
+        buf << '"' << "expressions" << '"' << ":"
+            << to_key(terms, set_as_array, sort_arrays);
+        if (!bindings->empty())
+        {
+          buf << ", ";
+        }
+      }
+
+      if (!bindings->empty())
+      {
+        buf << '"' << "bindings" << '"' << ":"
+            << to_key(bindings, set_as_array, sort_arrays);
+      }
+
+      buf << "}";
+    }
+    else if (node == Terms)
+    {
+      buf << '[';
+      join(
+        buf,
+        node->begin(),
+        node->end(),
+        ", ",
+        [set_as_array, sort_arrays](std::ostream& stream, const Node& n) {
+          stream << to_key(n, set_as_array, sort_arrays);
+          return true;
+        });
+      buf << ']';
+    }
+    else if (node->type() == Var)
     {
-      buf << (node / Var)->location().view() << " = "
-          << to_key(node / Term, set_as_array, sort_arrays);
+      buf << node->location().view();
     }
     else if (node->type() == TermSet)
     {
@@ -295,6 +334,25 @@ namespace rego
       buf << node->type().str() << "(" << (node / Var)->location().view() << ":"
           << static_cast<void*>(node.get()) << ")";
     }
+    else if (node == Results)
+    {
+      if (node->size() == 1)
+      {
+        return to_key(node->front(), set_as_array, sort_arrays);
+      }
+
+      buf << '[';
+      join(
+        buf,
+        node->begin(),
+        node->end(),
+        ", ",
+        [set_as_array, sort_arrays](std::ostream& stream, const Node& n) {
+          stream << to_key(n, set_as_array, sort_arrays);
+          return true;
+        });
+      buf << ']';
+    }
     else
     {
       buf << node->type().str() << "(" << static_cast<void*>(node.get()) << ")";
diff --git a/src/internal.cc b/src/internal.cc
index cf108f23..f2190268 100644
--- a/src/internal.cc
+++ b/src/internal.cc
@@ -1,5 +1,7 @@
 #include "internal.hh"
 
+#include <trieste/json.h>
+
 namespace logging = trieste::logging;
 
 #ifdef _WIN32
@@ -195,6 +197,18 @@ namespace rego
     return std::string(str);
   }
 
+  std::string add_quotes(const std::string_view& str)
+  {
+    if (is_quoted(str))
+    {
+      return std::string(str);
+    }
+
+    std::string quoted(str.size() + 2, '"');
+    std::copy(str.begin(), str.end(), quoted.begin() + 1);
+    return quoted;
+  }
+
   Node err(NodeRange& r, const std::string& msg, const std::string& code)
   {
     return Error << (ErrorMsg ^ msg) << (ErrorAst << r) << (ErrorCode ^ code);
@@ -221,6 +235,7 @@ namespace rego
   Node concat_refs(const Node& lhs, const Node& rhs)
   {
     Node ref;
+
     if (lhs->type() == Var)
     {
       ref = Ref << (RefHead << lhs->clone()) << RefArgSeq;
@@ -263,6 +278,16 @@ namespace rego
       else
       {
         Node index = arg->front();
+        if (index == Expr)
+        {
+          index = index->front();
+        }
+
+        if (index == Term)
+        {
+          index = index->front();
+        }
+
         if (index->type() == Scalar)
         {
           index = index->front();
@@ -370,6 +395,20 @@ namespace rego
     return node->type() == True;
   }
 
+  std::optional<Node> try_get_item(
+    const Node& node, const std::string_view& key)
+  {
+    assert(node == Object || node == DynamicObject);
+
+    auto defs = Resolver::object_lookdown(node, key);
+    if (defs.empty())
+    {
+      return std::nullopt;
+    }
+
+    return defs[0];
+  }
+
   bool is_falsy(const Node& node)
   {
     Node value = node;
@@ -426,6 +465,16 @@ namespace rego
       return "boolean";
     }
 
+    if (type == DynamicObject)
+    {
+      return "object";
+    }
+
+    if (type == DynamicSet)
+    {
+      return "set";
+    }
+
     std::string name(type.str());
     if (starts_with(name, "rego-"))
     {
@@ -459,7 +508,16 @@ namespace rego
   Node UnwrapOpt::unwrap(const Nodes& args) const
   {
     Node node = args[m_index];
-    auto result = rego::unwrap(node, std::set(m_types.begin(), m_types.end()));
+    std::set<Token> types(m_types.begin(), m_types.end());
+    if (contains(types, Object))
+    {
+      types.insert(DynamicObject);
+    }
+    if (contains(types, Set))
+    {
+      types.insert(DynamicSet);
+    }
+    auto result = rego::unwrap(node, types);
     if (result.success)
     {
       return result.node;
@@ -510,6 +568,37 @@ namespace rego
     return err(node, error.str(), m_code);
   }
 
+  UnwrapResult unwrap(const Node& node, const Token& type)
+  {
+    Node value = node;
+    if (value == type)
+    {
+      return {value, true};
+    }
+
+    if (value == Term)
+    {
+      value = value->front();
+    }
+
+    if (value == type)
+    {
+      return {value, true};
+    }
+
+    if (value == Scalar)
+    {
+      value = value->front();
+    }
+
+    if (value == type)
+    {
+      return {value, true};
+    }
+
+    return {value, false};
+  }
+
   UnwrapResult unwrap(const Node& node, const std::set<Token>& types)
   {
     Node value = node;
diff --git a/src/internal.hh b/src/internal.hh
index 66c9c4a0..fa36ffe2 100644
--- a/src/internal.hh
+++ b/src/internal.hh
@@ -15,6 +15,7 @@ namespace rego
   bool all_alnum(const std::string_view& str);
   bool contains_local(const Node& node);
   bool contains_ref(const Node& node);
+  bool refargs_contain_varref(const Node& node);
   Node concat_refs(const Node& lhs, const Node& rhs);
   std::string flatten_ref(const Node& ref);
   bool is_in(const Node& node, const std::set<Token>& token);
@@ -27,6 +28,7 @@ namespace rego
   bool is_ref_to_type(const Node& var, const std::set<Token>& types);
   bool is_module(const Node& var);
   std::string strip_quotes(const std::string_view& str);
+  std::string add_quotes(const std::string_view& str);
   std::string type_name(const Token& type, bool specify_number = false);
   std::string type_name(const Node& node, bool specify_number = false);
 
@@ -53,7 +55,7 @@ namespace rego
     static Node term(const char* value);
     static Node term(const std::string& value);
     static Node term();
-    static Node resolve_query(const Node& query, BuiltIns builtins);
+    static Nodes resolve_query(const Node& query, BuiltIns builtins);
     static void stmt_str(logging::Log&, const Node& stmt);
     static void func_str(logging::Log&, const Node& func);
     static void arg_str(logging::Log&, const Node& arg);
@@ -73,15 +75,20 @@ namespace rego
     static Node boolinfix(const Node& op, const Node& lhs, const Node& rhs);
     static std::optional<Nodes> apply_access(
       const Node& container, const Node& index);
-    static Node object(const Node& object_items, bool is_rule);
+    static Node merge_sets(const Node& lhs, const Node& rhs, bool unwrapped);
+    static Node merge_objects(const Node& lhs, const Node& rhs, bool unwrapped);
+    static Node object(const Node& object_items, bool is_dynamic);
     static Node array(const Node& array_members);
-    static Node set(const Node& set_members);
+    static Node set(const Node& set_members, bool is_dynamic);
     static Node set_intersection(const Node& lhs, const Node& rhs);
     static Node set_union(const Node& lhs, const Node& rhs);
     static Node set_difference(const Node& lhs, const Node& rhs);
     static Nodes resolve_varseq(const Node& varseq);
     static Nodes object_lookdown(const Node& object, const Node& query);
-    static Nodes module_lookdown(const Node& module, const std::string& name);
+    static Nodes object_lookdown(
+      const Node& object, const std::string_view& query);
+    static Nodes module_lookdown(
+      const Node& module, const std::string_view& name);
     static Node inject_args(const Node& rulefunc, const Nodes& args);
     static Node membership(
       const Node& index, const Node& item, const Node& itemseq);
@@ -121,6 +128,8 @@ namespace rego
   inline const auto AssignArg = TokenDef("rego-assignarg");
   inline const auto Idx = TokenDef("rego-idx");
   inline const auto ArgSeq = TokenDef("rego-argseq");
+  inline const auto DynamicObject = TokenDef("rego-dynamicobject");
+  inline const auto DynamicSet = TokenDef("rego-dynamicset");
 
   inline const auto wf_json = JSONString | Int | Float | True | False | Null;
   inline const auto wf_parse_tokens = Query | Module | wf_json | wf_arith_op |
diff --git a/src/interpreter.cc b/src/interpreter.cc
index 54c34b9d..07ccf611 100644
--- a/src/interpreter.cc
+++ b/src/interpreter.cc
@@ -69,7 +69,7 @@ namespace rego
     }
   }
 
-  bool Interpreter::add_module_file(const std::filesystem::path& path)
+  Node Interpreter::add_module_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -83,14 +83,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::add_module(
+  Node Interpreter::add_module(
     const std::string& name, const std::string& contents)
   {
     std::string debug = "module" + std::to_string(m_data_count++);
@@ -100,16 +100,16 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(result.ast->front());
     logging::Info() << "Adding module: " << name << "(" << contents.size()
                     << " bytes)";
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::add_data_json_file(const std::filesystem::path& path)
+  Node Interpreter::add_data_json_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -124,14 +124,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(Data << result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::add_data_json(const std::string& json)
+  Node Interpreter::add_data_json(const std::string& json)
   {
     logging::Info() << "Adding data (" << json.size() << " bytes)";
     std::string debug = "data" + std::to_string(m_data_count++);
@@ -142,14 +142,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(Data << result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::add_data(const Node& node)
+  Node Interpreter::add_data(const Node& node)
   {
     logging::Info() << "Adding data from JSON AST";
     std::string debug = "data" + std::to_string(m_data_count++);
@@ -158,14 +158,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(Data << result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::set_input_json_file(const std::filesystem::path& path)
+  Node Interpreter::set_input_json_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -179,14 +179,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(Input << result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::set_input_term(const std::string& term)
+  Node Interpreter::set_input_term(const std::string& term)
   {
     logging::Info() << "Setting input (" << term.size() << " bytes)";
     auto result = m_reader.synthetic(term) >> m_to_input;
@@ -194,14 +194,14 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
     }
 
     merge(result.ast->front());
-    return true;
+    return nullptr;
   }
 
-  bool Interpreter::set_input(const Node& node)
+  Node Interpreter::set_input(const Node& node)
   {
     logging::Info() << "Setting input from JSON AST";
     auto result = node >> m_from_json.debug_path(m_debug_path / "input");
@@ -209,11 +209,12 @@ namespace rego
     {
       logging::Error err;
       result.print_errors(err);
-      return false;
+      return ErrorSeq << result.errors;
+      ;
     }
 
     merge(Input << result.ast->front());
-    return true;
+    return nullptr;
   }
 
   Node Interpreter::raw_query(const std::string& query_expr)
@@ -278,22 +279,7 @@ namespace rego
     else
     {
       WFContext context(rego::wf_result);
-      std::vector<std::string> results;
-      std::transform(
-        ast->begin(),
-        ast->end(),
-        std::back_inserter(results),
-        [](auto& result) { return rego::to_key(result); });
-      std::sort(results.begin(), results.end());
-      join(
-        output_buf,
-        results.begin(),
-        results.end(),
-        "\n",
-        [](std::ostream& stream, const std::string& result) {
-          stream << result;
-          return true;
-        });
+      output_buf << rego::to_key(ast, true);
     }
 
     return output_buf.str();
@@ -354,8 +340,8 @@ namespace rego
     return m_reader.wf_check_enabled();
   }
 
-  BuiltIns Interpreter::builtins() const
+  BuiltInsDef& Interpreter::builtins() const
   {
-    return m_builtins;
+    return *m_builtins;
   }
 }
diff --git a/src/reader.cc b/src/reader.cc
index 72f619c6..507ce46a 100644
--- a/src/reader.cc
+++ b/src/reader.cc
@@ -1,4 +1,5 @@
 #include "internal.hh"
+#include "rego.hh"
 
 namespace
 {
@@ -209,8 +210,8 @@ namespace
   // clang-format off
   const auto wf_keywords =
     wf_prep
-    | (Module <<= (Group | Package | Import | Keyword)++)
-    | (Keyword <<= Var)[Var]
+    | (Module <<= (Group | Package | Import | Keyword | Version)++)
+    | (Keyword <<= Var)[Var] 
     | (Group <<= wf_keywords_tokens++)
     ;
   // clang-format on
@@ -235,22 +236,62 @@ namespace
           },
 
         In(Module) *
-            (T(Import)
+            (T(Import)[Import]
+             << (T(RefGroup)
+                 << (T(Var, "rego") * T(Dot) * T(Var, "v1")[Version]))) >>
+          [keywords](Match& _) {
+            auto nodes = NodeDef::create(Seq);
+            if (keywords->empty())
+            {
+              keywords->insert({"if", If});
+              nodes << (Keyword << (Var ^ "if"));
+              keywords->insert({"in", IsIn});
+              nodes << (Keyword << (Var ^ "in"));
+              keywords->insert({"contains", Contains});
+              nodes << (Keyword << (Var ^ "contains"));
+              keywords->insert({"every", Every});
+              nodes << (Keyword << (Var ^ "every"));
+              keywords->insert({"version", Version});
+              nodes << (Version ^ _(Version));
+            }
+            else
+            {
+              return err(
+                _(Import),
+                "the `rego.v1` import implies `future.keywords`, these are "
+                "therefore mutually exclusive",
+                RegoParseError);
+            }
+
+            return nodes;
+          },
+
+        In(Module) *
+            (T(Import)[Import]
              << (T(RefGroup)
                  << (T(Var, "future") * T(Dot) * T(Var, "keywords") *
                      ~(T(Dot) * T(Var, "(?:if|in|contains|every)")[Var])))) >>
           [keywords](Match& _) {
+            if (contains(keywords, "version"))
+            {
+              return err(
+                _(Import),
+                "the `rego.v1` import implies `future.keywords`, these are "
+                "therefore mutually exclusive",
+                RegoParseError);
+            }
+
             auto nodes = NodeDef::create(Seq);
             if (_(Var) == nullptr)
             {
               keywords->insert({"if", If});
-              nodes << (Lift << Module << (Keyword << (Var ^ "if")));
+              nodes << (Keyword << (Var ^ "if"));
               keywords->insert({"in", IsIn});
-              nodes << (Lift << Module << (Keyword << (Var ^ "in")));
+              nodes << (Keyword << (Var ^ "in"));
               keywords->insert({"contains", Contains});
-              nodes << (Lift << Module << (Keyword << (Var ^ "contains")));
+              nodes << (Keyword << (Var ^ "contains"));
               keywords->insert({"every", Every});
-              nodes << (Lift << Module << (Keyword << (Var ^ "every")));
+              nodes << (Keyword << (Var ^ "every"));
             }
             else
             {
@@ -272,7 +313,7 @@ namespace
                 keywords->insert({"in", IsIn});
                 keywords->insert({"every", Every});
               }
-              nodes << (Lift << Module << (Keyword << _(Var)));
+              nodes << (Keyword << _(Var));
             }
 
             return nodes;
@@ -295,6 +336,16 @@ namespace
     ;
   // clang-format on
 
+  Node to_var(Node node)
+  {
+    if (node == Var)
+    {
+      return node;
+    }
+
+    return Var ^ node->fresh(node->location());
+  }
+
   PassDef some_every()
   {
     return {
@@ -316,13 +367,15 @@ namespace
           return some;
         },
 
-        In(Group) * (T(Every) * T(Var)[Head] * (T(Comma) * T(Var))++[Tail]) >>
+        In(Group) *
+            (T(Every) * T(Var, Placeholder)[Head] *
+             (T(Comma) * T(Var, Placeholder))++[Tail]) >>
           [](Match& _) {
-            Node every = Every << _(Head);
+            Node every = Every << to_var(_(Head));
             for (auto it = _[Tail].begin(); it != _[Tail].end(); it += 2)
             {
               // skip the comma
-              every << it[1];
+              every << to_var(it[1]);
             }
 
             return every;
@@ -428,7 +481,8 @@ namespace
                        << (RefArgSeq << _(Head) << _[Tail]);
           },
 
-        In(Group, RefGroup) * T(Var)[Var] >>
+        In(Group, RefGroup) *
+            T(Var)[Var]([](auto& n) { return !in_parent(n, RefArgBrack); }) >>
           [](Match& _) { return Ref << (RefHead << _(Var)) << RefArgSeq; },
 
         In(Group) * T(ExprCall)[ExprCall] >>
@@ -636,10 +690,31 @@ namespace
       return 0;
     });
 
-    pass.post([comma_groups, colon_groups, or_groups](Node) {
+    std::shared_ptr<std::map<Location, Location>> import_vars =
+      std::make_shared<std::map<Location, Location>>();
+    pass.post(Import, [import_vars](Node node) {
+      Location ref = (node / Ref)->location();
+      Location loc = (node / Var)->location();
+      if (contains(*import_vars, loc))
+      {
+        std::ostringstream err_buf;
+        err_buf << "import must not shadow import "
+                << import_vars->at(loc).view();
+        node->parent()->replace(
+          node, err(node, err_buf.str(), RegoCompileError));
+      }
+      else
+      {
+        import_vars->insert({loc, ref});
+      }
+      return 0;
+    });
+
+    pass.post([comma_groups, colon_groups, or_groups, import_vars](Node) {
       comma_groups->clear();
       colon_groups->clear();
       or_groups->clear();
+      import_vars->clear();
       return 0;
     });
 
@@ -964,7 +1039,8 @@ namespace
 
   PassDef else_not()
   {
-    return {
+    std::shared_ptr<bool> strict = std::make_shared<bool>(false);
+    PassDef pass = {
       "else_not",
       wf_else_not,
       dir::bottomup | dir::once,
@@ -993,11 +1069,7 @@ namespace
           },
 
         In(Group) * (T(Else) * T(Assign, Unify) * T(Expr)[Expr]) >>
-          [](Match& _) {
-            return Else << _(Expr)
-                        << (Query
-                            << (Group << (Expr << (Term << (Scalar << True)))));
-          },
+          [](Match& _) { return Else << _(Expr) << (Query << Group); },
 
         In(Group) * (T(If)[If] * ~T(Not)[Not] * T(Expr, SomeDecl)[Query]) >>
           [](Match& _) {
@@ -1012,6 +1084,21 @@ namespace
         In(Group) * (T(Not) * T(Expr)[Expr]) >>
           [](Match& _) { return NotExpr << _(Expr); },
 
+        In(ExprInfix) *
+            ((T(Expr)
+              << (T(Term)
+                  << (T(Ref)
+                      << ((T(RefHead) << T(Var, "input|data")[Var]) *
+                          (T(RefArgSeq) << End))))) *
+             (T(InfixOperator) << T(AssignOperator)))(
+              [strict](auto&) { return *strict; }) >>
+          [](Match& _) {
+            std::ostringstream err_buf;
+            err_buf << "variables must not shadow " << _(Var)->location().view()
+                    << " (use a different variable name)";
+            return err(_(Var), err_buf.str(), RegoCompileError);
+          },
+
         // errors
 
         T(Else)[Else] << End >>
@@ -1019,6 +1106,18 @@ namespace
             return err(_(Else), "Invalid else statement", WellFormedError);
           },
       }};
+
+    pass.pre(Version, [strict](Node) {
+      *strict = true;
+      return 0;
+    });
+
+    pass.post([strict](Node) {
+      *strict = false;
+      return 0;
+    });
+
+    return pass;
   }
 
   const auto wf_collections_tokens = wf_else_not_tokens -
@@ -1218,13 +1317,14 @@ namespace
 
   PassDef rules()
   {
-    return {
+    std::shared_ptr<bool> strict = std::make_shared<bool>(false);
+    PassDef pass = {
       "rules",
       wf_rules,
       dir::bottomup | dir::once,
       {
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
-            ((T(Expr)
+            ((T(Expr)[Rule]
               << (T(ExprInfix)
                   << ((T(Expr)
                        << (T(Term)
@@ -1237,8 +1337,16 @@ namespace
                                    })))))) *
                       (T(InfixOperator) << T(AssignOperator)) *
                       T(Expr)[Val]))) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+             ~T(If)[If] * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             Node refargseq = _(RefArgSeq);
             Node key = refargseq->pop_back()->front();
             Node ruleref = _(RuleRef);
@@ -1253,7 +1361,7 @@ namespace
           },
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
-            ((T(Expr)
+            ((T(Expr)[Rule]
               << (T(Term)
                   << (T(Ref)
                       << ((T(RefHead) << T(Var)[RuleRef]) *
@@ -1262,19 +1370,28 @@ namespace
                             return !refargseq->empty() &&
                               refargseq->back()->type() == RefArgBrack;
                           })))))) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+             ~T(If)[If] * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             Node refargseq = _(RefArgSeq);
             Node value = refargseq->pop_back()->front();
             Node ruleref = _(RuleRef);
+            auto defs = _(RuleRef)->scope()->lookdown({"contains"});
+
             if (!refargseq->empty())
             {
               ruleref = Ref << (RefHead << ruleref) << refargseq;
             }
 
-            auto defs = _(RuleRef)->scope()->lookdown({"contains"});
             Node rulehead;
-            if (defs.empty())
+            if (defs.empty() && !refargs_contain_varref(refargseq))
             {
               rulehead = RuleHead << (RuleRef << ruleref)
                                   << (RuleHeadSet << value);
@@ -1292,13 +1409,21 @@ namespace
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
             (~T(Default)[Default] *
-             (T(Expr)
+             (T(Expr)[Rule]
               << (T(ExprInfix)
                   << ((T(Expr) << (T(Term) << T(Ref)[RuleRef])) *
                       (T(InfixOperator) << T(AssignOperator)) *
                       T(Expr)[Expr]))) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+             ~T(If)[If] * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             return Rule << (_(Default) != nullptr ? True : False)
                         << (RuleHead << (RuleRef << _(RuleRef))
                                      << (RuleHeadComp << _(Expr)))
@@ -1306,9 +1431,18 @@ namespace
           },
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
-            (~T(Default)[Default] * (T(Expr) << (T(Term) << T(Ref)[RuleRef])) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+            (~T(Default)[Default] *
+             (T(Expr)[Rule] << (T(Term) << T(Ref)[RuleRef])) * ~T(If)[If] *
+             T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             return Rule << (_(Default) != nullptr ? True : False)
                         << (RuleHead
                             << (RuleRef << _(RuleRef))
@@ -1320,15 +1454,23 @@ namespace
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
             (~T(Default)[Default] *
-             (T(Expr)
+             (T(Expr)[Rule]
               << (T(ExprInfix)
                   << ((T(Expr)
                        << (T(ExprCall)
                            << (T(Ref)[RuleRef] * T(ExprSeq)[RuleArgs]))) *
                       (T(InfixOperator) << T(AssignOperator)) *
                       T(Expr)[Expr]))) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+             ~T(If)[If] * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             return Rule << (_(Default) != nullptr ? True : False)
                         << (RuleHead
                             << (RuleRef << _(RuleRef))
@@ -1339,10 +1481,18 @@ namespace
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
             (~T(Default)[Default] *
-             (T(Expr)
+             (T(Expr)[Rule]
               << (T(ExprCall) << (T(Ref)[RuleRef] * T(ExprSeq)[RuleArgs]))) *
-             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
-          [](Match& _) {
+             ~T(If)[If] * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             return Rule << (_(Default) != nullptr ? True : False)
                         << (RuleHead
                             << (RuleRef << _(RuleRef))
@@ -1354,10 +1504,18 @@ namespace
           },
 
         In(Group)([](auto& n) { return in_parent(n, Module); }) *
-            ((T(Expr) << (T(Term) << (T(Ref)[RuleRef]))) * T(Contains) *
-             T(Expr)[Expr] * ~T(If) * T(Query, Else)++[RuleBodySeq] *
+            ((T(Expr)[Rule] << (T(Term) << (T(Ref)[RuleRef]))) * T(Contains) *
+             T(Expr)[Expr] * ~T(If)[If] * T(Query, Else)++[RuleBodySeq] *
              T(NewLine)) >>
-          [](Match& _) {
+          [strict](Match& _) {
+            if (*strict && _(If) == nullptr && !_[RuleBodySeq].empty())
+            {
+              return err(
+                _(Rule),
+                "`if` keyword is required before rule body",
+                RegoParseError);
+            }
+
             return Rule << False
                         << (RuleHead << (RuleRef << _(RuleRef))
                                      << (RuleHeadSet << _(Expr)))
@@ -1369,10 +1527,22 @@ namespace
             return err(_(Op), "Invalid rule head", WellFormedError);
           },
       }};
+
+    pass.pre(Version, [strict](Node) {
+      *strict = true;
+      return 0;
+    });
+
+    pass.post([strict](Node) {
+      *strict = false;
+      return 0;
+    });
+
+    return pass;
   }
 
   const auto wf_literals_tokens = (wf_rules_tokens | Literal) -
-    (Expr | NotExpr | SomeDecl | With | NewLine | Keyword);
+    (Expr | NotExpr | SomeDecl | With | NewLine | Keyword | Version);
 
   // clang-format off
   const auto wf_literals =
@@ -1422,16 +1592,54 @@ namespace
       dir::bottomup | dir::once,
       {
         In(Module) *
-            (T(Package)[Package] * T(Import)++[ImportSeq] *
+            (T(Package)[Package] * T(Import, Version)++[ImportSeq] *
              (T(Group) << (T(Rule)++[Policy] * End))) >>
           [](Match& _) {
-            return Seq << _(Package) << (ImportSeq << _[ImportSeq])
+            Node version;
+            Node importseq = NodeDef::create(ImportSeq);
+            for (auto node : _[ImportSeq])
+            {
+              if (node == Version)
+              {
+                version = node;
+              }
+              else
+              {
+                importseq << node;
+              }
+            }
+
+            if (version == nullptr)
+            {
+              version = Version ^ DefaultVersion;
+            }
+
+            return Seq << _(Package) << version << importseq
                        << (Policy << _[Policy]);
           },
 
         In(Module) * (T(Package)[Package] * T(Import)++[ImportSeq] * End) >>
           [](Match& _) {
-            return Seq << _(Package) << (ImportSeq << _[ImportSeq]) << Policy;
+            Node version;
+            Node importseq = NodeDef::create(ImportSeq);
+            for (auto node : _[ImportSeq])
+            {
+              if (node == Version)
+              {
+                version = node;
+              }
+              else
+              {
+                importseq << node;
+              }
+            }
+
+            if (version == nullptr)
+            {
+              version = Version ^ DefaultVersion;
+            }
+
+            return Seq << _(Package) << version << importseq << Policy;
           },
 
         In(Query) * (T(Group) << (T(Literal)++[Query] * End)) >>
@@ -1452,13 +1660,6 @@ namespace
         In(VarSeq) * (T(Term) << T(Var)[Var]) >>
           [](Match& _) { return _(Var); },
 
-        In(RefArgBrack) *
-            (T(Expr) << (T(Term) << T(Scalar, Var, Object, Array, Set)[Idx])) >>
-          [](Match& _) { return _(Idx); },
-
-        In(RefArgBrack) * (T(Expr) << T(ExprInfix)[ExprInfix]) >>
-          [](Match& _) { return _(ExprInfix); },
-
         In(RuleHead) * (T(RuleHeadComp) << End) >>
           [](Match&) {
             return RuleHeadComp
@@ -1471,14 +1672,14 @@ namespace
             return err(_(Group), "Syntax error", WellFormedError);
           },
 
-        T(Module)[Module] << (T(Import, Group)++ * End) >>
+        T(Module)[Module] << (T(Import, Version, Group)++ * End) >>
           [](Match& _) {
             return err(
               _(Module), "Missing package declaration", WellFormedError);
           },
 
-        T(Module)[Module] << T(Package) * T(Import)++ * T(Group) *
-              T(Import)[Import] >>
+        T(Module)[Module] << T(Package) * T(Import, Version)++ * T(Group) *
+              T(Import, Version)[Import] >>
           [](Match& _) {
             return err(
               _(Import),
diff --git a/src/rego_c.cc b/src/rego_c.cc
index 9de94283..6bb32d26 100644
--- a/src/rego_c.cc
+++ b/src/rego_c.cc
@@ -1,3 +1,5 @@
+#include "rego_c.h"
+
 #include "internal.hh"
 
 namespace logging = trieste::logging;
@@ -86,13 +88,26 @@ extern "C"
     delete reinterpret_cast<rego::Interpreter*>(rego);
   }
 
+  regoEnum ok_or_error(const rego::Node& result)
+  {
+    if (result == nullptr)
+    {
+      return REGO_OK;
+    }
+
+    std::ostringstream output_buf;
+    output_buf << result;
+    rego::setError(nullptr, output_buf.str());
+    return REGO_ERROR;
+  }
+
   regoEnum regoAddModuleFile(regoInterpreter* rego, const char* path)
   {
     logging::Debug() << "regoAddModuleFile: " << path;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_module_file(path);
-      return REGO_OK;
+      return ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->add_module_file(path));
     }
     catch (const std::exception& e)
     {
@@ -107,8 +122,8 @@ extern "C"
     logging::Debug() << "regoAddModule: " << name;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_module(name, contents);
-      return REGO_OK;
+      return ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->add_module(name, contents));
     }
     catch (const std::exception& e)
     {
@@ -122,8 +137,8 @@ extern "C"
     logging::Debug() << "regoAddDataJSONFile: " << path;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_data_json_file(path);
-      return REGO_OK;
+      return ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->add_data_json_file(path));
     }
     catch (const std::exception& e)
     {
@@ -137,8 +152,8 @@ extern "C"
     logging::Debug() << "regoAddDataJSON: " << contents;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->add_data_json(contents);
-      return REGO_OK;
+      return ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->add_data_json(contents));
     }
     catch (const std::exception& e)
     {
@@ -152,8 +167,8 @@ extern "C"
     logging::Debug() << "regoSetInputJSONFile: " << path;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->set_input_json_file(path);
-      return REGO_OK;
+      return ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->set_input_json_file(path));
     }
     catch (const std::exception& e)
     {
@@ -175,7 +190,8 @@ extern "C"
     logging::Debug() << "regoSetInputTerm: " << contents;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->set_input_term(contents);
+      ok_or_error(
+        reinterpret_cast<rego::Interpreter*>(rego)->set_input_term(contents));
       return REGO_OK;
     }
     catch (const std::exception& e)
@@ -248,7 +264,7 @@ extern "C"
   void regoSetStrictBuiltInErrors(regoInterpreter* rego, regoBoolean enabled)
   {
     logging::Debug() << "regoSetStrictBuiltInErrors: " << enabled;
-    reinterpret_cast<rego::Interpreter*>(rego)->builtins()->strict_errors(
+    reinterpret_cast<rego::Interpreter*>(rego)->builtins().strict_errors(
       enabled);
   }
 
@@ -257,7 +273,7 @@ extern "C"
     logging::Debug() << "regoGetStrictBuiltInErrors";
     return reinterpret_cast<rego::Interpreter*>(rego)
       ->builtins()
-      ->strict_errors();
+      .strict_errors();
   }
 
   // Output functions
@@ -268,6 +284,43 @@ extern "C"
       rego::ErrorSeq;
   }
 
+  regoSize regoOutputSize(regoOutput* output)
+  {
+    logging::Debug() << "regoOutputSize";
+    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)
+    {
+      return 0;
+    }
+
+    assert(node_ptr->type() == rego::Results);
+    return static_cast<regoSize>(node_ptr->size());
+  }
+
+  regoNode* regoOutputExpressionsAtIndex(regoOutput* output, regoSize index)
+  {
+    logging::Debug() << "regoOutputExpressionsAtIndex: " << index;
+    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)
+    {
+      return nullptr;
+    }
+
+    assert(node_ptr->type() == rego::Results);
+    trieste::WFContext context(rego::wf_result);
+    rego::Node result = node_ptr->at(index);
+    rego::Node terms = result / rego::Terms;
+    return reinterpret_cast<regoNode*>(terms.get());
+  }
+
+  regoNode* regoOutputExpressions(regoOutput* output)
+  {
+    logging::Debug() << "regoOutputExpressions";
+    return regoOutputExpressionsAtIndex(output, 0);
+  }
+
   regoNode* regoOutputNode(regoOutput* output)
   {
     logging::Debug() << "regoOutputNode";
@@ -275,9 +328,10 @@ extern "C"
       reinterpret_cast<rego::regoOutput*>(output)->node.get());
   }
 
-  regoNode* regoOutputBinding(regoOutput* output, const char* name)
+  regoNode* regoOutputBindingAtIndex(
+    regoOutput* output, regoSize index, const char* name)
   {
-    logging::Debug() << "regoOutputBinding: " << name;
+    logging::Debug() << "regoOutputBindingAtIndex: " << 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)
@@ -285,18 +339,25 @@ extern "C"
       return nullptr;
     }
 
-    assert(node_ptr->type() == rego::Result);
+    assert(node_ptr->type() == rego::Results);
     trieste::WFContext context(rego::wf_result);
-    auto defs = node_ptr->lookdown({name});
+    auto result = node_ptr->at(index);
+    auto defs = result->lookdown({name});
     if (defs.empty())
     {
       return nullptr;
     }
 
-    rego::Node val = defs[0] / rego::Term;
+    rego::Node val = defs[0] / rego::Val;
     return reinterpret_cast<regoNode*>(val.get());
   }
 
+  regoNode* regoOutputBinding(regoOutput* output, const char* name)
+  {
+    logging::Debug() << "regoOutputBinding: " << name;
+    return regoOutputBindingAtIndex(output, 0, name);
+  }
+
   const char* regoOutputString(regoOutput* output)
   {
     logging::Debug() << "regoOutputString";
@@ -315,102 +376,122 @@ extern "C"
     logging::Debug() << "regoNodeType";
     auto node = reinterpret_cast<trieste::NodeDef*>(node_ptr);
 
-    if (node->type() == rego::Binding)
+    if (node == rego::Binding)
     {
       return REGO_NODE_BINDING;
     }
 
-    if (node->type() == rego::Var)
+    if (node == rego::Var)
     {
       return REGO_NODE_VAR;
     }
 
-    if (node->type() == rego::Term)
+    if (node == rego::Term)
     {
       return REGO_NODE_TERM;
     }
 
-    if (node->type() == rego::Scalar)
+    if (node == rego::Scalar)
     {
       return REGO_NODE_SCALAR;
     }
 
-    if (node->type() == rego::Array)
+    if (node == rego::Array)
     {
       return REGO_NODE_ARRAY;
     }
 
-    if (node->type() == rego::Set)
+    if (node == rego::Set)
     {
       return REGO_NODE_SET;
     }
 
-    if (node->type() == rego::Object)
+    if (node == rego::Object)
     {
       return REGO_NODE_OBJECT;
     }
 
-    if (node->type() == rego::ObjectItem)
+    if (node == rego::ObjectItem)
     {
       return REGO_NODE_OBJECT_ITEM;
     }
 
-    if (node->type() == rego::Int)
+    if (node == rego::Int)
     {
       return REGO_NODE_INT;
     }
 
-    if (node->type() == rego::Float)
+    if (node == rego::Float)
     {
       return REGO_NODE_FLOAT;
     }
 
-    if (node->type() == rego::JSONString)
+    if (node == rego::JSONString)
     {
       return REGO_NODE_STRING;
     }
 
-    if (node->type() == rego::True)
+    if (node == rego::True)
     {
       return REGO_NODE_TRUE;
     }
 
-    if (node->type() == rego::False)
+    if (node == rego::False)
     {
       return REGO_NODE_FALSE;
     }
 
-    if (node->type() == rego::Null)
+    if (node == rego::Null)
     {
       return REGO_NODE_NULL;
     }
 
-    if (node->type() == rego::Undefined)
+    if (node == rego::Undefined)
     {
       return REGO_NODE_UNDEFINED;
     }
 
-    if (node->type() == rego::Error)
+    if (node == rego::Terms)
+    {
+      return REGO_NODE_TERMS;
+    }
+
+    if (node == rego::Bindings)
+    {
+      return REGO_NODE_BINDINGS;
+    }
+
+    if (node == rego::Results)
+    {
+      return REGO_NODE_RESULTS;
+    }
+
+    if (node == rego::Result)
+    {
+      return REGO_NODE_RESULT;
+    }
+
+    if (node == rego::Error)
     {
       return REGO_NODE_ERROR;
     }
 
-    if (node->type() == rego::ErrorMsg)
+    if (node == rego::ErrorMsg)
     {
       return REGO_NODE_ERROR_MESSAGE;
     }
 
-    if (node->type() == rego::ErrorAst)
+    if (node == rego::ErrorAst)
     {
       return REGO_NODE_ERROR_AST;
     }
 
-    if (node->type() == rego::ErrorCode)
+    if (node == rego::ErrorCode)
     {
       return REGO_NODE_ERROR_CODE;
     }
 
-    if (node->type() == rego::ErrorSeq)
+    if (node == rego::ErrorSeq)
     {
       return REGO_NODE_ERROR_SEQ;
     }
diff --git a/src/resolver.cc b/src/resolver.cc
index a395e599..a1e748a6 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -1,5 +1,8 @@
+#include "rego.hh"
 #include "unify.hh"
 
+#include <algorithm>
+
 namespace
 {
   using namespace rego;
@@ -362,8 +365,8 @@ namespace rego
     }
     else
     {
-      auto maybe_lhs_set = unwrap(lhs, {Set});
-      auto maybe_rhs_set = unwrap(rhs, {Set});
+      auto maybe_lhs_set = unwrap(lhs, {Set, DynamicSet});
+      auto maybe_rhs_set = unwrap(rhs, {Set, DynamicSet});
       if (maybe_lhs_set.success && maybe_rhs_set.success)
       {
         return bininfix(op, maybe_lhs_set.node, maybe_rhs_set.node);
@@ -385,8 +388,8 @@ namespace rego
 
   Node Resolver::bininfix(const Node& op, const Node& lhs, const Node& rhs)
   {
-    auto maybe_lhs_set = unwrap(lhs, {Set});
-    auto maybe_rhs_set = unwrap(rhs, {Set});
+    auto maybe_lhs_set = unwrap(lhs, {Set, DynamicSet});
+    auto maybe_rhs_set = unwrap(rhs, {Set, DynamicSet});
 
     if (maybe_lhs_set.success && maybe_rhs_set.success)
     {
@@ -488,10 +491,10 @@ namespace rego
       }
     }
 
-    if (container->type() == Object)
+    if (container == Object || container == DynamicObject)
     {
       Node query = arg;
-      if (query->type() == Term)
+      if (query == Term)
       {
         query = arg->front();
       }
@@ -506,7 +509,7 @@ namespace rego
       return module_lookdown(container, key_str);
     }
 
-    if (container->type() == Set)
+    if (container == Set || container == DynamicSet)
     {
       std::set<std::string> reprs;
       std::string query_repr = to_key(arg);
@@ -527,14 +530,14 @@ namespace rego
 
   Node Resolver::to_term(const Node& value)
   {
-    if (value->type() == Term || value->type() == TermSet)
+    if (value == Term || value == TermSet)
     {
       return value->clone();
     }
 
     if (
-      value->type() == Array || value->type() == Set ||
-      value->type() == Object || value->type() == Scalar)
+      value == Array || value == DynamicSet || value == Set ||
+      value == DynamicObject || value == Object || value == Scalar)
     {
       return Term << value->clone();
     }
@@ -550,42 +553,240 @@ namespace rego
     return err(value, "Not a term");
   }
 
-  Node Resolver::object(const Node& object_items, bool is_rule)
+  Node Resolver::merge_sets(const Node& lhs, const Node& rhs, bool unwrapped)
+  {
+    logging::Trace() << "merge_sets" << lhs << rhs << std::endl;
+
+    Node lhs_set = lhs;
+    Node rhs_set = rhs;
+
+    if (!unwrapped)
+    {
+      auto maybe_lhs = unwrap(lhs, DynamicSet);
+      auto maybe_rhs = unwrap(rhs, DynamicSet);
+
+      if (!maybe_lhs.success && !maybe_rhs.success)
+      {
+        auto lhs_key = to_key(lhs);
+        auto rhs_key = to_key(rhs);
+        if (lhs_key == rhs_key)
+        {
+          return lhs;
+        }
+        else
+        {
+          return err(lhs, "object keys must be unique", EvalConflictError);
+        }
+      }
+
+      if (!maybe_lhs.success)
+      {
+        return err(lhs, "conflicting values for rule", EvalConflictError);
+      }
+
+      if (!maybe_rhs.success)
+      {
+        return err(rhs, "conflicting values for rule", EvalConflictError);
+      }
+
+      lhs_set = maybe_lhs.node;
+      rhs_set = maybe_rhs.node;
+    }
+
+    Node set = NodeDef::create(DynamicSet);
+    std::set<std::string> items;
+    for (auto& item : *lhs_set)
+    {
+      items.insert(to_key(item));
+      set << item;
+    }
+
+    for (auto& item : *rhs_set)
+    {
+      if (!contains(items, to_key(item)))
+      {
+        set << item;
+      }
+    }
+
+    return Term << set;
+  }
+
+  Node Resolver::merge_objects(const Node& lhs, const Node& rhs, bool unwrapped)
+  {
+    logging::Trace() << "merge_objects" << lhs << rhs << std::endl;
+
+    Node lhs_obj = lhs;
+    Node rhs_obj = rhs;
+    if (!unwrapped)
+    {
+      auto maybe_lhs = unwrap(lhs, {DynamicObject, DynamicSet});
+      auto maybe_rhs = unwrap(rhs, {DynamicObject, DynamicSet});
+
+      if (!maybe_lhs.success && !maybe_rhs.success)
+      {
+        auto lhs_key = to_key(lhs);
+        auto rhs_key = to_key(rhs);
+        if (lhs_key == rhs_key)
+        {
+          return lhs;
+        }
+        else
+        {
+          return err(lhs, "object keys must be unique", EvalConflictError);
+        }
+      }
+
+      if (!maybe_lhs.success)
+      {
+        return err(lhs, "conflicting values for rule", EvalConflictError);
+      }
+
+      if (!maybe_rhs.success)
+      {
+        return err(rhs, "conflicting values for rule", EvalConflictError);
+      }
+
+      lhs_obj = maybe_lhs.node;
+      rhs_obj = maybe_rhs.node;
+
+      if (lhs_obj == DynamicSet && rhs_obj == DynamicSet)
+      {
+        return merge_sets(lhs_obj, rhs_obj, true);
+      }
+
+      if (lhs_obj != DynamicObject || rhs_obj != DynamicObject)
+      {
+        return err(rhs, "conflicting values for rule", EvalConflictError);
+      }
+    }
+
+    std::map<std::string, Node> items;
+    for (auto& item : *lhs_obj)
+    {
+      items[to_key(item / Key)] = item;
+    }
+
+    for (auto& item : *rhs_obj)
+    {
+      std::string key = to_key(item / Key);
+      if (contains(items, key))
+      {
+        Node merged = merge_objects(items[key] / Val, item / Val, false);
+        if (merged == Error)
+        {
+          return merged;
+        }
+        items[key] = ObjectItem << (item / Key) << merged;
+      }
+      else
+      {
+        items[key] = item;
+      }
+    }
+
+    auto object = NodeDef::create(DynamicObject);
+    for (auto& [_, item] : items)
+    {
+      object->push_back(item);
+    }
+
+    return Term << object;
+  }
+
+  Node Resolver::object(const Node& object_items, bool is_dynamic)
   {
-    Node object = NodeDef::create(Object);
+    Node object = NodeDef::create(is_dynamic ? DynamicObject : Object);
     std::map<std::string, Node> items;
     for (std::size_t i = 0; i < object_items->size(); i += 2)
     {
-      if (object_items->at(i)->type() == Error)
+      const Node& key = object_items->at(i);
+      const Node& val = object_items->at(i + 1);
+      if (key == Error)
       {
-        return object_items->at(i);
+        return key;
       }
 
-      std::string key = to_key(object_items->at(i));
-      if (contains(items, key))
+      if (val == Error)
+      {
+        return val;
+      }
+
+      std::string key_str = to_key(key);
+      if (contains(items, key_str))
       {
-        std::string current = to_key(items[key] / Val);
-        std::string next = to_key(object_items->at(i + 1));
-        if (current != next)
+        Node old_val = items[key_str] / Val;
+        auto maybe_lhs = unwrap(old_val, {DynamicObject, DynamicSet});
+        auto maybe_rhs = unwrap(val, {DynamicObject, DynamicSet});
+
+        if (!maybe_lhs.success && !maybe_rhs.success)
         {
-          if (is_rule)
+          auto lhs_str = to_key(old_val);
+          auto rhs_str = to_key(val);
+          if (lhs_str != rhs_str)
           {
-            return err(
-              object_items->at(i),
-              "complete rules must not produce multiple outputs",
-              EvalConflictError);
+            if (is_dynamic)
+            {
+              return err(
+                val,
+                "complete rules must not produce multiple outputs",
+                EvalConflictError);
+            }
+            else
+            {
+              return err(val, "object keys must be unique", EvalConflictError);
+            }
+          }
+          else
+          {
+            continue;
           }
+        }
 
-          return err(
-            object_items->at(i),
-            "object keys must be unique",
-            EvalConflictError);
+        if (!maybe_lhs.success)
+        {
+          return err(old_val, "conflicting values for rule", EvalConflictError);
         }
-      }
 
-      Node item = ObjectItem << to_term(object_items->at(i))
-                             << to_term(object_items->at(i + 1));
-      items[key] = item;
+        if (!maybe_rhs.success)
+        {
+          return err(val, "conflicting values for rule", EvalConflictError);
+        }
+
+        Node lhs = maybe_lhs.node;
+        Node rhs = maybe_rhs.node;
+
+        if (lhs == DynamicObject)
+        {
+          if (rhs == DynamicObject)
+          {
+            items[key_str] = ObjectItem << key << merge_objects(lhs, rhs, true);
+          }
+          else
+          {
+            return err(val, "conflicting values for rule", EvalConflictError);
+          }
+        }
+        else if (lhs == DynamicSet)
+        {
+          if (rhs == DynamicSet)
+          {
+            items[key_str] = ObjectItem << key << merge_sets(lhs, rhs, true);
+          }
+          else
+          {
+            return err(val, "conflicting values for rule", EvalConflictError);
+          }
+        }
+        else
+        {
+          return err(old_val, "conflicting values for rule", EvalConflictError);
+        }
+      }
+      else
+      {
+        items[key_str] = ObjectItem << key << val;
+      }
     }
 
     // objects need to be created with sorted keys
@@ -620,7 +821,7 @@ namespace rego
     }
   }
 
-  Node Resolver::set(const Node& set_members)
+  Node Resolver::set(const Node& set_members, bool is_dynamic)
   {
     std::map<std::string, Node> members;
     for (Node member : *set_members)
@@ -637,7 +838,7 @@ namespace rego
       }
     }
 
-    Node set = NodeDef::create(Set);
+    Node set = NodeDef::create(is_dynamic ? DynamicSet : Set);
     for (auto [_, member] : members)
     {
       set->push_back(member);
@@ -647,17 +848,19 @@ namespace rego
 
   Node Resolver::set_intersection(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set)
+    if (!(lhs == Set || lhs == DynamicSet))
     {
       return err(lhs, "intersection: both arguments must be sets");
     }
 
-    if (rhs->type() != Set)
+    if (!(rhs == Set || rhs == DynamicSet))
     {
       return err(rhs, "intersection: both arguments must be sets");
     }
 
-    Node set = NodeDef::create(Set);
+    Node set = NodeDef::create(
+      lhs == DynamicSet || rhs == DynamicSet ? DynamicSet : Set);
+
     std::set<std::string> values;
     for (auto term : *lhs)
     {
@@ -677,16 +880,19 @@ namespace rego
 
   Node Resolver::set_union(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set)
+    if (!(lhs == Set || lhs == DynamicSet))
     {
       return err(lhs, "union: both arguments must be sets");
     }
 
-    if (rhs->type() != Set)
+    if (!(rhs == Set || rhs == DynamicSet))
     {
       return err(rhs, "union: both arguments must be sets");
     }
 
+    Node set = NodeDef::create(
+      lhs == DynamicSet || rhs == DynamicSet ? DynamicSet : Set);
+
     std::map<std::string, Node> members;
     for (auto term : *lhs)
     {
@@ -702,7 +908,6 @@ namespace rego
       }
     }
 
-    Node set = NodeDef::create(Set);
     for (auto [_, member] : members)
     {
       set->push_back(member->clone());
@@ -713,17 +918,18 @@ namespace rego
 
   Node Resolver::set_difference(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set)
+    if (!(lhs == Set || lhs == DynamicSet))
     {
       return err(lhs, "difference: both arguments must be sets");
     }
 
-    if (rhs->type() != Set)
+    if (!(rhs == Set || rhs == DynamicSet))
     {
       return err(rhs, "difference: both arguments must be sets");
     }
 
-    Node set = NodeDef::create(Set);
+    Node set = NodeDef::create(
+      lhs == DynamicSet || rhs == DynamicSet ? DynamicSet : Set);
     std::set<std::string> values;
     for (auto term : *rhs)
     {
@@ -977,7 +1183,7 @@ namespace rego
   }
 
   Nodes Resolver::module_lookdown(
-    const Node& container, const std::string& query)
+    const Node& container, const std::string_view& query)
   {
     Node module = container;
     if (module->type() == Submodule || module->type() == Data)
@@ -1013,7 +1219,7 @@ namespace rego
         name = (rule / Var)->location();
       }
 
-      if (name == query)
+      if (name.view() == query)
       {
         rules.push_back(rule);
       }
@@ -1023,9 +1229,14 @@ namespace rego
   }
 
   Nodes Resolver::object_lookdown(const Node& object, const Node& query)
+  {
+    return Resolver::object_lookdown(object, to_key(query));
+  }
+
+  Nodes Resolver::object_lookdown(
+    const Node& object, const std::string_view& query_str)
   {
     Nodes terms;
-    std::string query_str = to_key(query);
     for (auto& object_item : *object)
     {
       Node key = object_item / Key;
@@ -1100,44 +1311,46 @@ namespace rego
     return reduce;
   }
 
-  Node Resolver::resolve_query(const Node& query, BuiltIns builtins)
+  Nodes Resolver::resolve_query(const Node& query, BuiltIns builtins)
   {
     Nodes defs = query->front()->lookup();
     if (defs.size() != 1)
     {
-      return err(query, "query not found");
+      return {err(query, "query not found")};
     }
 
     Node rulebody = defs[0] / Val;
     Location rulename{"query"};
+    Location version = (defs[0] / Version)->location();
     UnifierCache cache = std::make_shared<std::map<UnifierKey, Unifier>>();
     auto unifier = UnifierDef::create(
       UnifierKey{rulename, UnifierType::RuleBody},
       rulename,
+      version,
       rulebody,
       std::make_shared<std::vector<Location>>(),
       std::make_shared<std::vector<ValuesLookup>>(),
       builtins,
       cache);
-    Node result = unifier->unify();
+    Node maybe_error = 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)
+    if (maybe_error == Error)
     {
-      return Query << result;
+      return {Result << maybe_error};
     }
 
-    result = NodeDef::create(Query);
+    Nodes results{Result << Terms << Bindings};
 
     for (auto& child : *rulebody)
     {
       if (child->type() == Error)
       {
-        result->push_back(child);
+        results.back() / Terms << child;
         continue;
       }
 
@@ -1161,40 +1374,70 @@ namespace rego
         }
       }
 
-      if (term->type() != Term)
-      {
-        term = Term << term;
-      }
-
       if (is_undefined(term))
       {
         continue;
       }
 
-      if (contains_multiple_outputs(term))
-      {
-        result->push_back(err(
-          term,
-          "complete rules must not produce multiple outputs",
-          EvalConflictError));
-        continue;
-      }
-
       std::string name = std::string(var->location().view());
 
-      if (starts_with(name, "value$"))
+      if (term == TermSet)
       {
-        result->push_back(term);
+        Node termset = term;
+        // there are multiple bindings/terms
+        while (results.size() < termset->size())
+        {
+          results.push_back(Result << Terms << Bindings);
+        }
+
+        for (std::size_t i = 0; i < termset->size(); i++)
+        {
+          term = termset->at(i);
+          if (contains_multiple_outputs(term))
+          {
+            results[i] / Terms << err(
+              term,
+              "complete rules must not produce multiple outputs",
+              EvalConflictError);
+          }
+          else if (starts_with(name, "value$"))
+          {
+            results[i] / Terms << term;
+          }
+          else
+          {
+            results[i] / Bindings << (Binding << var->clone() << term);
+          }
+        }
       }
-      else if (name.find('$') == std::string::npos || name[0] == '$')
+      else if (results.size() == 1)
       {
-        // either a user-defined variable (no $) or
-        // a fuzzer variable ($ followed by a number)
-        result->push_back(Binding << var << term);
+        if (contains_multiple_outputs(term))
+        {
+          results.back() / Terms << err(
+            term,
+            "complete rules must not produce multiple outputs",
+            EvalConflictError);
+        }
+        else if (starts_with(name, "value$"))
+        {
+          results.back() / Terms << term;
+        }
+        else if (name.find('$') == std::string::npos || name[0] == '$')
+        {
+          results.back() / Bindings << (Binding << var << term);
+        }
       }
     }
 
-    return result;
+    if (
+      results.size() == 1 && (results.back() / Terms)->empty() &&
+      (results.back() / Bindings)->empty())
+    {
+      results.pop_back();
+    }
+
+    return results;
   }
 
   Node Resolver::membership(
@@ -1489,7 +1732,7 @@ namespace rego
       pos = path.find('.', start);
     }
 
-    if (current->type() != Object)
+    if (!(current == Object || current == DynamicObject))
     {
       logging::Warn() << "Conflict: cannot merge partials into non-object: "
                       << TermStr(current);
diff --git a/src/unifier.cc b/src/unifier.cc
index 7bdabe77..6502304e 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -7,6 +7,7 @@ namespace rego
   UnifierDef::UnifierDef(
     const Location& rule,
     const Node& rulebody,
+    const Location& version,
     CallStack call_stack,
     WithStack with_stack,
     BuiltIns builtins,
@@ -17,7 +18,8 @@ namespace rego
     m_builtins(builtins),
     m_cache(cache),
     m_parent_type(rulebody->parent()->type()),
-    m_negate(false)
+    m_negate(false),
+    m_version(version)
   {
     logging::Debug() << "---ASSEMBLING UNIFICATION---";
     m_dependency_graph.push_back({"start", {}, 0});
@@ -276,7 +278,7 @@ namespace rego
         Values values = evaluate(lhs->location(), stmt.node / Val);
         if (values.size() == 0)
         {
-          if (m_negate && var.is_unify())
+          if (m_negate && (var.is_unify() || var.is_user_var()))
           {
             var.unify({ValueDef::create(True ^ "true")});
             logging::Debug() << "> result: " << var;
@@ -288,7 +290,7 @@ namespace rego
         }
         else
         {
-          if (m_negate && var.is_unify())
+          if (m_negate && (var.is_unify() || var.is_user_var()))
           {
             bool all_false = true;
             for (auto& value : values)
@@ -542,6 +544,10 @@ namespace rego
       {
         values.push_back(ValueDef::create(NodeDef::create(Object)));
       }
+      else if (func_name == "dynamic-object")
+      {
+        values.push_back(ValueDef::create(NodeDef::create(DynamicObject)));
+      }
       else if (func_name == "set")
       {
         values.push_back(ValueDef::create(NodeDef::create(Set)));
@@ -828,7 +834,7 @@ namespace rego
         {
           Resolver::flatten_terms_into(termset_value->node(), argseq);
         }
-        values.push_back(ValueDef::create(var, Resolver::set(argseq)));
+        values.push_back(ValueDef::create(var, Resolver::set(argseq, false)));
       }
       else if (func_name == "object-compr")
       {
@@ -886,10 +892,130 @@ namespace rego
       peek_type == RuleSet || peek_type == RuleObj || peek_type == RuleComp ||
       peek_type == Submodule)
     {
-      auto maybe_node = resolve_rule(defs);
-      if (maybe_node)
+      // TODO
+      // if a prefix of this rule is a ruleobj, then we need to resolve that
+      // instead and look this up within it. However, the current implementation
+      // was built on assumptions from the pre-varrefhead era (0.55) and so
+      // there really isn't an elegant way of doing this. Instead, we have to
+      // walk the reference, find the first prefix which is a RuleObj (i.e. a
+      // varref) and then resolve that and look this up within it instead of
+      // just resolving the rule as originally designed. When the eventual
+      // refactor of unify() is done, this should be taken into consideration
+      // and made as efficient as possible, probably via a trie.
+      auto scope = defs.front()->scope();
+      Location loc = node->location();
+      std::string_view var_str = loc.view();
+      size_t end = var_str.find_first_of('.');
+      Node value;
+      while (end != std::string::npos)
       {
-        values.push_back(ValueDef::create(*maybe_node));
+        loc.len = end;
+        Nodes prefix_defs = scope->lookdown(loc);
+        if (std::find_if(prefix_defs.begin(), prefix_defs.end(), [](auto& def) {
+              if (def == RuleObj)
+              {
+                return def / IsVarRef == True;
+              }
+              else
+              {
+                return false;
+              }
+            }) != prefix_defs.end())
+        {
+          auto maybe_node = resolve_rule(prefix_defs);
+          if (maybe_node)
+          {
+            value = maybe_node.value();
+            break;
+          }
+        }
+
+        end = var_str.find_first_of('.', end + 1);
+      }
+
+      if (value)
+      {
+        // We found a RuleObj, so we have to do this the hard way
+        auto maybe_object = unwrap(value, DynamicObject);
+        if (!maybe_object.success)
+        {
+          values.push_back(ValueDef::create(err(value, "Expected object")));
+          return values;
+        }
+
+        Node object = maybe_object.node;
+
+        // look down into the object
+        size_t start = end + 1;
+        end = var_str.find_first_of('.', start);
+        while (end != std::string::npos)
+        {
+          // Oh yay, multiple nesting levels.
+          std::string key = add_quotes(var_str.substr(start, end - start));
+          defs = Resolver::object_lookdown(object, key);
+          if (defs.empty())
+          {
+            object = nullptr;
+            break;
+          }
+          else if (defs.size() > 1)
+          {
+            logging::Warn() << "Multiple definitions found for "
+                            << var_str.substr(start, end - start);
+            object = nullptr;
+            break;
+          }
+          else
+          {
+            value = defs.front();
+          }
+
+          maybe_object = unwrap(value, DynamicObject);
+          if (!maybe_object.success)
+          {
+            values.push_back(ValueDef::create(err(value, "Expected object")));
+            return values;
+          }
+
+          object = maybe_object.node;
+          start = end + 1;
+          end = var_str.find_first_of('.', start);
+        }
+
+        if (object)
+        {
+          // look up the final key
+          std::string key = add_quotes(var_str.substr(start));
+          defs = Resolver::object_lookdown(object, key);
+          if (defs.empty())
+          {
+            value = nullptr;
+          }
+          else if (defs.size() > 1)
+          {
+            logging::Warn()
+              << "Multiple definitions found for " << var_str.substr(start);
+            value = nullptr;
+          }
+          else
+          {
+            value = defs.front();
+          }
+        }
+      }
+      else
+      {
+        // the old path
+        auto maybe_node = resolve_rule(defs);
+        if (maybe_node)
+        {
+          value = maybe_node.value();
+        }
+      }
+
+      if (value)
+      {
+        values.push_back(ValueDef::create(value));
       }
     }
     else
@@ -1066,7 +1192,7 @@ namespace rego
       return ValueDef::create(var, result, sources);
     }
 
-    if (func_name == "object")
+    if (func_name == "object" || func_name == "dynamic-object")
     {
       Node object_items = NodeDef::create(ArgSeq);
       for (auto& arg : args)
@@ -1079,7 +1205,9 @@ namespace rego
         object_items->push_back(arg->node()->clone());
       }
       return ValueDef::create(
-        var, Resolver::object(object_items, false), sources);
+        var,
+        Resolver::object(object_items, func_name == "dynamic-object"),
+        sources);
     }
 
     if (func_name == "array")
@@ -1097,7 +1225,7 @@ namespace rego
       return ValueDef::create(var, Resolver::array(array_members), sources);
     }
 
-    if (func_name == "set")
+    if (func_name == "set" || func_name == "dynamic-set")
     {
       Node set_members = NodeDef::create(ArgSeq);
       for (auto& arg : args)
@@ -1109,7 +1237,8 @@ namespace rego
 
         set_members->push_back(arg->node()->clone());
       }
-      return ValueDef::create(var, Resolver::set(set_members), sources);
+      return ValueDef::create(
+        var, Resolver::set(set_members, func_name == "dynamic-set"), sources);
     }
 
     return std::nullopt;
@@ -1134,7 +1263,8 @@ namespace rego
 
     if (m_builtins->is_builtin(function->location()))
     {
-      Node node = m_builtins->call(function->location(), function_args);
+      Node node =
+        m_builtins->call(function->location(), m_version, function_args);
       return ValueDef::create(var, node, sources);
     }
 
@@ -1179,7 +1309,7 @@ namespace rego
         }
       }
 
-      if (container->type() == Object)
+      if (container == Object || container == DynamicObject)
       {
         for (const Node& object_item : *container)
         {
@@ -1190,7 +1320,7 @@ namespace rego
         }
       }
 
-      if (container->type() == Set)
+      if (container == Set || container == DynamicSet)
       {
         for (const Node& value : *container)
         {
@@ -1364,7 +1494,7 @@ namespace rego
         object = object->front();
       }
 
-      if (object->type() != Object)
+      if (!(object == Object || object == DynamicObject))
       {
         logging::Debug() << "Replacing with object (cannot merge partials)";
         object = NodeDef::create(Object);
@@ -1372,7 +1502,7 @@ namespace rego
     }
     else
     {
-      object = NodeDef::create(Object);
+      object = NodeDef::create(DynamicObject);
     }
 
     for (auto& [key, val] : partials)
@@ -1439,7 +1569,7 @@ namespace rego
     Location prefix = (dataitem / Key)->location();
     std::size_t prefix_len = prefix.len;
 
-    Node object = NodeDef::create(Object);
+    Node object = NodeDef::create(DynamicObject);
     std::map<Location, Nodes> rule_nodes;
     for (auto& rule : *module)
     {
@@ -1549,7 +1679,7 @@ namespace rego
       return resolve_ruleset(rules_by_type[RuleSet]);
     }
 
-    Node object = NodeDef::create(Object);
+    Node object = NodeDef::create(DynamicObject);
     if (contains(rules_by_type, RuleObj))
     {
       auto maybe_node = resolve_ruleobj(rules_by_type[RuleObj]);
@@ -1574,7 +1704,11 @@ namespace rego
         }
 
         mod_object = mod_object->front();
-        object->insert(object->end(), mod_object->begin(), mod_object->end());
+        object = Resolver::merge_objects(object, mod_object, false);
+        if (object->type() == Error)
+        {
+          return object;
+        }
       }
     }
 
@@ -1595,6 +1729,7 @@ namespace rego
 
       Location rulekey = (rulecomp / Key)->location();
       Location rulename = (rulecomp / Var)->location();
+      Location version = (rulecomp / Version)->location();
       Node rulebody = rulecomp / Body;
       Node value = rulecomp / Val;
       rank_t index = ValueDef::get_rank(rulecomp / Idx);
@@ -1608,10 +1743,12 @@ namespace rego
       {
         try
         {
-          body_result =
-            rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
-              ->unify();
+          body_result = rule_unifier(
+                          UnifierKey{rulekey, UnifierType::RuleBody},
+                          rulename,
+                          version,
+                          rulebody)
+                          ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1634,7 +1771,10 @@ namespace rego
           try
           {
             Unifier unifier = rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
+              UnifierKey{rulekey, UnifierType::RuleValue},
+              rulename,
+              version,
+              value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node binding_val;
@@ -1722,6 +1862,7 @@ namespace rego
 
     Location rulekey = (rule / Key)->location();
     Location rulename = (rule / Var)->location();
+    Location version = (rule / Version)->location();
     Node rulebody = rule / Body;
     Node body_result;
 
@@ -1733,10 +1874,12 @@ namespace rego
     {
       try
       {
-        body_result =
-          rule_unifier(
-            UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
-            ->unify();
+        body_result = rule_unifier(
+                        UnifierKey{rulekey, UnifierType::RuleBody},
+                        rulename,
+                        version,
+                        rulebody)
+                        ->unify();
       }
       catch (const std::exception& e)
       {
@@ -1765,7 +1908,10 @@ namespace rego
       try
       {
         Unifier unifier = rule_unifier(
-          UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
+          UnifierKey{rulekey, UnifierType::RuleValue},
+          rulename,
+          version,
+          value);
         unifier->unify();
         auto bindings = unifier->bindings();
         for (auto& binding : bindings)
@@ -1801,6 +1947,7 @@ namespace rego
 
       Location rulekey = (rule / Key)->location();
       Location rulename = (rule / Var)->location();
+      Location version = (rule / Version)->location();
       Node rulebody = rule / Body;
       Node value = rule / Val;
 
@@ -1814,10 +1961,12 @@ namespace rego
       {
         try
         {
-          body_result =
-            rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
-              ->unify();
+          body_result = rule_unifier(
+                          UnifierKey{rulekey, UnifierType::RuleBody},
+                          rulename,
+                          version,
+                          rulebody)
+                          ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1840,7 +1989,10 @@ namespace rego
           try
           {
             Unifier unifier = rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
+              UnifierKey{rulekey, UnifierType::RuleValue},
+              rulename,
+              version,
+              value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node result;
@@ -1883,7 +2035,7 @@ namespace rego
       return NodeDef::create(Set);
     }
 
-    return Resolver::set(argseq);
+    return Resolver::set(argseq, true);
   }
 
   std::optional<Node> UnifierDef::resolve_ruleobj(const Nodes& ruleobj) const
@@ -1904,6 +2056,7 @@ namespace rego
 
       Location rulekey = (rule / Key)->location();
       Location rulename = (rule / Var)->location();
+      Location version = (rule / Version)->location();
       Node rulebody = rule / Body;
       Node value = rule / Val;
 
@@ -1917,10 +2070,12 @@ namespace rego
       {
         try
         {
-          body_result =
-            rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleBody}, rulename, rulebody)
-              ->unify();
+          body_result = rule_unifier(
+                          UnifierKey{rulekey, UnifierType::RuleBody},
+                          rulename,
+                          version,
+                          rulebody)
+                          ->unify();
         }
         catch (const std::exception& e)
         {
@@ -1943,7 +2098,10 @@ namespace rego
           try
           {
             Unifier unifier = rule_unifier(
-              UnifierKey{rulekey, UnifierType::RuleValue}, rulename, value);
+              UnifierKey{rulekey, UnifierType::RuleValue},
+              rulename,
+              version,
+              value);
             unifier->unify();
             auto bindings = unifier->bindings();
             Node result;
@@ -2042,6 +2200,7 @@ namespace rego
   Unifier UnifierDef::create(
     const UnifierKey& key,
     const Location& rule,
+    const Location& version,
     const Node& rulebody,
     const CallStack& call_stack,
     const WithStack& with_stack,
@@ -2057,17 +2216,27 @@ namespace rego
     else
     {
       Unifier unifier = std::shared_ptr<UnifierDef>(new UnifierDef(
-        rule, rulebody, call_stack, with_stack, builtins, cache));
+        rule, rulebody, version, call_stack, with_stack, builtins, cache));
       cache->insert({key, unifier});
       return unifier;
     }
   }
 
   Unifier UnifierDef::rule_unifier(
-    const UnifierKey& key, const Location& rule, const Node& rulebody) const
+    const UnifierKey& key,
+    const Location& rule,
+    const Location& version,
+    const Node& rulebody) const
   {
     return create(
-      key, rule, rulebody, m_call_stack, m_with_stack, m_builtins, m_cache);
+      key,
+      rule,
+      version,
+      rulebody,
+      m_call_stack,
+      m_with_stack,
+      m_builtins,
+      m_cache);
   }
 
   bool UnifierDef::is_variable(const Location& name) const
diff --git a/src/unify.cc b/src/unify.cc
index e6fd9748..6f12a6a8 100644
--- a/src/unify.cc
+++ b/src/unify.cc
@@ -169,6 +169,7 @@ namespace rego
       {
         strings(),
         merge_data(),
+        varrefheads(),
         lift_refheads(),
         symbols(),
         replace_argvals(),
diff --git a/src/unify.hh b/src/unify.hh
index ca718e0e..393fd637 100644
--- a/src/unify.hh
+++ b/src/unify.hh
@@ -52,7 +52,7 @@ namespace rego
   inline const auto UnifyExpr = TokenDef("rego-unifyexpr");
   inline const auto UnifyExprNot = TokenDef("rego-unifyexprnot");
   inline const auto UnifyExprWith = TokenDef("rego-unifyexprwith");
-  inline const auto UnifyExprEnum = TokenDef("rego-unifyexrenum");
+  inline const auto UnifyExprEnum = TokenDef("rego-unifyexprenum");
   inline const auto UnifyExprCompr = TokenDef("rego-unifyexprcompr");
   inline const auto Compr = TokenDef("rego-compr");
   inline const auto SkipSeq = TokenDef("rego-skipseq");
@@ -60,6 +60,7 @@ namespace rego
   inline const auto BuiltInHook = TokenDef("rego-builtinhook", flag::lookup);
   inline const auto ElseSeq = TokenDef("rego-elseseq");
   inline const auto TermSet = TokenDef("rego-termset");
+  inline const auto IsVarRef = TokenDef("rego-isvarref");
 
   const std::set<Token> RuleTypes(
     {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
@@ -115,7 +116,18 @@ namespace rego
   // clang-format on
 
   // clang-format off
-  inline const auto wf_pass_lift_refheads = wf_pass_merge_data;
+  inline const auto wf_pass_varrefheads =
+    wf_pass_merge_data
+    | (Term <<= Ref | Var | Scalar | Array | DynamicObject | Object | DynamicSet | Set | Membership | ArrayCompr | ObjectCompr | SetCompr)
+    | (RuleHead <<= RuleRef * (RuleHeadType >>= (RuleHeadSet | RuleHeadObj | RuleHeadFunc | RuleHeadComp)))
+    | (RuleHeadObj <<= Expr * Expr * (IsVarRef >>= True | False))
+    | (DynamicObject <<= ObjectItem)
+    | (DynamicSet <<= Expr)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_lift_refheads = wf_pass_varrefheads;
   // clang-format on
 
   inline const auto wf_symbols_exprs =
@@ -124,12 +136,12 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_symbols =
     wf_pass_lift_refheads
-    | (Module <<= Package * Policy)
+    | (Module <<= Package * Version * Policy)
     | (Policy <<= (Import | RuleComp | RuleFunc | RuleSet | RuleObj)++)
-    | (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 >>= Expr | Term) * (Idx >>= Int))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | Term) * (Val >>= Expr | Term) * (Idx >>= Int))[Var]
+    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Version * (Idx >>= Int))[Var]
+    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Version * (Idx >>= Int))[Var]
+    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= Expr | Term) * Version)[Var]
+    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | Term) * (Val >>= Expr | Term) * (IsVarRef >>= True | False) * Version)[Var]
     | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum)++[1])
     | (Local <<= Var * (Val >>= Undefined))[Var]
     | (Literal <<= Expr | NotExpr | SomeDecl)
@@ -138,15 +150,16 @@ namespace rego
     | (With <<= RuleRef * Expr)
     | (ExprCall <<= RuleRef * ExprSeq)
     | (Query <<= (Body >>= UnifyBody))
-    | (Term <<= Scalar | Array | Object | Set | ArrayCompr | SetCompr | ObjectCompr | Membership)
+    | (Term <<= Scalar | Array | DynamicObject | Object | DynamicSet | Set | ArrayCompr | SetCompr | ObjectCompr | Membership)
     | (ObjectCompr <<= Expr * Expr * (Body >>= NestedBody))
     | (ArrayCompr <<= Expr * (Body >>= NestedBody))
     | (SetCompr <<= Expr * (Body >>= NestedBody))
     | (RefTerm <<= Ref | Var)
     | (NumTerm <<= Int | Float)
-    | (RefArgBrack <<= RefTerm | Scalar | Object | Array | Set | Expr)
+    | (RefArgBrack <<= RefTerm | NumTerm | Scalar | Object | Array | Set | Expr)
     | (Expr <<= wf_symbols_exprs)
     | (NestedBody <<= Key * (Val >>= UnifyBody))
+    | (Skip <<= Key * (Val >>= Undefined))[Key]
     ;
   // clang-format on
 
@@ -172,10 +185,10 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_constants =
     wf_pass_expand_imports
-    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | DataTerm) * (Idx >>= Int))[Var]
-    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | DataTerm) * (Idx >>= Int))[Var]
-    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= Expr | DataTerm) * (Idx >>= Int))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | DataTerm) * (Val >>= Expr | DataTerm) * (Idx >>= Int))[Var]
+    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | DataTerm) * Version * (Idx >>= Int))[Var]
+    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | DataTerm) * Version * (Idx >>= Int))[Var]
+    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= Expr | DataTerm) * Version)[Var]
+    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | DataTerm) * (Val >>= Expr | DataTerm) * (IsVarRef >>= True | False) * Version)[Var]
     ;
   // clang-format on
 
@@ -190,8 +203,8 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_rules_to_compr =
     wf_pass_locals
-    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= (UnifyBody | DataTerm)))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= (UnifyBody | DataTerm)))[Var]
+    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= (UnifyBody | DataTerm)) * Version)[Var]
+    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= (UnifyBody | DataTerm)) * (IsVarRef >>= True | False) * Version)[Var]
     ;
   // clang-format on
 
@@ -219,7 +232,6 @@ namespace rego
   inline const auto wf_pass_datarule = 
     wf_pass_merge_modules
     | (DataModule <<= (RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
-    | (Rego <<= Query * Input * Data)
     ;
   // clang-format on
 
@@ -332,17 +344,20 @@ namespace rego
     | (Set <<= Term++)
     | (Object <<= ObjectItem++)
     | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
-    | (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]
+    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Version * (Idx >>= Int) * Key)[Var]
+    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Version * (Idx >>= Int) * Key)[Var]
+    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Version * Key)[Var]
+    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (IsVarRef >>= True | False) * Version * Key)[Var]
     ;
   // clang-format on
 
   // clang-format off
   inline const auto wf_pass_unify =
     wf_pass_functions
-    | (Query <<= (Term | Binding)++)
+    | (Query <<= Result++)
+    | (Result <<= Terms * Bindings)
+    | (Terms <<= Term++)
+    | (Bindings <<= Binding++)
     | (Binding <<= Var * Term)[Var]
     | (Term <<= Scalar | Array | Object | Set)
     ;
@@ -350,6 +365,7 @@ namespace rego
 
   PassDef strings();
   PassDef merge_data();
+  PassDef varrefheads();
   PassDef lift_refheads();
   PassDef symbols();
   PassDef replace_argvals();
@@ -563,6 +579,7 @@ namespace rego
     UnifierDef(
       const Location& rule,
       const Node& rulebody,
+      const Location& version,
       CallStack call_stack,
       WithStack with_stack,
       BuiltIns builtins,
@@ -575,6 +592,7 @@ namespace rego
     static Unifier create(
       const UnifierKey& key,
       const Location& rule,
+      const Location& version,
       const Node& rulebody,
       const CallStack& call_stack,
       const WithStack& with_stack,
@@ -603,7 +621,10 @@ namespace rego
 
   private:
     Unifier rule_unifier(
-      const UnifierKey& key, const Location& rule, const Node& rulebody) const;
+      const UnifierKey& key,
+      const Location& rule,
+      const Location& version,
+      const Node& rulebody) const;
     void init_from_body(
       const Node& rulebody,
       std::vector<Statement>& statements,
@@ -669,6 +690,7 @@ namespace rego
     Token m_parent_type;
     std::vector<Dependency> m_dependency_graph;
     bool m_negate;
+    Location m_version;
   };
 
   Node expr_infix(Token op_token, Node lhs, Node rhs);
diff --git a/src/unify/compr.cc b/src/unify/compr.cc
index b9d663fa..ea73c49c 100644
--- a/src/unify/compr.cc
+++ b/src/unify/compr.cc
@@ -13,14 +13,15 @@ namespace rego
         In(Policy) *
             (T(RuleSet)
              << (T(Var)[Var] * (T(UnifyBody) / T(Empty))[Body] *
-                 T(DataTerm)[Val])) >>
+                 T(DataTerm)[Val] * T(Version)[Version])) >>
           [](Match& _) {
             ACTION();
             return RuleSet << _(Var) << _(Body)
-                           << (DataTerm << (DataSet << _[Val]));
+                           << (DataTerm << (DataSet << _[Val])) << _(Version);
           },
 
-        In(Policy) * T(RuleSet) << (T(Var)[Var] * T(Empty) * T(Expr)[Val]) >>
+        In(Policy) * T(RuleSet)
+            << (T(Var)[Var] * T(Empty) * T(Expr)[Val] * T(Version)[Version]) >>
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
@@ -31,11 +32,13 @@ namespace rego
                                    << (Expr << expr_infix(
                                          Unify,
                                          RefTerm << (Var ^ value),
-                                         Term << (Set << _(Val))))));
+                                         Term << (Set << _(Val))))))
+                           << _(Version);
           },
 
         In(Policy) * T(RuleSet)
-            << (T(Var)[Var] * T(UnifyBody)[Body] * T(Expr)[Val]) >>
+            << (T(Var)[Var] * T(UnifyBody)[Body] * T(Expr)[Val] *
+                T(Version)[Version]) >>
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
@@ -48,12 +51,14 @@ namespace rego
                                 << (Expr << expr_infix(
                                       Unify,
                                       RefTerm << (Var ^ value),
-                                      Term << (SetCompr << _(Val) << body)))));
+                                      Term << (SetCompr << _(Val) << body)))))
+              << _(Version);
           },
 
         In(Policy) *
             (T(RuleObj)
-             << (T(Var)[Var] * T(Empty) * T(Expr)[Key] * T(Expr)[Val])) >>
+             << (T(Var)[Var] * T(Empty) * T(Expr)[Key] * T(Expr)[Val] *
+                 T(True, False)[IsVarRef] * T(Version)[Version])) >>
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
@@ -74,25 +79,29 @@ namespace rego
                               << (Object
                                   << (ObjectItem
                                       << (Expr << (RefTerm << (Var ^ key)))
-                                      << _(Val)))))));
+                                      << _(Val)))))))
+              << _(IsVarRef) << _(Version);
           },
 
         In(Policy) *
             (T(RuleObj)
              << (T(Var)[Var] * (T(UnifyBody) / T(Empty))[Body] *
-                 (T(DataTerm)[Key]) * T(DataTerm)[Val])) >>
+                 T(DataTerm)[Key] * T(DataTerm)[Val] *
+                 T(True, False)[IsVarRef] * T(Version)[Version])) >>
           [](Match& _) {
             ACTION();
             return RuleObj << _(Var) << _(Body)
                            << (DataTerm
                                << (DataObject
-                                   << (DataObjectItem << _(Key) << _(Val))));
+                                   << (DataObjectItem << _(Key) << _(Val))))
+                           << _(IsVarRef) << _(Version);
           },
 
         In(Policy) *
             (T(RuleObj)
              << (T(Var)[Var] * T(UnifyBody)[Body] * T(Expr)[Key] *
-                 T(Expr)[Val])) >>
+                 T(Expr)[Val] * T(True, False)[IsVarRef] *
+                 T(Version)[Version])) >>
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
@@ -115,7 +124,8 @@ namespace rego
                                          RefTerm << (Var ^ value),
                                          Term
                                            << (ObjectCompr << _(Key) << _(Val)
-                                                           << body)))));
+                                                           << body)))))
+                           << _(IsVarRef) << _(Version);
           },
 
         // errors
diff --git a/src/unify/constants.cc b/src/unify/constants.cc
index c2b3b13d..418aafa8 100644
--- a/src/unify/constants.cc
+++ b/src/unify/constants.cc
@@ -152,10 +152,11 @@ namespace rego
           [](Match& _) {
             ACTION();
             Node pkg = _(Lhs) / Package;
+            Node version = _(Lhs) / Version;
             Node policy = _(Lhs) / Policy;
             Node rhs_policy = _(Rhs) / Policy;
             policy->push_back({rhs_policy->begin(), rhs_policy->end()});
-            return Module << pkg << policy;
+            return Module << pkg << version << policy;
           },
 
         // errors
diff --git a/src/unify/functions.cc b/src/unify/functions.cc
index e5792665..88c74fa2 100644
--- a/src/unify/functions.cc
+++ b/src/unify/functions.cc
@@ -142,6 +142,16 @@ namespace
       return Function << (JSONString ^ "set") << argseq;
     }
 
+    if (node == DynamicSet)
+    {
+      Node argseq = NodeDef::create(ArgSeq);
+      for (auto& child : *node)
+      {
+        argseq << unwrap_node(child);
+      }
+      return Function << (JSONString ^ "dynamic-set") << argseq;
+    }
+
     if (node == Object)
     {
       if (is_constant(node))
@@ -157,6 +167,16 @@ namespace
       return Function << (JSONString ^ "object") << argseq;
     }
 
+    if (node == DynamicObject)
+    {
+      Node argseq = NodeDef::create(ArgSeq);
+      for (auto& child : *node)
+      {
+        argseq << unwrap_node(child / Key) << unwrap_node(child / Val);
+      }
+      return Function << (JSONString ^ "dynamic-object") << argseq;
+    }
+
     return err(term, "Invalid term");
   }
 
diff --git a/src/unify/lift_query.cc b/src/unify/lift_query.cc
index 12567a72..0b3e17b1 100644
--- a/src/unify/lift_query.cc
+++ b/src/unify/lift_query.cc
@@ -19,8 +19,10 @@ namespace rego
          Node module =
            Module << (Package
                       << (Ref << (RefHead << (Var ^ querymodule)) << RefArgSeq))
+                  << (Version ^ DefaultVersion)
                   << (Policy
                       << (RuleComp << (Var ^ queryrule) << Empty << _(Query)
+                                   << (Version ^ DefaultVersion)
                                    << (Int ^ "0")));
          std::ostringstream oss;
          oss << "data." << querymodule.view() << "." << queryrule.view();
diff --git a/src/unify/lift_refheads.cc b/src/unify/lift_refheads.cc
index 9feb81ef..640dd005 100644
--- a/src/unify/lift_refheads.cc
+++ b/src/unify/lift_refheads.cc
@@ -4,7 +4,205 @@ namespace
 {
   using namespace rego;
 
-  using RefHeads = std::shared_ptr<std::set<std::string>>;
+  class RefTree
+  {
+  public:
+    RefTree() : root{"", {}} {}
+
+    void insert(Node& ref)
+    {
+      assert(ref == Ref);
+      Node refhead = ref / RefHead;
+      Node refargseq = ref / RefArgSeq;
+
+      Node var = refhead->front();
+      assert(var == Var);
+      RefNode* current = root.at(var->location().view());
+      for (auto& refarg : *refargseq)
+      {
+        current = current->at(
+          refarg == RefArgDot ? refarg->front()->location().view() :
+                                refargbrack_string(refarg));
+      }
+    }
+
+    size_t max_prefix(Node& ref) const
+    {
+      assert(ref == Ref);
+      Node refhead = ref / RefHead;
+      Node refargseq = ref / RefArgSeq;
+
+      Node var = refhead->front();
+      assert(var == Var);
+      if (!root.contains(var->location().view()))
+      {
+        return 0;
+      }
+
+      const RefNode* current = root.at(var->location().view());
+      size_t prefix = 0;
+      for (auto& refarg : *refargseq)
+      {
+        if (refarg == RefArgDot)
+        {
+          if (current->contains(refarg->front()->location().view()))
+          {
+            current = current->at(refarg->front()->location().view());
+            prefix++;
+          }
+          else
+          {
+            break;
+          }
+        }
+        else if (refarg == RefArgBrack)
+        {
+          std::string key = refargbrack_string(refarg);
+          if (current->contains(key))
+          {
+            current = current->at(key);
+            prefix++;
+          }
+          else
+          {
+            break;
+          }
+        }
+      }
+
+      return prefix;
+    }
+
+    void clear()
+    {
+      root.children.clear();
+    }
+
+    size_t size() const
+    {
+      size_t size = 0;
+      std::vector<const RefNode*> stack{&root};
+      while (!stack.empty())
+      {
+        const RefNode* current = stack.back();
+        stack.pop_back();
+        size += current->children.size();
+        for (auto& child : current->children)
+        {
+          stack.push_back(&child.second);
+        }
+      }
+
+      return size;
+    }
+
+    void dump(std::ostream& os) const
+    {
+      std::vector<std::pair<const RefNode*, std::string>> stack{
+        {&this->root, ""}};
+      while (!stack.empty())
+      {
+        auto [current, indent] = stack.back();
+        stack.pop_back();
+        if (current == nullptr)
+        {
+          os << indent << ')' << std::endl;
+          continue;
+        }
+
+        os << indent << '(' << current->arg;
+        if (current->children.empty())
+        {
+          os << ')' << std::endl;
+          continue;
+        }
+
+        os << std::endl;
+        stack.push_back({nullptr, indent});
+        for (auto& child : current->children)
+        {
+          stack.push_back({&child.second, indent + "  "});
+        }
+      }
+    }
+
+  private:
+    static std::string refargbrack_string(const Node& arg)
+    {
+      std::ostringstream buf;
+
+      Node index = arg->front();
+      if (index->type() == Scalar)
+      {
+        index = index->front();
+      }
+
+      Location key = index->location();
+      if (index->type() == JSONString)
+      {
+        key.pos += 1;
+        key.len -= 2;
+      }
+
+      if (all_alnum(key.view()))
+      {
+        buf << key.view();
+      }
+      else
+      {
+        buf << "[" << index->location().view() << "]";
+      }
+
+      return buf.str();
+    }
+
+    struct RefNode
+    {
+      std::string arg;
+      std::map<std::string, RefNode> children;
+
+      RefNode* at(const std::string_view& key)
+      {
+        if (!contains(key))
+        {
+          std::string key_str(key);
+          children[key_str] = RefNode{key_str, {}};
+        }
+
+        auto it =
+          std::find_if(children.begin(), children.end(), [key](auto& p) {
+            return p.first == key;
+          });
+        return &it->second;
+      }
+
+      const RefNode* at(const std::string_view& key) const
+      {
+        auto it =
+          std::find_if(children.begin(), children.end(), [key](auto& p) {
+            return p.first == key;
+          });
+        return &it->second;
+      }
+
+      bool contains(const std::string_view& key) const
+      {
+        return std::find_if(children.begin(), children.end(), [key](auto& p) {
+                 return p.first == key;
+               }) != children.end();
+      }
+    };
+
+    RefNode root;
+  };
+
+  using RefHeads = std::shared_ptr<RefTree>;
+
+  static std::ostream& operator<<(std::ostream& os, RefHeads refheads)
+  {
+    refheads->dump(os);
+    return os;
+  }
 
   void collect_refheads(const Node& node, RefHeads refheads)
   {
@@ -31,7 +229,7 @@ namespace
         return;
       }
 
-      refheads->insert(flatten_ref(full_ref));
+      refheads->insert(full_ref);
     }
     else
     {
@@ -42,25 +240,7 @@ namespace
     }
   }
 
-  bool starts_with(const std::string& ref, RefHeads refheads)
-  {
-    for (auto& refhead : *refheads)
-    {
-      if (rego::starts_with(ref, refhead))
-      {
-        if (ref.size() > refhead.size())
-        {
-          return ref[refhead.size()] == '.';
-        }
-
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  void prepend_refs(const Node& node, Node& prefix_ref, RefHeads refheads)
+  void prepend_refs(const Node& node, Node& prefix_ref, const RefHeads refheads)
   {
     if (node == Term || node == ExprCall)
     {
@@ -79,8 +259,9 @@ namespace
           return;
         }
 
-        std::string new_ref_str = flatten_ref(new_ref);
-        if (starts_with(new_ref_str, refheads))
+        size_t prefix = refheads->max_prefix(prefix_ref);
+        size_t full = refheads->max_prefix(new_ref);
+        if (full > prefix)
         {
           node->replace(ref, new_ref);
         }
@@ -109,7 +290,7 @@ namespace rego
   // rule foo.
   PassDef lift_refheads()
   {
-    RefHeads refheads = std::make_shared<std::set<std::string>>();
+    RefHeads refheads = std::make_shared<RefTree>();
 
     PassDef lift_refheads{
       "lift_refheads",
@@ -138,6 +319,7 @@ namespace rego
             Node module = _(Rule)->parent()->parent()->shared_from_this();
             Node imports = (module / ImportSeq)->clone();
             Node package_ref = (module / Package)->front();
+            Node version = (module / Version)->clone();
             Node rulehead = _(Rule) / RuleHead;
             Node ruleref = rulehead / RuleRef;
             Node ref = ruleref->front();
@@ -175,8 +357,8 @@ namespace rego
             rulehead->replace(ruleref, RuleRef << var);
 
             return Lift << ModuleSeq
-                        << (Module << (Package << new_package_ref) << imports
-                                   << (Policy << _(Rule)));
+                        << (Module << (Package << new_package_ref) << version
+                                   << imports << (Policy << _(Rule)));
           },
       }};
 
@@ -186,6 +368,8 @@ namespace rego
         collect_refheads(node, refheads);
       }
 
+      logging::Trace() << "RefHeads: " << refheads;
+
       return 0;
     });
 
diff --git a/src/unify/lift_to_rule.cc b/src/unify/lift_to_rule.cc
index c2505abd..8565ac8b 100644
--- a/src/unify/lift_to_rule.cc
+++ b/src/unify/lift_to_rule.cc
@@ -135,6 +135,27 @@ namespace
       }
     }
   }
+
+  Node get_version(NodeDef* node)
+  {
+    if (node == Top)
+    {
+      return nullptr;
+    }
+
+    if (node->in({Module, RuleComp, RuleFunc, RuleObj, RuleSet}))
+    {
+      auto it = node->find_first(Version, node->begin());
+      if (it != node->end())
+      {
+        return (*it)->clone();
+      }
+
+      return err(node->shared_from_this(), "Missing version");
+    }
+
+    return get_version(node->parent());
+  }
 }
 
 namespace rego
@@ -154,6 +175,7 @@ namespace rego
                  T(UnifyBody)[UnifyBody])) >>
           [](Match& _) {
             ACTION();
+            Node version = get_version(_(UnifyBody)->parent());
             Node rulebody = _(UnifyBody);
             // in vars
             Locs invars;
@@ -183,7 +205,7 @@ namespace rego
                 // we don't want return values to be passed in as arguments.
                 // i.e. we implicitly disable an in/out pattern, as the
                 // values which are are returned from this function will
-                // be merged with the varibles in the outer unification via
+                // be merged with the variables in the outer unification via
                 // a different mechanism.
                 invars.erase(var);
               }
@@ -227,7 +249,7 @@ namespace rego
             Node result = Seq
               << (Lift << DataModule
                        << (RuleFunc << rulename << ruleargs << rulebody
-                                    << rulevalue << (Int ^ "0")))
+                                    << rulevalue << version << (Int ^ "0")))
               << (UnifyExpr
                   << _(Item)
                   << (Expr << (Enumerate << (Expr << (RefTerm << _(ItemSeq))))))
@@ -256,6 +278,7 @@ namespace rego
                  (T(NestedBody) << (T(Key)[Key] * T(UnifyBody)[UnifyBody])))) >>
           [](Match& _) {
             ACTION();
+            Node version = get_version(_(Var)->parent());
             Node rulebody = _(UnifyBody);
             Locs invars;
             find_invars(_(UnifyBody), invars);
@@ -284,12 +307,15 @@ namespace rego
                 << (UnifyExpr
                     << (Var ^ value)
                     << (Expr << (_(Compr)->type() << _(Compr) / Var)));
-              return Seq << (Lift << DataModule
-                                  << (RuleComp << rulename << rulebody
-                                               << rulevalue << (Int ^ "0")))
-                         << (Lift << DataModule
-                                  << (RuleComp << rulename->clone() << Empty
-                                               << default_value << (Int ^ "1")))
+              return Seq << (Lift
+                             << DataModule
+                             << (RuleComp << rulename << rulebody << rulevalue
+                                          << version->clone() << (Int ^ "0")))
+                         << (Lift
+                             << DataModule
+                             << (RuleComp << rulename->clone() << Empty
+                                          << default_value << version->clone()
+                                          << (Int ^ "1")))
                          << (UnifyExpr
                              << _(Var)
                              << (Expr << (RefTerm << rulename->clone())));
@@ -315,10 +341,12 @@ namespace rego
               return Seq
                 << (Lift << DataModule
                          << (RuleFunc << rulename << ruleargs << rulebody
-                                      << rulevalue << (Int ^ "0")))
+                                      << rulevalue << version->clone()
+                                      << (Int ^ "0")))
                 << (Lift << DataModule
                          << (RuleFunc << rulename->clone() << ruleargs->clone()
-                                      << Empty << default_value << (Int ^ "1")))
+                                      << Empty << default_value
+                                      << version->clone() << (Int ^ "1")))
                 << (Local << (Var ^ partial) << Undefined)
                 << (UnifyExpr
                     << (Var ^ partial)
@@ -332,6 +360,7 @@ namespace rego
             ((T(ExprEvery) * In(DataModule)++) << T(UnifyBody)[UnifyBody]) >>
           [](Match& _) {
             ACTION();
+            Node version = get_version(_(UnifyBody)->parent());
             Node rulebody = _(UnifyBody);
             Locs invars;
             find_invars(_(UnifyBody), invars);
@@ -342,7 +371,8 @@ namespace rego
             {
               return Seq << (Lift << DataModule
                                   << (RuleComp << (Var ^ rulename) << rulebody
-                                               << rulevalue << (Int ^ "0")))
+                                               << rulevalue << version
+                                               << (Int ^ "0")))
                          << (RefTerm << (Var ^ rulename));
             }
 
@@ -356,7 +386,7 @@ namespace rego
 
             return Seq << (Lift << DataModule
                                 << (RuleFunc << (Var ^ rulename) << ruleargs
-                                             << rulebody << rulevalue
+                                             << rulebody << rulevalue << version
                                              << (Int ^ "0")))
                        << (ExprCall << (Var ^ rulename) << exprseq);
           },
diff --git a/src/unify/merge_modules.cc b/src/unify/merge_modules.cc
index 7f999ad3..75b629a2 100644
--- a/src/unify/merge_modules.cc
+++ b/src/unify/merge_modules.cc
@@ -24,7 +24,7 @@ namespace
         {
           // NB virtual doc rules are only merged if there is a name conflict
           // with a non-virtual doc rule. This seems counter-intuitive but
-          // is correct as of v0.55.0 of Rego.
+          // is correct as of v0.65.0 of Rego.
           dst->push_back(src);
         }
       }
@@ -91,7 +91,7 @@ namespace rego
                  << (T(Ref)
                      << ((T(RefHead) << T(Var)[Var]) *
                          T(RefArgSeq)[RefArgSeq]))) *
-                T(Policy)[Policy])) >>
+                T(Version) * T(Policy)[Policy])) >>
          [](Match& _) {
            ACTION();
            Node args = _(RefArgSeq);
diff --git a/src/unify/result.cc b/src/unify/result.cc
index e126a4ee..9173ff25 100644
--- a/src/unify/result.cc
+++ b/src/unify/result.cc
@@ -1,5 +1,10 @@
 #include "unify.hh"
 
+namespace
+{
+  const auto FreshVarPattern = R"([[:alnum:]|_]+\$\d+)";
+}
+
 namespace rego
 {
   // Extracts the query result from the AST.
@@ -10,12 +15,23 @@ namespace rego
       wf_result,
       dir::topdown,
       {
-        In(Top) * (T(Rego) << T(Query)[Query]) >>
+        In(Top) * (T(Rego) << (T(Query) << T(Result)++[Result])) >>
           [](Match& _) {
             ACTION();
-            return Result << *_[Query];
+            if (_[Result].empty())
+            {
+              return Undefined ^ "";
+            }
+            return Results << _[Result];
           },
 
+        T(ObjectItem)
+            << (T(Term) << (T(Scalar) << T(JSONString, FreshVarPattern))) >>
+          [](Match&) -> Node { return nullptr; },
+
+        T(Binding) << T(Var, FreshVarPattern) >>
+          [](Match&) -> Node { return nullptr; },
+
         (T(Array) / T(Set)) * T(Error)[Error] >>
           [](Match& _) {
             ACTION();
diff --git a/src/unify/rulebody.cc b/src/unify/rulebody.cc
index 0191ed64..81ea5801 100644
--- a/src/unify/rulebody.cc
+++ b/src/unify/rulebody.cc
@@ -624,6 +624,24 @@ namespace rego
             return _(UnifyExpr);
           },
 
+        In(UnifyExprNot) * (T(UnifyBody)[UnifyBody] << T(Local)) >>
+          [](Match& _) {
+            ACTION();
+            Node body = _(UnifyBody);
+            auto end = std::find_if(body->begin(), body->end(), [](Node n) {
+              return n->type() != Local;
+            });
+
+            Node seq = NodeDef::create(Seq);
+            for (auto it = body->begin(); it != end; ++it)
+            {
+              seq << (Lift << UnifyBody << *it);
+            }
+
+            body->erase(body->begin(), end);
+            return seq << body;
+          },
+
         // <compr>
         In(UnifyBody) *
             (T(UnifyExpr)
@@ -687,6 +705,9 @@ namespace rego
 
         // errors
 
+        In(UnifyExprNot) * (T(UnifyBody)[UnifyBody] << End) >>
+          [](Match& _) { return err(_(UnifyBody), "Invalid not expression"); },
+
         In(Term, Expr) * T(SetCompr)[SetCompr] >>
           [](Match& _) {
             ACTION();
diff --git a/src/unify/skips.cc b/src/unify/skips.cc
index 74c538b3..2dda1e76 100644
--- a/src/unify/skips.cc
+++ b/src/unify/skips.cc
@@ -108,7 +108,8 @@ namespace rego
             (T(DataRule) << (T(Var)[Var] * T(DataTerm)[DataTerm])) >>
           [](Match& _) {
             ACTION();
-            return RuleComp << _(Var) << Empty << _(DataTerm) << (Int ^ "0");
+            return RuleComp << _(Var) << Empty << _(DataTerm)
+                            << (Version ^ DefaultVersion) << (Int ^ "0");
           },
       }};
   }
diff --git a/src/unify/strings.cc b/src/unify/strings.cc
index 28e66e9e..27f96398 100644
--- a/src/unify/strings.cc
+++ b/src/unify/strings.cc
@@ -10,9 +10,9 @@ namespace rego
     return {
       "strings",
       wf_pass_strings,
-      dir::topdown,
+      dir::bottomup | dir::once,
       {
-        In(String) * T(RawString)[RawString] >>
+        In(Scalar) * (T(String) << T(RawString)[RawString]) >>
           [](Match& _) {
             ACTION();
             std::string raw_string =
diff --git a/src/unify/symbols.cc b/src/unify/symbols.cc
index ef83512e..fb22b53e 100644
--- a/src/unify/symbols.cc
+++ b/src/unify/symbols.cc
@@ -31,6 +31,19 @@ namespace
     }
   }
 
+  Node get_version(Node rule)
+  {
+    auto policy = rule->parent();
+    auto module = policy->parent();
+    assert(module == Module);
+    auto it = module->find_first(Version, module->begin());
+    if (it == module->end())
+    {
+      return err(module->shared_from_this(), "No version found in module");
+    }
+    return (*it)->clone();
+  }
+
   const inline auto LocalToken = T(RefTerm) / T(Term);
 }
 
@@ -83,7 +96,7 @@ namespace rego
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(False) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
@@ -92,6 +105,7 @@ namespace rego
           [](Match& _) {
             ACTION();
             std::size_t num_items = _(RuleBodySeq)->size();
+            Node version = get_version(_(Rule));
 
             Node val = _(Expr);
             if (is_constant(val->front()))
@@ -101,7 +115,8 @@ namespace rego
 
             if (num_items == 0)
             {
-              return RuleComp << _(Id) << Empty << val << (Int ^ "0");
+              return RuleComp << _(Id) << Empty << val << version
+                              << (Int ^ "0");
             }
 
             if (num_items == 1)
@@ -109,7 +124,7 @@ namespace rego
               Node item = _(RuleBodySeq)->front();
               Node body = UnifyBody ^ item;
               body->insert(body->end(), item->begin(), item->end());
-              return RuleComp << _(Id) << body << val << (Int ^ "0");
+              return RuleComp << _(Id) << body << val << version << (Int ^ "0");
             }
 
             // Else precedence is per rule chain. This means that
@@ -141,6 +156,7 @@ namespace rego
                 body->insert(body->end(), item->begin(), item->end());
                 rule_seq
                   << (RuleComp << rule_id->clone() << body << val->clone()
+                               << version->clone()
                                << (Int ^ std::to_string(i)));
               }
               else if (item == Else)
@@ -152,10 +168,19 @@ namespace rego
                 }
 
                 Node query = item / Query;
-                body = UnifyBody ^ query;
-                body->insert(body->end(), query->begin(), query->end());
+                if (query->empty())
+                {
+                  body = NodeDef::create(Empty);
+                }
+                else
+                {
+                  body = UnifyBody ^ query;
+                  body->insert(body->end(), query->begin(), query->end());
+                }
+
                 rule_seq
                   << (RuleComp << rule_id->clone() << body << item_val
+                               << version->clone()
                                << (Int ^ std::to_string(i)));
               }
             }
@@ -163,14 +188,14 @@ namespace rego
             // then we create the proxy rule
             rule_seq
               << (RuleComp << _(Id) << Empty
-                           << (Expr << (Term << (Var ^ rule_id)))
+                           << (Expr << (Term << (Var ^ rule_id))) << version
                            << (Int ^ "0"));
 
             return rule_seq;
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(True) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
@@ -179,12 +204,12 @@ namespace rego
           [](Match& _) {
             ACTION();
             std::size_t rank = std::numeric_limits<std::uint16_t>::max();
-            return RuleComp << _(Id) << Empty << _(Expr)
+            return RuleComp << _(Id) << Empty << _(Expr) << get_version(_(Rule))
                             << (Int ^ std::to_string(rank));
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(False) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
@@ -195,6 +220,7 @@ namespace rego
             ACTION();
             std::size_t num_items = _(RuleBodySeq)->size();
 
+            Node version = get_version(_(Rule));
             Node val = _(Expr);
             if (is_constant(val->front()))
             {
@@ -203,7 +229,7 @@ namespace rego
 
             if (num_items == 0)
             {
-              return RuleFunc << _(Id) << _(RuleArgs) << Empty << val
+              return RuleFunc << _(Id) << _(RuleArgs) << Empty << val << version
                               << (Int ^ "0");
             }
 
@@ -212,7 +238,7 @@ namespace rego
               Node item = _(RuleBodySeq)->front();
               Node body = UnifyBody ^ item;
               body->insert(body->end(), item->begin(), item->end());
-              return RuleFunc << _(Id) << _(RuleArgs) << body << val
+              return RuleFunc << _(Id) << _(RuleArgs) << body << val << version
                               << (Int ^ "0");
             }
 
@@ -231,7 +257,7 @@ namespace rego
                 body->insert(body->end(), item->begin(), item->end());
                 rule_seq
                   << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
-                               << body << val->clone()
+                               << body << val->clone() << version->clone()
                                << (Int ^ std::to_string(i)));
               }
               else if (item == Else)
@@ -243,11 +269,19 @@ namespace rego
                 }
 
                 Node query = item / Query;
-                body = UnifyBody ^ query;
-                body->insert(body->end(), query->begin(), query->end());
+                if (query->empty())
+                {
+                  body = NodeDef::create(Empty);
+                }
+                else
+                {
+                  body = UnifyBody ^ query;
+                  body->insert(body->end(), query->begin(), query->end());
+                }
+
                 rule_seq
                   << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
-                               << body << item_val
+                               << body << item_val << version->clone()
                                << (Int ^ std::to_string(i)));
               }
               else if (item == Literal)
@@ -255,7 +289,7 @@ namespace rego
                 body = UnifyBody << item;
                 rule_seq
                   << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
-                               << body << val->clone()
+                               << body << val->clone() << version->clone()
                                << (Int ^ std::to_string(i)));
               }
             }
@@ -293,13 +327,13 @@ namespace rego
             Node proxy_val = Expr << (Term << (Var ^ out));
             rule_seq
               << (RuleFunc << _(Id) << _(RuleArgs) << proxy_body << proxy_val
-                           << (Int ^ "0"));
+                           << version << (Int ^ "0"));
 
             return rule_seq;
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(True) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
@@ -310,11 +344,12 @@ namespace rego
             ACTION();
             std::size_t rank = std::numeric_limits<std::uint16_t>::max();
             return RuleFunc << _(Id) << _(RuleArgs) << Empty << _(Expr)
+                            << get_version(_(Rule))
                             << (Int ^ std::to_string(rank));
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(False) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
@@ -324,6 +359,7 @@ namespace rego
             ACTION();
             std::size_t num_items = _(RuleBodySeq)->size();
 
+            Node version = get_version(_(Rule));
             Node val = _(Expr);
             if (is_constant(val->front()))
             {
@@ -332,50 +368,49 @@ namespace rego
 
             if (num_items == 0)
             {
-              return RuleSet << _(Id) << Empty << val << (Int ^ "0");
+              return RuleSet << _(Id) << Empty << val << version;
             }
 
-            Node rule_seq = NodeDef::create(Seq);
+            Node item = _(RuleBodySeq)->front();
 
-            for (std::size_t i = 0; i < num_items; ++i)
+            if (item == Else)
             {
-              Node item = _(RuleBodySeq)->at(i);
-              Node body;
-              if (item == Query)
-              {
-                body = UnifyBody ^ item;
-                body->insert(body->end(), item->begin(), item->end());
-                rule_seq
-                  << (RuleSet << _(Id)->clone() << body << val->clone()
-                              << (Int ^ std::to_string(i)));
-              }
-              else if (item == Else)
-              {
-                return err(
-                  item, "else keyword cannot be used on multi-value rules");
-              }
-              else if (item == Literal)
-              {
-                body = UnifyBody << item;
-                rule_seq
-                  << (RuleSet << _(Id)->clone() << body << val->clone()
-                              << (Int ^ std::to_string(i)));
-              }
+              return err(
+                item, "else keyword cannot be used on multi-value rules");
             }
 
-            return rule_seq;
+            Node body;
+            if (item == Query)
+            {
+              body = UnifyBody ^ item;
+              body->insert(body->end(), item->begin(), item->end());
+              return RuleSet << _(Id)->clone() << body << val->clone()
+                             << version->clone();
+            }
+
+            if (item == Literal)
+            {
+              body = UnifyBody << item;
+              return RuleSet << _(Id)->clone() << body << val->clone()
+                             << version->clone();
+            }
+
+            return err(item, "Invalid rule body");
           },
 
         In(Policy) *
-            (T(Rule)
+            (T(Rule)[Rule]
              << (T(False) *
                  (T(RuleHead)
                   << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadObj) << (T(Expr)[Key] * T(Expr)[Val])))) *
+                      (T(RuleHeadObj)
+                       << (T(Expr)[Key] * T(Expr)[Val] *
+                           T(True, False)[IsVarRef])))) *
                  T(RuleBodySeq)[RuleBodySeq])) >>
           [](Match& _) {
             ACTION();
             std::size_t num_items = _(RuleBodySeq)->size();
+            Node version = get_version(_(Rule));
 
             Node key = _(Key);
             if (is_constant(key->front()))
@@ -390,38 +425,36 @@ namespace rego
 
             if (num_items == 0)
             {
-              return RuleObj << _(Id) << Empty << key << val << (Int ^ "0");
+              return RuleObj << _(Id) << Empty << key << val << _(IsVarRef)
+                             << version;
             }
 
-            Node rule_seq = NodeDef::create(Seq);
+            Node item = _(RuleBodySeq)->front();
+            Node body;
+            if (item == Else)
+            {
+              return err(
+                item, "else keyword cannot be used on multi-value rules");
+            }
 
-            for (std::size_t i = 0; i < num_items; ++i)
+            if (item == Query)
             {
-              Node item = _(RuleBodySeq)->at(i);
-              Node body;
-              if (item == Query)
-              {
-                body = UnifyBody ^ item;
-                body->insert(body->end(), item->begin(), item->end());
-                rule_seq
-                  << (RuleObj << _(Id)->clone() << body << key->clone()
-                              << val->clone() << (Int ^ std::to_string(i)));
-              }
-              else if (item == Else)
-              {
-                return err(
-                  item, "else keyword cannot be used on multi-value rules");
-              }
-              else if (item == Literal)
-              {
-                body = UnifyBody << item;
-                rule_seq
-                  << (RuleObj << _(Id)->clone() << body << key->clone()
-                              << val->clone() << (Int ^ std::to_string(i)));
-              }
+              body = UnifyBody ^ item;
+              body->insert(body->end(), item->begin(), item->end());
+              return RuleObj << _(Id)->clone() << body << key->clone()
+                             << val->clone() << _(IsVarRef)->clone()
+                             << version->clone();
             }
 
-            return rule_seq;
+            if (item == Literal)
+            {
+              body = UnifyBody << item;
+              return RuleObj << _(Id)->clone() << body << key->clone()
+                             << val->clone() << _(IsVarRef)->clone()
+                             << version->clone();
+            }
+
+            return err(item, "Invalid rule body");
           },
 
         In(Expr) * (T(Term) << (T(Ref) / T(Var))[Val]) >>
@@ -436,16 +469,23 @@ namespace rego
             return NumTerm << _(Val);
           },
 
+        In(RefArgBrack) * (T(Expr) << T(RefTerm, NumTerm)[Term]) >>
+          [](Match& _) {
+            ACTION();
+            return _(Term);
+          },
+
         In(RefArgBrack) * T(Var)[Var] >>
           [](Match& _) {
             ACTION();
             return RefTerm << _(Var);
           },
 
-        In(RefArgBrack) * T(ExprInfix)[ExprInfix] >>
+        In(RefArgBrack) *
+            (T(Expr) << (T(Term) << T(Scalar, Object, Array, Set)[Term])) >>
           [](Match& _) {
             ACTION();
-            return Expr << _(ExprInfix);
+            return _(Term);
           },
 
         In(UnifyBody) *
@@ -646,7 +686,7 @@ namespace rego
                        << (ExprSeq
                            << (Expr
                                << (ExprCall
-                                   << (Ref << (RefHead << (Var ^ "cast_set"))
+                                   << (Ref << (RefHead << (Var ^ "cast_set_ex"))
                                            << RefArgSeq)
                                    << (ExprSeq
                                        << (Expr
@@ -784,6 +824,18 @@ namespace rego
             ACTION();
             return err(_(Term), "Invalid with term");
           },
+
+        T(Query)[Query] << End >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Query), "Invalid query");
+          },
+
+        T(Placeholder)[Placeholder] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Placeholder), "Invalid placeholder");
+          },
       }};
   }
 
diff --git a/src/unify/unify.cc b/src/unify/unify.cc
index b6e20f5d..a4d36f8e 100644
--- a/src/unify/unify.cc
+++ b/src/unify/unify.cc
@@ -18,13 +18,12 @@ namespace rego
       wf_pass_unify,
       dir::bottomup | dir::once,
       {
-        In(Query, Array, Set, ObjectItem) *
-            (ScalarToken / T(JSONString))[Scalar] >>
+        In(Array, Set, ObjectItem) * (ScalarToken / T(JSONString))[Scalar] >>
           [](Match& _) {
             ACTION();
             return Term << (Scalar << _(Scalar));
           },
-        In(Query, Array, Set, ObjectItem) *
+        In(Array, Set, ObjectItem) *
             (T(Object) / T(Array) / T(Set) / T(Scalar))[Term] >>
           [](Match& _) {
             ACTION();
@@ -50,6 +49,26 @@ namespace rego
             ACTION();
             return Object << *_[DataObject];
           },
+        In(ObjectItem) * T(DynamicObject)[DynamicObject] >>
+          [](Match& _) {
+            ACTION();
+            return Term << (Object << *_[DynamicObject]);
+          },
+        In(Term) * T(DynamicObject)[DynamicObject] >>
+          [](Match& _) {
+            ACTION();
+            return Object << *_[DynamicObject];
+          },
+        In(ObjectItem) * T(DynamicSet)[DynamicSet] >>
+          [](Match& _) {
+            ACTION();
+            return Term << (Set << *_[DynamicSet]);
+          },
+        In(Term) * T(DynamicSet)[DynamicSet] >>
+          [](Match& _) {
+            ACTION();
+            return Set << *_[DynamicSet];
+          },
       }};
 
     unify.pre(Rego, [builtins](Node node) {
@@ -59,8 +78,8 @@ namespace rego
       Node query = node / Query;
       try
       {
-        Node result = Resolver::resolve_query(query, builtins);
-        node->replace(query, result);
+        Nodes results = Resolver::resolve_query(query, builtins);
+        node->replace(query, Query << results);
       }
       catch (const std::exception& e)
       {
diff --git a/src/unify/varrefheads.cc b/src/unify/varrefheads.cc
new file mode 100644
index 00000000..ad7d8aa8
--- /dev/null
+++ b/src/unify/varrefheads.cc
@@ -0,0 +1,248 @@
+#include "rego.hh"
+#include "trieste/rewrite.h"
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+
+  bool refarg_is_varref(const Node& refarg)
+  {
+    if (refarg != RefArgBrack)
+    {
+      return false;
+    }
+
+    const Node& expr = refarg->front();
+    if (expr != Expr)
+    {
+      return false;
+    }
+
+    const Node& term = expr->front();
+    if (term != Term)
+    {
+      return false;
+    }
+
+    return term->front() == Var || term->front() == Ref;
+  }
+
+  Node term_to_expr(const Node& node)
+  {
+    return Expr << (Term << node);
+  }
+
+  Node refarg_to_object_key(const Node& node)
+  {
+    if (node == RefArgDot)
+    {
+      std::string key_str = add_quotes(node->front()->location().view());
+      return term_to_expr(Scalar << (JSONString ^ key_str));
+    }
+    else if (node->front() == Expr)
+    {
+      return node->front();
+    }
+    else if (node->front() == Placeholder)
+    {
+      return Expr << (Term << (Var ^ node->fresh(node->location())));
+    }
+    else
+    {
+      return err(node, "Cannot convert refarg to object key");
+    }
+  }
+}
+
+namespace rego
+{
+  bool refargs_contain_varref(const Node& node)
+  {
+    Node refargseq = node;
+    if (node == Ref)
+    {
+      refargseq = node / RefArgSeq;
+    }
+
+    bool exists =
+      std::find_if(refargseq->begin(), refargseq->end(), refarg_is_varref) !=
+      refargseq->end();
+    return exists;
+  }
+
+  // This pass finds rules which have refheads that contain variables.
+  // The strategy is to trim the refhead to the point before the variable,
+  // and then alter the query such that the object or set (as appropriate)
+  // is constructed as a comprehension.
+  PassDef varrefheads()
+  {
+    PassDef pass =
+      {
+        "varrefheads",
+        wf_pass_varrefheads,
+        dir::bottomup | dir::once,
+        {
+          In(Rule) *
+              (T(True, False)[Default] *
+               (T(RuleHead)
+                << ((T(RuleRef) << T(Ref)[Ref]([](auto& n) {
+                       return refargs_contain_varref(n.front());
+                     })) *
+                    (T(RuleHeadComp) << T(Expr)[Expr]))) *
+               (T(RuleBodySeq) << ~T(Query)[Query])) >>
+            [](Match& _) {
+              ACTION();
+              logging::Trace()
+                << "-- Found rulecomp with variable in refhead --";
+
+              Node refargseq = _(Ref) / RefArgSeq;
+
+              auto start = std::find_if(
+                refargseq->begin(), refargseq->end(), refarg_is_varref);
+
+              Node key = refarg_to_object_key(refargseq->back());
+              Node val = _(Expr);
+
+              std::vector<Node> varrefargs(start, refargseq->end() - 1);
+              while (!varrefargs.empty())
+              {
+                Node new_key = refarg_to_object_key(varrefargs.back());
+                varrefargs.pop_back();
+                val = term_to_expr(DynamicObject << (ObjectItem << key << val));
+                key = new_key;
+              }
+
+              // everything before the varref is a valid rule ref
+              refargseq->erase(start, refargseq->end());
+              Node ref = _(Ref);
+              if (refargseq->empty())
+              {
+                ref = (_(Ref) / RefHead)->front();
+                if (ref != Var)
+                {
+                  ref = err(ref, "Invalid rule head");
+                }
+              }
+
+              return Seq << _(Default)
+                         << (RuleHead
+                             << (RuleRef << ref)
+                             << (RuleHeadObj << key << val << (True ^ "true")))
+                         << (RuleBodySeq << _(Query));
+            },
+
+          In(Rule) *
+              (T(False)[Default] *
+               (T(RuleHead)
+                << ((T(RuleRef) << T(Ref)[Ref]([](auto& n) {
+                       return refargs_contain_varref(n.front());
+                     })) *
+                    (T(RuleHeadObj) << (T(Expr)[Key] * T(Expr)[Val])))) *
+               (T(RuleBodySeq) << ~T(Query)[Query])) >>
+            [](Match& _) {
+              ACTION();
+              logging::Trace()
+                << "-- Found ruleobj with variable in refhead --";
+              Node refargseq = _(Ref) / RefArgSeq;
+
+              auto start = std::find_if(
+                refargseq->begin(), refargseq->end(), refarg_is_varref);
+
+              Node key = _(Key);
+              Node val = _(Val);
+
+              std::vector<Node> varrefargs(start, refargseq->end());
+              while (!varrefargs.empty())
+              {
+                Node new_key = refarg_to_object_key(varrefargs.back());
+                varrefargs.pop_back();
+                val = term_to_expr(DynamicObject << (ObjectItem << key << val));
+                key = new_key;
+              }
+
+              // everything before the varref is a valid rule ref
+              refargseq->erase(start, refargseq->end());
+              Node ref = _(Ref);
+              if (refargseq->empty())
+              {
+                ref = (_(Ref) / RefHead)->front();
+                if (ref != Var)
+                {
+                  ref = err(ref, "Invalid rule head");
+                }
+              }
+
+              return Seq << _(Default)
+                         << (RuleHead
+                             << (RuleRef << ref)
+                             << (RuleHeadObj << key << val << (True ^ "true")))
+                         << (RuleBodySeq << _(Query));
+            },
+
+          In(Rule) *
+              (T(False)[Default] *
+               (T(RuleHead)
+                << ((T(RuleRef) << T(Ref)[Ref]([](auto& n) {
+                       return refargs_contain_varref(n.front());
+                     })) *
+                    (T(RuleHeadSet) << T(Expr)[Expr]))) *
+               (T(RuleBodySeq) << ~T(Query)[Query])) >>
+            [](Match& _) {
+              ACTION();
+              logging::Trace()
+                << "-- Found ruleset with variable in refhead --";
+              Node refargseq = _(Ref) / RefArgSeq;
+
+              auto start = std::find_if(
+                refargseq->begin(), refargseq->end(), refarg_is_varref);
+
+              Node key = refarg_to_object_key(refargseq->back());
+              Node val = term_to_expr(DynamicSet << _(Expr));
+
+              std::vector<Node> varrefargs(start, refargseq->end() - 1);
+              while (!varrefargs.empty())
+              {
+                Node new_key = refarg_to_object_key(varrefargs.back());
+                varrefargs.pop_back();
+                val = term_to_expr(DynamicObject << (ObjectItem << key << val));
+                key = new_key;
+              }
+
+              // everything before the varref is a valid rule ref
+              refargseq->erase(start, refargseq->end());
+              Node ref = _(Ref);
+              if (refargseq->empty())
+              {
+                ref = (_(Ref) / RefHead)->front();
+                if (ref != Var)
+                {
+                  ref = err(ref, "Invalid rule head");
+                }
+              }
+
+              return Seq << _(Default)
+                         << (RuleHead
+                             << (RuleRef << ref)
+                             << (RuleHeadObj << key << val << (True ^ "true")))
+                         << (RuleBodySeq << _(Query));
+            },
+
+          In(Rule) *
+              (T(True, False)[Default] *
+               (T(RuleHead)
+                << (T(RuleRef)[RuleRef] *
+                    (T(RuleHeadObj) << (T(Expr)[Key] * T(Expr)[Val])))) *
+               T(RuleBodySeq)[RuleBodySeq]) >>
+            [](Match& _) {
+              return Seq << _(Default)
+                         << (RuleHead << _(RuleRef)
+                                      << (RuleHeadObj << _(Key) << _(Val)
+                                                      << (False ^ "false")))
+                         << _(RuleBodySeq);
+            },
+        }};
+
+    return pass;
+  }
+}
\ No newline at end of file
diff --git a/src/value.cc b/src/value.cc
index ef3ab3ad..964d087e 100644
--- a/src/value.cc
+++ b/src/value.cc
@@ -5,7 +5,8 @@ namespace
   using namespace rego;
 
   std::set<Token> scalar_tokens = {Int, Float, JSONString, True, False, Null};
-  std::set<Token> term_tokens = {Scalar, Array, Object, Set, Undefined};
+  std::set<Token> term_tokens = {
+    Scalar, Array, DynamicObject, Object, DynamicSet, Set, Undefined};
 }
 
 namespace rego
diff --git a/wrappers/python/CMakeLists.txt b/wrappers/python/CMakeLists.txt
index 191e6fb6..68d71f77 100644
--- a/wrappers/python/CMakeLists.txt
+++ b/wrappers/python/CMakeLists.txt
@@ -9,22 +9,22 @@ 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})
+if (DEFINED ENV{REGOCPP_REPO})
+  set(REGOCPP_REPO $ENV{REGOCPP_REPO})
 else ()
-  set(REGOCPP_C_WRAPPER_REPO "https://github.com/microsoft/rego-cpp/")
+  set(REGOCPP_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})
+if (DEFINED ENV{REGOCPP_TAG})
+  set(REGOCPP_TAG $ENV{REGOCPP_TAG})
 else ()
-  set(REGOCPP_C_WRAPPER_TAG "main")
+  set(REGOCPP_TAG "main")
 endif()
 
 FetchContent_Declare(
   regocpp
-  GIT_REPOSITORY ${REGOCPP_C_WRAPPER_REPO}
-  GIT_TAG        ${REGOCPP_C_WRAPPER_TAG}
+  GIT_REPOSITORY ${REGOCPP_REPO}
+  GIT_TAG        ${REGOCPP_TAG}
 )
 
 FetchContent_Declare(
@@ -33,7 +33,6 @@ FetchContent_Declare(
   GIT_TAG        v2.12.0
 )
 
-set(BUILD_SHARED_LIBS ON)
 set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 
 set(SNMALLOC_ENABLE_DYNAMIC_LOADING ON)
 set(REGOCPP_USE_CXX17 ON)
@@ -45,5 +44,7 @@ set(PYBIND11_CPP_STANDARD -std=c++17)
 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 yaml PROPERTY POSITION_INDEPENDENT_CODE ON)
+set_property(TARGET json 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
index 9a9aa277..ad3cd547 100644
--- a/wrappers/python/README.md
+++ b/wrappers/python/README.md
@@ -86,8 +86,6 @@ print(rego.query("x=[data.one, input.b, data.objects.sites[1]]"))
 
 ## 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
@@ -118,7 +116,7 @@ set-compr       = "{" term "|" query "}"
 object-compr    = "{" object-item "|" query "}"
 infix-operator  = assign-operator | bool-operator | arith-operator | bin-operator
 bool-operator   = "==" | "!=" | "<" | ">" | ">=" | "<="
-arith-operator  = "+" | "-" | "*" | "/"
+arith-operator  = "+" | "-" | "*" | "/" | "%"
 bin-operator    = "&" | "|"
 assign-operator = ":=" | "="
 ref             = ( var | array | object | set | array-compr | object-compr | set-compr | expr-call ) { ref-arg }
@@ -137,6 +135,9 @@ non-empty-set   = "{" term { "," term } "}"
 empty-set       = "set(" ")"
 ```
 
+> [!NOTE]
+> This grammar corresponds to Rego with `rego.v1` enabled (See [OPA v1.0](https://www.openpolicyagent.org/docs/latest/opa-1) for more info).
+
 Definitions:
 ```
 []     optional (zero or one instances)
diff --git a/wrappers/python/src/_regopy.cc b/wrappers/python/src/_regopy.cc
index 39103a2e..a14b06a3 100644
--- a/wrappers/python/src/_regopy.cc
+++ b/wrappers/python/src/_regopy.cc
@@ -52,6 +52,10 @@ PYBIND11_MODULE(_regopy, m)
   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_TERMS") = REGO_NODE_TERMS;
+  m.attr("REGO_NODE_BINDINGS") = REGO_NODE_BINDINGS;
+  m.attr("REGO_NODE_RESULTS") = REGO_NODE_RESULTS;
+  m.attr("REGO_NODE_RESULT") = REGO_NODE_RESULT;
 
   m.attr("REGO_NODE_ERROR") = REGO_NODE_ERROR;
   m.attr("REGO_NODE_ERROR_MESSAGE") = REGO_NODE_ERROR_MESSAGE;
@@ -183,8 +187,31 @@ PYBIND11_MODULE(_regopy, m)
     &regoOutputOk,
     "Returns true if the output is ok.",
     "output"_a);
+  m.def(
+    "regoOutputSize",
+    &regoOutputSize,
+    "Returns the number of results in the output.",
+    "output"_a);
+  m.def(
+    "regoOutputExpressionsAtIndex",
+    &regoOutputExpressionsAtIndex,
+    "Returns the terms at the specified index.",
+    "output"_a,
+    "index"_a);
+  m.def(
+    "regoOutputExpressions",
+    &regoOutputExpressions,
+    "Returns the terms.",
+    "output"_a);
   m.def(
     "regoOutputNode", &regoOutputNode, "Returns the output node.", "output"_a);
+  m.def(
+    "regoOutputBindingAtIndex",
+    &regoOutputBindingAtIndex,
+    "Returns the output binding at the specified index.",
+    "output"_a,
+    "index"_a,
+    "name"_a);
   m.def(
     "regoOutputBinding",
     &regoOutputBinding,
diff --git a/wrappers/python/src/regopy/interpreter.py b/wrappers/python/src/regopy/interpreter.py
index 41f3ab78..8138e73e 100644
--- a/wrappers/python/src/regopy/interpreter.py
+++ b/wrappers/python/src/regopy/interpreter.py
@@ -45,8 +45,7 @@ class Interpreter:
         >>> 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
+        {"bindings":{"x":5, "y":9.4}}
 
         >>> input = {
         ...     "a": 10,
@@ -102,7 +101,7 @@ class Interpreter:
         >>> 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"}]
+        {"bindings":{"x":[{"bar":"Foo", "baz":5, "be":true, "bop":23.4}, "20", {"name":"smoke1"}]}}
     """
 
     def __init__(self):
@@ -138,7 +137,7 @@ def add_module(self, name: str, source: str):
             >>> rego.add_module("scalars", module)
             >>> output = rego.query("data.scalars.greeting")
             >>> print(output)
-            "Hello"
+            {"expressions":["Hello"]}
         """
         err = regoAddModule(self._impl, name, source)
         if err == REGO_ERROR:
@@ -177,7 +176,7 @@ def add_data_json(self, json: str):
             >>> rego.add_data_json(data)
             >>> output = rego.query("data.one.bar")
             >>> print(output)
-            "Foo"
+            {"expressions":["Foo"]}
         """
         err = regoAddDataJSON(self._impl, json)
         if err == REGO_ERROR:
@@ -224,7 +223,7 @@ def set_input_term(self, term: str):
             >>> rego.set_input_json(input)
             >>> output = rego.query("input.a")
             >>> print(output)
-            10
+            {"expressions":[10]}
         """
         err = regoSetInputTerm(self._impl, term)
         if err == REGO_ERROR:
@@ -256,7 +255,7 @@ def set_input_json(self, json: str):
             >>> rego.set_input_json(input)
             >>> output = rego.query("input.a")
             >>> print(output)
-            10
+            {"expressions":[10]}
         """
         err = regoSetInputTerm(self._impl, json)
         if err == REGO_ERROR:
@@ -384,15 +383,15 @@ def query(self, query: str) -> Output:
             >>> rego.set_input(input0)
             >>> output = rego.query("data.multi.a")
             >>> print(output)
-            1
+            {"expressions":[1]}
             >>> rego.set_input(input1)
             >>> output = rego.query("data.multi.a")
             >>> print(output)
-            41
+            {"expressions":[41]}
             >>> rego.set_input(input2)
             >>> output = rego.query("data.multi.a")
             >>> print(output)
-            70
+            {"expressions":[70]}
         """
         impl = regoQuery(self._impl, query)
         if impl is None:
diff --git a/wrappers/python/src/regopy/node.py b/wrappers/python/src/regopy/node.py
index 3c83d347..324db987 100644
--- a/wrappers/python/src/regopy/node.py
+++ b/wrappers/python/src/regopy/node.py
@@ -25,6 +25,10 @@
     REGO_NODE_TRUE,
     REGO_NODE_UNDEFINED,
     REGO_NODE_VAR,
+    REGO_NODE_TERMS,
+    REGO_NODE_BINDINGS,
+    REGO_NODE_RESULTS,
+    REGO_NODE_RESULT,
     regoNodeGet,
     regoNodeJSON,
     regoNodeSize,
@@ -51,6 +55,10 @@ class NodeKind(IntEnum):
     Boolean = REGO_NODE_TRUE
     Null = REGO_NODE_NULL
     Undefined = REGO_NODE_UNDEFINED
+    Terms = REGO_NODE_TERMS
+    Bindings = REGO_NODE_BINDINGS
+    Results = REGO_NODE_RESULTS
+    Result = REGO_NODE_RESULT
     Error = REGO_NODE_ERROR
     ErrorMessage = REGO_NODE_ERROR_MESSAGE
     ErrorAst = REGO_NODE_ERROR_AST
@@ -188,7 +196,7 @@ def index(self, index: int) -> "Node":
         if self._kind == NodeKind.Term:
             return self.at(0).index(index)
 
-        if self._kind == NodeKind.Array:
+        if self._kind in [NodeKind.Array, NodeKind.Terms, NodeKind.Results]:
             if index < 0 or index >= len(self):
                 raise IndexError("index out of bounds")
 
diff --git a/wrappers/python/src/regopy/output.py b/wrappers/python/src/regopy/output.py
index dea5163e..152ddd11 100644
--- a/wrappers/python/src/regopy/output.py
+++ b/wrappers/python/src/regopy/output.py
@@ -1,14 +1,52 @@
 """Module providing an interface to a rego-cpp output object."""
 
+import json
+from typing import Union
 from ._regopy import (
     regoFreeOutput,
-    regoOutputBinding,
     regoOutputNode,
     regoOutputOk,
     regoOutputString,
+    regoOutputExpressionsAtIndex,
+    regoOutputBindingAtIndex,
 )
 from .node import Node
 
+class Result:
+    """A result from a Rego output.
+    
+    Each result contains a list of terms, and set of bindings.
+
+    Examples:
+        >>> from regopy import Interpreter
+        >>> rego = Interpreter()
+        >>> output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5")
+        >>> result = output[0]
+        >>> print(result)
+        {"expressions": [10], "bindings": {"x": 5, "y": 9.4}}
+
+        >>> print(result["x"])
+        5
+
+        >>> print(result[0])
+        10
+    """
+    def __init__(self, obj: dict):
+        self.expressions = obj.get("expressions", [])
+        self.bindings = obj.get("bindings", {})
+    
+    def __str__(self) -> str:
+        return json.dumps({"expressions": self.expressions, "bindings": self.bindings})
+
+    def __repr__(self) -> str:
+        return f"Result({self.expressions}, {self.bindings})"
+
+    def __getitem__(self, key: Union[str, int]):
+        if isinstance(key, int):
+            return self.expressions[key]
+        else:
+            return self.bindings[key]
+
 
 class Output:
     """Interface for the Rego output.
@@ -20,10 +58,9 @@ class Output:
     Examples:
         >>> from regopy import Interpreter
         >>> rego = Interpreter()
-        >>> output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4")
+        >>> output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5")
         >>> print(output)
-        x = 5
-        y = 9.4
+        {"expressions":[10], "bindings":{"x":5, "y":9.4}}
 
         >>> x = output.binding("x")
         >>> print(x.json())
@@ -37,6 +74,14 @@ def __init__(self, impl):
         As such, this initializer should not be called directly.
         """
         self._impl = impl
+        if regoOutputOk(impl):
+            output = json.loads(regoOutputString(impl))
+            if isinstance(output, list):
+                self.results = [Result(obj) for obj in output]
+            else:
+                self.results = [Result(output)]
+        else:
+            self.results = []
 
     def __del__(self):
         """Destructor."""
@@ -57,12 +102,35 @@ def __repr__(self) -> str:
         """Returns a string representation of the output."""
         return "Output({}@{})".format(regoOutputString(self._impl), self._impl)
 
+    def __len__(self) -> int:
+        """Returns the number of results in the output."""
+        return len(self.results)
+
+    def __getitem__(self, index: int) -> Result:
+        """Returns the result at the given index."""
+        return self.results[index]
+
     def node(self) -> Node:
         """Returns the root node of the output."""
         return Node(regoOutputNode(self._impl))
+       
+    def expressions(self, index=0) -> Node:
+        """Returns the output terms at the given index.
+
+        Args:
+            index (int, optional): The index of the term to return.
+        
+        Returns:
+            Node: A node contains a list of node objects
+        """
+        return Node(regoOutputExpressionsAtIndex(self._impl, index))
 
-    def binding(self, name: str) -> Node:
+    def binding(self, name: str, index=0) -> Node:
         """Attempts to return the binding for the given variable name.
+    
+        Args:
+            name (str): The name of the variable to return.
+            index (int, optional): The index of the binding to return.
 
         Returns:
             Node: The binding for the given variable name.
@@ -70,7 +138,7 @@ def binding(self, name: str) -> Node:
         Raises:
             ValueError: If the variable is not bound.
         """
-        impl = regoOutputBinding(self._impl, name)
+        impl = regoOutputBindingAtIndex(self._impl, index, name)
         if impl:
             return Node(impl)
 
diff --git a/wrappers/rust/regorust/README.md b/wrappers/rust/regorust/README.md
index f7293062..3e803a1d 100644
--- a/wrappers/rust/regorust/README.md
+++ b/wrappers/rust/regorust/README.md
@@ -128,7 +128,7 @@ fn main() {
 
 ## Language Support
 
-At present we support v0.55.0 of Rego as defined by OPA, with the following grammar:
+At present we support v0.65.0 of Rego as defined by OPA, with the following grammar:
 
 ```ebnf
 module          = package { import } policy
@@ -160,7 +160,7 @@ set-compr       = "{" term "|" query "}"
 object-compr    = "{" object-item "|" query "}"
 infix-operator  = assign-operator | bool-operator | arith-operator | bin-operator
 bool-operator   = "==" | "!=" | "<" | ">" | ">=" | "<="
-arith-operator  = "+" | "-" | "*" | "/"
+arith-operator  = "+" | "-" | "*" | "/" | "%"
 bin-operator    = "&" | "|"
 assign-operator = ":=" | "="
 ref             = ( var | array | object | set | array-compr | object-compr | set-compr | expr-call ) { ref-arg }
@@ -179,6 +179,9 @@ non-empty-set   = "{" term { "," term } "}"
 empty-set       = "set(" ")"
 ```
 
+> [!NOTE]
+> This grammar corresponds to Rego with `rego.v1` enabled (See [OPA v1.0](https://www.openpolicyagent.org/docs/latest/opa-1) for more info).
+
 Definitions:
 ```
 []     optional (zero or one instances)
diff --git a/wrappers/rust/regorust/build.rs b/wrappers/rust/regorust/build.rs
index 7420c49a..35a0431b 100644
--- a/wrappers/rust/regorust/build.rs
+++ b/wrappers/rust/regorust/build.rs
@@ -7,9 +7,9 @@ fn main() {
     let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
     let regocpp_path = out_path.join("rego-cpp");
     let num_procs = std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
-    let rego_git_repo = std::env::var("REGOCPP_C_WRAPPER_REPO")
+    let rego_git_repo = std::env::var("REGOCPP_REPO")
         .unwrap_or("https://github.com/microsoft/rego-cpp.git".to_string());
-    let rego_git_tag = std::env::var("REGOCPP_C_WRAPPER_TAG").unwrap_or("main".to_string());
+    let rego_git_tag = std::env::var("REGOCPP_TAG").unwrap_or("main".to_string());
 
     if !regocpp_path.exists() {
         Command::new("git")
diff --git a/wrappers/rust/regorust/src/lib.rs b/wrappers/rust/regorust/src/lib.rs
index c342ff79..410b726c 100644
--- a/wrappers/rust/regorust/src/lib.rs
+++ b/wrappers/rust/regorust/src/lib.rs
@@ -216,6 +216,10 @@ pub enum NodeKind {
     True,
     False,
     Null,
+    Terms,
+    Bindings,
+    Results,
+    Result,
     Error,
     ErrorSeq,
     ErrorMessage,
@@ -419,7 +423,7 @@ impl Interpreter {
     /// rego.add_module("scalars", module);
     /// let result = rego.query("data.scalars.greeting").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), r#""Hello""#);
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":["Hello"]}"#);
     /// ```
     pub fn add_module(&self, name: &str, source: &str) -> Result<(), &str> {
         let name_cstr = CString::new(name).unwrap();
@@ -495,7 +499,7 @@ impl Interpreter {
     /// rego.add_data_json(data);
     /// let result = rego.query("data.one.bar").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), r#""Foo""#);
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":["Foo"]}"#);
     /// ```
     pub fn add_data_json(&self, data: &str) -> Result<(), &str> {
         let data_cstr = CString::new(data).unwrap();
@@ -578,7 +582,7 @@ impl Interpreter {
     /// rego.set_input_json(input);
     /// let result = rego.query("input.a").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), "10");
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":[10]}"#);
     /// ```
     pub fn set_input_json(&self, input: &str) -> Result<(), &str> {
         let input_cstr = CString::new(input).unwrap();
@@ -701,15 +705,15 @@ impl Interpreter {
     /// rego.set_input_json(input0);
     /// let result = rego.query("data.multi.a").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), "1");
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":[1]}"#);
     /// rego.set_input_json(input1);
     /// let result = rego.query("data.multi.a").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), "41");
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":[41]}"#);
     /// rego.set_input_json(input2);
     /// let result = rego.query("data.multi.a").unwrap();
     /// println!("{}", result.to_str().unwrap());
-    /// # assert_eq!(result.to_str().unwrap(), "70");
+    /// # assert_eq!(result.to_str().unwrap(), r#"{"expressions":[70]}"#);
     /// ```
     pub fn query(&self, query: &str) -> Result<Output, &str> {
         let query_cstr = CString::new(query).unwrap();
@@ -762,7 +766,7 @@ impl Output {
         c_ok == 1
     }
 
-    /// Returns the output as a human readable string.
+    /// Returns the output as a JSON-encoded string.
     ///
     /// If the result of [`Self::ok()`] is false, the result will be an string
     /// containing error information.
@@ -796,14 +800,14 @@ impl Output {
     /// If the output is not OK or the variable is not bound, then
     /// this will return an [`Result::Err`] with the output node.
     /// Otherwise it will return the bound value for the variable.
-    pub fn binding(&self, name: &str) -> Result<Node, Node> {
+    pub fn binding_at_index(&self, index: regoSize, name: &str) -> Result<Node, Node> {
         if !self.ok() {
             return self.to_node();
         }
 
         let name_cstr = CString::new(name).unwrap();
         let name_ptr = name_cstr.as_ptr();
-        let node_ptr = unsafe { regoOutputBinding(self.c_ptr, name_ptr) };
+        let node_ptr = unsafe { regoOutputBindingAtIndex(self.c_ptr, index, name_ptr) };
 
         if node_ptr == std::ptr::null_mut() {
             self.to_node()
@@ -811,6 +815,40 @@ impl Output {
             Ok(Node::new(node_ptr))
         }
     }
+
+    /// Attempts to return the binding for the given variable name.
+    ///
+    /// If the output is not OK or the variable is not bound, then
+    /// this will return an [`Result::Err`] with the output node.
+    /// Otherwise it will return the bound value for the variable.
+    pub fn binding(&self, name: &str) -> Result<Node, Node> {
+        self.binding_at_index(0, name)
+    }
+
+    /// Attempts to return the expressions at a given result index.
+    pub fn expressions_at_index(&self, index: regoSize) -> Result<Node, Node> {
+        if !self.ok() {
+            return self.to_node();
+        }
+
+        let node_ptr = unsafe { regoOutputExpressionsAtIndex(self.c_ptr, index) };
+
+        if node_ptr == std::ptr::null_mut() {
+            self.to_node()
+        } else {
+            Ok(Node::new(node_ptr))
+        }
+    }
+
+    /// Attempts to return the expressions at a given result index.
+    pub fn expressions(&self) -> Result<Node, Node> {
+        self.expressions_at_index(0)
+    }
+
+    /// Returns the number of results in the output.
+    pub fn size(&self) -> regoSize {
+        unsafe { regoOutputSize(self.c_ptr) }
+    }
 }
 
 impl PartialEq for Output {
@@ -846,6 +884,10 @@ impl NodeKind {
             REGO_NODE_TRUE => NodeKind::True,
             REGO_NODE_FALSE => NodeKind::False,
             REGO_NODE_NULL => NodeKind::Null,
+            REGO_NODE_TERMS => NodeKind::Terms,
+            REGO_NODE_BINDINGS => NodeKind::Bindings,
+            REGO_NODE_RESULTS => NodeKind::Results,
+            REGO_NODE_RESULT => NodeKind::Result,
             REGO_NODE_ERROR => NodeKind::Error,
             REGO_NODE_ERROR_SEQ => NodeKind::ErrorSeq,
             REGO_NODE_ERROR_MESSAGE => NodeKind::ErrorMessage,


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index aed403fa..b4f78787 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -15,12 +15,15 @@ endif()
 
 add_test(NAME rego_test_regocpp COMMAND rego_test regocpp.yaml -wf WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
 add_test(NAME rego_test_bugs COMMAND rego_test bugs.yaml -wf WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
+add_test(NAME rego_test_v1 COMMAND rego_test regov1.yaml -wf WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
 add_test(NAME rego_test_aci COMMAND rego_test aci/aci.yaml WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
 add_test(NAME rego_test_c_api COMMAND rego_test_c_api WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
 set_tests_properties(rego_test_aci PROPERTIES TIMEOUT 300)
 
 add_custom_command(TARGET rego_test POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/regocpp.yaml $<TARGET_FILE_DIR:rego_test>/regocpp.yaml)
+add_custom_command(TARGET rego_test POST_BUILD
+                   COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/regov1.yaml $<TARGET_FILE_DIR:rego_test>/regov1.yaml)
 add_custom_command(TARGET rego_test POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/bugs.yaml $<TARGET_FILE_DIR:rego_test>/bugs.yaml)
 add_custom_command(TARGET rego_test POST_BUILD
@@ -72,6 +75,7 @@ if(REGOCPP_OPA_TESTS)
   negation
   nestedreferences
   numbersrange
+  numbersrangestep
   objectfilter
   objectfilteridempotent
   objectfilternonstringkey
@@ -88,7 +92,6 @@ if(REGOCPP_OPA_TESTS)
   partialsetdoc
   rand
   reachable
-  replacen
   refheads
   regexfind
   regexfindallstringsubmatch
@@ -97,6 +100,7 @@ if(REGOCPP_OPA_TESTS)
   regexreplace
   regexsplit
   regexmatchtemplate
+  replacen
   semvercompare
   semverisvalid
   sets
diff --git a/tests/bugs.yaml b/tests/bugs.yaml
index e650608f..b8e500f4 100644
--- a/tests/bugs.yaml
+++ b/tests/bugs.yaml
@@ -45,6 +45,16 @@ cases:
       - 1
       - 2
       - 3
+- note: regocpp/bug110
+  query: "[1, 2, 3][_] = x"
+  want_result:
+    - x: 1
+    - x: 2
+    - x: 3
+- note: regocpp/bug111
+  query: "not data.foo = x"
+  want_result:
+    - x: true
 - note: regocpp/bug112
   modules:
   - |
@@ -83,6 +93,25 @@ cases:
   query: x = (12/3)%3
   want_result:
     - x: 1
+- note: regocpp/bug117
+  data:
+    letters:
+    - i: 0
+      x: 'a'
+    - i: 1
+      x: 'b'
+    - i: 2
+      x: 'c'
+    - i: 3
+      x: 'd'
+  modules:
+  - |
+    package test
+
+    odd_letters := [ letter.x | letter = data.letters[_]; letter.i % 2 == 1 ]
+  query: data.test.odd_letters = x
+  want_result:
+  - x: [b, d]
 - note: regocpp/bug124
   modules:
   - |
@@ -92,6 +121,19 @@ cases:
   query: data.test.x = x
   want_result:
     - x: true
+- note: regocpp/bug121
+  modules:
+  - |
+    package test
+
+    a.b.c = 1
+    a.b.d = 2
+    y = a.b
+  query: data.test.y = x
+  want_result:
+    - x:
+        c: 1
+        d: 2
 - note: regocpp/bug126
   modules:
   - |
diff --git a/tests/regocpp.yaml b/tests/regocpp.yaml
index 313ca865..ebb8291c 100644
--- a/tests/regocpp.yaml
+++ b/tests/regocpp.yaml
@@ -96,9 +96,10 @@ cases:
 - note: regocpp/query_math
   query: 'x=5;
     y=x + (2 - 4 * 0.25) * -3 + 7.4'
+  sort_bindings: true
   want_result:
     - x: 5
-    - y: 9.4
+      y: 9.4
 - note: regocpp/expressions
   modules:
   - |
diff --git a/tests/regov1.yaml b/tests/regov1.yaml
new file mode 100644
index 00000000..144cc5e9
--- /dev/null
+++ b/tests/regov1.yaml
@@ -0,0 +1,314 @@
+cases:
+- note: regov1/keywords/if
+  modules:
+  - |
+    package test
+    import rego.v1
+
+    p if {
+      true
+    }
+  query: data.test.p=x
+  want_result:
+    - x: true
+- note: regov1/keywords/contains
+  modules:
+  - |
+    package test
+    import rego.v1
+
+    p contains 1 if {
+      true
+    }
+  query: data.test.p=x
+  want_result:
+    - x: [1]
+- note: regov1/keywords/every
+  modules:
+  - |
+    package test
+    import rego.v1
+
+    p if {
+      every i in [1, 3, 5]{
+        i % 2 == 1
+      }
+    }
+  query: data.test.p=x
+  want_result:
+    - x: true
+- note: regov1/keywords/in
+  modules:
+  - |
+    package test
+    import rego.v1
+
+    p if {
+      1 in [1, 2, 3]
+    }
+  query: data.test.p=x
+  want_result:
+    - x: true
+- note: regov1/keywords/conflicting import
+  modules:
+  - |
+    package test
+    import rego.v1
+    import future.keywords
+
+    p if {
+      true
+    }
+  query: data.test.p=x
+  want_error_code: rego_parse_error
+  want_error: the `rego.v1` import implies `future.keywords`, these are therefore mutually exclusive
+- note: regov1/conflicting imports
+  modules:
+  - |
+    package a
+
+    b := 1
+  - |
+    package c
+
+    b := 2
+  - |
+    package test
+
+    import rego.v1
+    import data.a
+    import data.c as a
+
+    p if {
+      a.b == 2
+    }
+  query: data.test.p=x
+  want_error: import must not shadow import data.a
+  want_error_code: rego_compile_error
+- note: regov1/reserved/input
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := input if {
+      input := 3
+    }
+  query: data.test.p=x
+  want_error: variables must not shadow input (use a different variable name)
+  want_error_code: rego_compile_error
+- note: regov1/reserved/data
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := data if {
+      data := 3
+    }
+  query: data.test.p=x
+  want_error: variables must not shadow data (use a different variable name)
+  want_error_code: rego_compile_error
+- note: regov1/deprecated/all
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p if {
+      all([true, true])
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: all"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/any
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p if {
+      any([false, true])
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: any"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/re_match
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p if {
+      re_match("f.*", "foo")
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: re_match"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/set_diff
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := b if {
+      b := set_diff({1, 2, 3}, {1})
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: set_diff"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_array
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_array([1, 2, 3])
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_array"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_set
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_set({1, 2, 3})
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_set"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_string
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_string("foo")
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_string"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_boolean
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_boolean(true)
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_boolean"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_null
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_null(null)
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_null"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/cast_object
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a if {
+      b := cast_object({"a": 5})
+    }
+  query: data.test.p=x
+  want_error: "deprecated built-in function calls in expression: cast_object"
+  want_error_code: rego_type_error
+- note: regov1/deprecated/module specific
+  modules:
+  - |
+    package a
+
+    p := b {
+      b := cast_object({"a": 5})
+    }
+  - |
+    package test
+
+    import rego.v1
+
+    p := data.a.p
+    r := "foo"
+  query: data.test.p=x
+  want_result:
+  - x:
+      a: 5
+- note: regov1/required/comp_if
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p := a {
+      a := 2
+    }
+  query: data.test.p=x
+  want_error: "`if` keyword is required before rule body"
+  want_error_code: rego_parse_error
+- note: regov1/required/func_if
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p(x) := a {
+      a := x * 2
+    }
+  query: data.test.p(2)=x
+  want_error: "`if` keyword is required before rule body"
+  want_error_code: rego_parse_error
+- note: regov1/required/obj_if
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p[2] := 1 {
+      true
+    } 
+  query: data.test.p=x
+  want_error: "`if` keyword is required before rule body"
+  want_error_code: rego_parse_error
+- note: regov1/required/set_if
+  modules:
+  - |
+    package test
+
+    import rego.v1
+
+    p contains 2 {
+      true
+    }
+  query: data.test.p=x
+  want_error: "`if` keyword is required before rule body"
+  want_error_code: rego_parse_error
diff --git a/tests/test_case.cc b/tests/test_case.cc
index 660d34fe..9df6b995 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -281,44 +281,57 @@ namespace rego_test
     return modules;
   }
 
-  BindingMap TestCase::to_binding_map(const Node& node) const
+  BindingMaps TestCase::to_binding_maps(const Node& node) const
   {
-    BindingMap bindings;
+    BindingMaps binding_maps;
+
     if (node == Array)
     {
-      for (auto& binding : *node)
+      for (auto& object : *node)
       {
-        assert(binding == Object);
-        for (auto& member : *binding)
+        assert(object == Object);
+        BindingMap binding_map;
+        for (auto& item : *object)
         {
-          Node string = member->front()->front()->front();
+          Node string = (item / Key)->front()->front();
           assert(string == JSONString);
           std::string key = std::string(string->location().view());
           key = key.substr(1, key.size() - 2); // remove quotes
-          std::string value =
-            rego::to_key(member->back(), true, m_sort_bindings);
-          bindings[key] = value;
+          std::string value = rego::to_key(item / Val, true, m_sort_bindings);
+          binding_map[key] = value;
         }
+
+        std::string binding_key = rego::to_key(object, true, m_sort_bindings);
+        binding_maps[binding_key] = binding_map;
       }
     }
-    else
+    else if (node == Results)
     {
-      for (auto& binding : *node)
+      for (auto& result : *node)
       {
-        if (binding->type() != rego::Binding)
+        BindingMap binding_map;
+        assert(result == rego::Result);
+        Node bindings = result / Bindings;
+        if (bindings->empty())
         {
-          // raw term
           continue;
         }
 
-        std::string key = std::string((binding / rego::Var)->location().view());
-        std::string value =
-          rego::to_key((binding / rego::Term), true, m_sort_bindings);
-        bindings[key] = value;
+        for (auto& binding : *bindings)
+        {
+          std::string key =
+            std::string((binding / rego::Key)->location().view());
+          std::string value =
+            rego::to_key((binding / rego::Val), true, m_sort_bindings);
+          binding_map[key] = value;
+        }
+
+        std::string binding_key = rego::to_key(bindings, true, m_sort_bindings);
+        binding_maps[binding_key] = binding_map;
       }
     }
 
-    return bindings;
+    return binding_maps;
   }
 
   void TestCase::diff(
@@ -367,8 +380,8 @@ namespace rego_test
   bool TestCase::compare(
     const Node& actual, const Node& wanted, std::ostream& os) const
   {
-    BindingMap actual_bindings = to_binding_map(actual);
-    BindingMap wanted_bindings = to_binding_map(wanted);
+    BindingMaps actual_bindings = to_binding_maps(actual);
+    BindingMaps wanted_bindings = to_binding_maps(wanted);
 
     if (wanted_bindings.size() == 0)
     {
@@ -379,18 +392,13 @@ namespace rego_test
       }
     }
 
+    bool pass = true;
     for (auto& [key, value] : wanted_bindings)
     {
       if (actual_bindings.find(key) == actual_bindings.end())
       {
-        os << "Missing binding for " << key << std::endl;
-        return false;
-      }
-
-      if (actual_bindings[key] != value)
-      {
-        diff(key + " = " + actual_bindings[key], key + " = " + value, os);
-        return false;
+        os << "Wanted binding: " << key << std::endl;
+        pass = false;
       }
     }
 
@@ -398,12 +406,12 @@ namespace rego_test
     {
       if (wanted_bindings.find(key) == wanted_bindings.end())
       {
-        os << "Unexpected binding for " << key << std::endl;
-        return false;
+        os << "Actual binding: " << key << std::endl;
+        pass = false;
       }
     }
 
-    return true;
+    return pass;
   }
 
   bool TestCase::compare(
@@ -528,11 +536,13 @@ namespace rego_test
         // as written, this test case can never pass (the result is the
         // opa.runtime object, which is not equal to the empty object) so we
         // write in the result of calling rego::version()
-        Node want_result = NodeDef::create(yaml::Sequence);
-        Node result = NodeDef::create(rego::Binding);
-        result << (rego::Var ^ "x");
-        result << (rego::Term << rego::version());
-        test_case.want_result(want_result << result);
+        test_case.want_result(
+          rego::Results
+          << (rego::Result << rego::Terms
+                           << (rego::Bindings
+                               << (rego::Binding
+                                   << (rego::Var ^ "x")
+                                   << (rego::Term << rego::version())))));
       }
 
       if (test_case.note() == "regexmatch/re_match: bad pattern err")
@@ -575,15 +585,20 @@ namespace rego_test
     const std::filesystem::path& debug_path, bool wf_checks) const
   {
     rego::Interpreter interpreter;
-    interpreter.builtins()->strict_errors(m_strict_error);
+    interpreter.builtins().strict_errors(m_strict_error);
     interpreter.wf_check_enabled(wf_checks)
       .debug_enabled(!debug_path.empty())
       .debug_path(debug_path);
 
+    Node actual;
     for (std::size_t i = 0; i < m_modules.size(); ++i)
     {
       std::string name = "module" + std::to_string(i);
-      interpreter.add_module(name, m_modules[i]);
+      actual = interpreter.add_module(name, m_modules[i]);
+      if (actual != nullptr)
+      {
+        break;
+      }
     }
 
     if (m_data != nullptr)
@@ -591,7 +606,7 @@ namespace rego_test
       interpreter.add_data(m_data);
     }
 
-    if (m_input_term.size() > 0)
+    if (actual == nullptr && m_input_term.size() > 0)
     {
       interpreter.set_input_term(m_input_term);
     }
@@ -602,14 +617,17 @@ namespace rego_test
 
     bool pass = true;
     std::ostringstream error;
-    Node actual;
-    try
-    {
-      actual = interpreter.raw_query(m_query);
-    }
-    catch (const std::exception& e)
+
+    if (actual == nullptr)
     {
-      return {false, e.what()};
+      try
+      {
+        actual = interpreter.raw_query(m_query);
+      }
+      catch (const std::exception& e)
+      {
+        return {false, e.what()};
+      }
     }
 
     if (m_broken)
@@ -661,15 +679,23 @@ namespace rego_test
     }
     else if (m_want_error_code.length() > 0)
     {
-      std::string actual_code =
-        std::string((actual / ErrorCode)->location().view());
-      pass = compare(actual_code, m_want_error_code, error);
+      if (actual != Error)
+      {
+        pass = false;
+        error << "wanted an error code, actual: " << actual << std::endl;
+      }
+      else
+      {
+        std::string actual_code =
+          std::string((actual / ErrorCode)->location().view());
+        pass = compare(actual_code, m_want_error_code, error);
+      }
     }
     else if (m_want_result)
     {
-      if (actual->size() > 0)
+      if (actual != Undefined)
       {
-        if (actual->type() == Error)
+        if (actual == Error)
         {
           pass = false;
           error << "wanted a result, received: " << std::endl;
@@ -704,7 +730,7 @@ namespace rego_test
     }
     else if (m_want_defined)
     {
-      if (actual->size() > 0)
+      if (actual != Undefined)
       {
         pass = true;
       }
@@ -716,7 +742,7 @@ namespace rego_test
     }
     else
     {
-      if (actual->size() == 0)
+      if (actual == Undefined)
       {
         pass = true;
       }
diff --git a/tests/test_case.h b/tests/test_case.h
index 7cc83bfa..37df19d0 100644
--- a/tests/test_case.h
+++ b/tests/test_case.h
@@ -7,6 +7,7 @@ namespace rego_test
   using namespace rego;
 
   using BindingMap = std::map<std::string, std::string>;
+  using BindingMaps = std::map<std::string, BindingMap>;
 
   struct Result
   {
@@ -86,7 +87,7 @@ namespace rego_test
     TestCase& broken(bool broken);
 
   private:
-    BindingMap to_binding_map(const Node& node) const;
+    BindingMaps to_binding_maps(const Node& node) const;
     bool compare(
       const Node& actual, const Node& wanted, std::ostream& os) const;
     bool compare(
diff --git a/wrappers/python/test/test_interpreter.py b/wrappers/python/test/test_interpreter.py
index 726a7c8a..a1698d07 100644
--- a/wrappers/python/test/test_interpreter.py
+++ b/wrappers/python/test/test_interpreter.py
@@ -5,11 +5,12 @@
 
 def test_query_math():
     rego = Interpreter()
-    output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4")
+    output = rego.query("x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4;2 * 5")
     assert output is not None
-    assert str(output) == "x = 5\ny = 9.4"
+    assert str(output) == '{"expressions":[10], "bindings":{"x":5, "y":9.4}}'
     assert output.binding("x").json() == "5"
     assert output.binding("y").json() == "9.4"
+    assert output.expressions()[0].json() == "10"
 
 
 def test_input_data():
@@ -109,7 +110,7 @@ def test_multiple_inputs():
     rego.add_module("multi", module)
     rego.set_input(input0)
     output = rego.query("x = data.multi.a")
-    assert str(output) == "x = 1"
+    assert str(output) == """{"bindings":{"x":1}}"""
     assert output.binding("x").value == 1
     rego.set_input(input1)
     output = rego.query("x = data.multi.a")