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

Issue:
Result of division is treated as float even if value is integer
Ran into this recently. I don't think I reported this before.

`[1,2][1/1]` evaluates to undefined.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index 1a688d45..41940a88 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -127,7 +127,7 @@ jobs:
 
       - name: Cargo test
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
-        run: cargo test
+        run: cargo test -vv
         env:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
@@ -136,7 +136,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python_version: ["3.9", "3.10", "3.11", "3.12"]
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -222,7 +222,7 @@ jobs:
 
       - name: Cargo test
         working-directory: ${{github.workspace}}/wrappers/rust/regorust
-        run: cargo test
+        run: cargo test -vv
         env:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
@@ -231,7 +231,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python_version: ["3.9", "3.10", "3.11", "3.12"]
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -330,7 +330,7 @@ jobs:
 
       - name: Cargo test
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
-        run: cargo test
+        run: cargo test -vv
         env:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
@@ -339,7 +339,7 @@ jobs:
     runs-on: macos-latest
     strategy:
       matrix:
-        python_version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        python_version: ["3.9", "3.10", "3.11", "3.12"]
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -439,7 +439,7 @@ jobs:
 
       - name: Cargo test
         working-directory: ${{github.workspace}}/wrappers/rust/regorust/
-        run: cargo test
+        run: cargo test -vv
         env:
           REGOCPP_C_WRAPPER_REPO: https://github.com/${{github.repository}}
           REGOCPP_C_WRAPPER_TAG: ${{github.sha}}
diff --git a/CHANGELOG b/CHANGELOG
index 772412e2..f185e804 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,38 @@
 # Changelog
 
+## 2024-06-04 - Version 0.4.0
+Major refactor of the code to use the new version of Trieste.
+
+**New Features**
+- Updated to latest version of Trieste
+- Uses Trieste's YAML and JSON parsers and emitters
+- Uses Trieste's more robust Unicode support
+- Passes broken up into a `Reader`, which parses Rego files, and a `Rewriter` which performs unification
+- Added the `encoding` and `graphs` builtins
+- Added `set_log_level_from_string` and `setLogLevelFromString` to make it easier to set the log level. All executables now use this option.
+
+**Improvements**
+- The Interpreter caches intermediate versions of all modules, reducing query execution time
+- All of the early passes have been rewritten and are more efficient, resulting in reduced parsing time
+- The reduction of the input passes (and their restriction to a single file) along with other optimizations to header files has resulted in reduced compile times
+- The `rego_fuzzer` xecutable allows for more fine-grained fuzzing and testing of the Rego toolchain
+- Multiple bug fixes
+
+**Deprecation**
+- The `regoSetInputJSON` method is a misnomer, as the input does not strictly need to be JSON. This has been replaced with an (otherwise identical) function called
+  `regoSetInputTerm` which does the same thing, and the `JSON` version has been deprecated.
+
+**Breaking Changes**
+- The `rego_trieste` executable has been replaced by the new `rego_fuzzer` executable
+
+**New Test Coverage**
+- `base64builtins`
+- `base64urlbuiltins`
+- `hexbuiltins`
+- `jsonbuiltins`
+- `reachable`
+- `urlbuiltins`
+
 ## 2024-01-19 - Version 0.3.11
 Minor improvements and bug fixes.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1fee6b31..84d4fe50 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -80,16 +80,29 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
 # -------------------- Find packages --------------------------
 
+# Used to provide
+# FetchContent_MakeAvailable_ExcludeFromAll
+FetchContent_Declare(
+    cmake_utils
+    GIT_REPOSITORY https://github.com/mjp41/cmake_utils
+    GIT_TAG 2bf98b5773ea7282197c823e205547d8c2e323c0
+    GIT_SHALLOW FALSE
+)
+
 FetchContent_Declare(
   trieste
   GIT_REPOSITORY https://github.com/microsoft/trieste
-  GIT_TAG        58a6eeeaeda4f5c96dad30c23b2d4e3feae7a60c
+  GIT_TAG        27b11771a0c07fca676e5d515f6cde61ece5117c
 )
 
+FetchContent_MakeAvailable(cmake_utils)
+
 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")
@@ -104,7 +117,7 @@ if (REGOCPP_SPECULATIVE_LOAD_HARDENING)
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mllvm -x86-speculative-load-hardening")
 endif()
 
-FetchContent_MakeAvailable(trieste)
+FetchContent_MakeAvailable_ExcludeFromAll(trieste)
 
 if (REGOCPP_SPECULATIVE_LOAD_HARDENING)
   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_SAVED}")
@@ -119,7 +132,8 @@ else()
   file(GLOB_RECURSE ALL_SOURCE_FILES CONFIGURE_DEPENDS
         src/*.cc
         src/*.hh
-        src/passes/*.cc
+        src/reader/*.cc
+        src/unify/*.cc
         src/*.h
         include/rego/*.h
         include/rego/*.hh
@@ -177,7 +191,7 @@ set(INSTALL_CONFIGDIR cmake)
 set(INSTALL_LIBDIR lib)
 set(INSTALL_INCLUDEDIR include)
 
-install(TARGETS rego
+install(TARGETS rego trieste json yaml snmalloc snmallocshim-static re2
   EXPORT ${PROJECT_NAME}_Targets
   ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
   LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
@@ -208,7 +222,7 @@ install(FILES ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
   DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake)
 
 if (REGOCPP_BUILD_TOOLS)
-  install(TARGETS rego_interpreter rego_trieste RUNTIME)
+  install(TARGETS rego_interpreter rego_fuzzer RUNTIME)
 endif()
 
 if (REGOCPP_COPY_EXAMPLES)
@@ -216,8 +230,8 @@ if (REGOCPP_COPY_EXAMPLES)
 endif()
 
 if (REGOCPP_BUILD_TESTS)
-  install(TARGETS rego_test yaml_trieste RUNTIME)
-  install(FILES tests/regocpp.yaml tests/bigint.yaml DESTINATION tests)
+  install(TARGETS rego_test rego_test_c_api RUNTIME)
+  install(FILES tests/regocpp.yaml tests/bigint.yaml tests/bugs.yaml DESTINATION tests)
   install(DIRECTORY tests/aci DESTINATION tests)
 endif()
 
diff --git a/README.md b/README.md
index a29e5d9e..5893462d 100644
--- a/README.md
+++ b/README.md
@@ -154,6 +154,8 @@ all the standard builtins. The following builtins are currently supported:
 - `arrays`
 - `bits`
 - `casts`
+- `encoding`
+- `graphs`
 - `numbers`
 - `objects`
 - `regex`
@@ -163,9 +165,6 @@ all the standard builtins. The following builtins are currently supported:
 - `types`
 - `units`
 - miscellaneous
-    * `base64_encode`
-    * `base64_decode`
-    * `json.marshal`
     * `opa.runtime`
     * `print`
     * `time.now_ns`
@@ -182,18 +181,16 @@ To build with the OPA tests available for testing, use one of the following pres
 - `release-opa`
 
 At present, we are **NOT** passing the following test suites in full:
-- `base64*`
 - `crypto*`
 - `glob*`
 - `graphql`
 - `invalidkeyerror`
-- `json*`
+- `json*` (except `jsonbuiltins`)
 - `jwt*`
 - `net*`
 - `planner-ir`
 - `providers-aws`
-- `reachable`
-- `urlbuiltins`
+- `time`
 - `walkbuiltin`
 
 ## Contributing
diff --git a/VERSION b/VERSION
index 99a89b94..60a2d3e9 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.3.11
\ No newline at end of file
+0.4.0
\ No newline at end of file
diff --git a/cmake/regocppConfig.cmake.in b/cmake/regocppConfig.cmake.in
index 3275fb91..6dc536f8 100644
--- a/cmake/regocppConfig.cmake.in
+++ b/cmake/regocppConfig.cmake.in
@@ -4,7 +4,7 @@ include(FetchContent)
 FetchContent_Declare(
   trieste
   GIT_REPOSITORY https://github.com/microsoft/trieste
-  GIT_TAG        c23669df20956a19c263ae1638e6fa976c31eec5
+  GIT_TAG        58a6eeeaeda4f5c96dad30c23b2d4e3feae7a60c
 )
 
 FetchContent_GetProperties(trieste)
diff --git a/examples/cpp/custom_builtin.cc b/examples/cpp/custom_builtin.cc
index c98141b8..c56f11b1 100644
--- a/examples/cpp/custom_builtin.cc
+++ b/examples/cpp/custom_builtin.cc
@@ -29,5 +29,5 @@ int main()
   Interpreter rego;
   rego.builtins().register_builtin(
     BuiltInDef::create(Location("myadd"), 2, add));
-  std::cout << rego.query("myadd(2, 3)");
+  logging::Output() << rego.query("myadd(2, 3)");
 }
\ No newline at end of file
diff --git a/include/rego/rego.hh b/include/rego/rego.hh
index aa43f6a0..1b5f0104 100644
--- a/include/rego/rego.hh
+++ b/include/rego/rego.hh
@@ -16,770 +16,181 @@
 namespace rego
 {
   using namespace trieste;
+  using namespace wf::ops;
 
-  // grammar tokens
-  inline const auto Module = TokenDef("module", flag::symtab);
-  inline const auto Package = TokenDef("package");
-  inline const auto Policy = TokenDef("policy");
-  inline const auto Rule = TokenDef("rule", flag::symtab);
-  inline const auto RuleHead = TokenDef("rule-head");
-  inline const auto RuleHeadComp = TokenDef("rule-head-comp");
-  inline const auto RuleHeadFunc = TokenDef("rule-head-func");
-  inline const auto RuleHeadSet = TokenDef("rule-head-set");
-  inline const auto RuleHeadObj = TokenDef("rule-head-obj");
-  inline const auto RuleArgs = TokenDef("rule-args");
-  inline const auto Query = TokenDef("query");
-  inline const auto Literal = TokenDef("literal");
-  inline const auto Expr = TokenDef("expr");
-  inline const auto ExprInfix = TokenDef("expr-infix");
-  inline const auto ExprCall = TokenDef("expr-call");
-  inline const auto ExprEvery = TokenDef("expr-every");
-  inline const auto UnaryExpr = TokenDef("unary-expr");
-  inline const auto Term = TokenDef("term");
-  inline const auto InfixOperator = TokenDef("infix-operator");
-  inline const auto BoolOperator = TokenDef("bool-operator");
-  inline const auto ArithOperator = TokenDef("arith-operator");
-  inline const auto AssignOperator = TokenDef("assign-operator");
-  inline const auto Ref = TokenDef("ref");
-  inline const auto RefArgBrack = TokenDef("ref-arg-brack");
-  inline const auto RefArgDot = TokenDef("ref-arg-dot");
-  inline const auto Var = TokenDef("var", flag::print);
-  inline const auto Scalar = TokenDef("scalar");
-  inline const auto String = TokenDef("string");
-  inline const auto Array = TokenDef("array");
-  inline const auto Object = TokenDef("object");
-  inline const auto Set = TokenDef("set");
-  inline const auto EmptySet = TokenDef("empty-set");
-  inline const auto ObjectItem = TokenDef("object-item");
-  inline const auto RawString = TokenDef("raw-string", flag::print);
-  inline const auto JSONString = TokenDef("STRING", flag::print);
-  inline const auto Int = TokenDef("INT", flag::print);
-  inline const auto Float = TokenDef("FLOAT", flag::print);
-  inline const auto True = TokenDef("true");
-  inline const auto False = TokenDef("false");
-  inline const auto Null = TokenDef("null");
-  inline const auto Equals = TokenDef("==");
-  inline const auto NotEquals = TokenDef("!=");
-  inline const auto LessThan = TokenDef("<");
-  inline const auto GreaterThan = TokenDef(">");
-  inline const auto LessThanOrEquals = TokenDef("<=");
-  inline const auto GreaterThanOrEquals = TokenDef(">=");
-  inline const auto Add = TokenDef("+");
-  inline const auto Subtract = TokenDef("-");
-  inline const auto Multiply = TokenDef("*");
-  inline const auto Divide = TokenDef("/");
-  inline const auto Modulo = TokenDef("%");
-  inline const auto And = TokenDef("&");
-  inline const auto Or = TokenDef("|");
-  inline const auto Assign = TokenDef(":=");
-  inline const auto Unify = TokenDef("=");
-  inline const auto Default = TokenDef("default");
-  inline const auto Some = TokenDef("some");
-  inline const auto SomeDecl = TokenDef("some-decl");
-  inline const auto SomeExpr = TokenDef("some-expr");
-  inline const auto If = TokenDef("if");
-  inline const auto IsIn = TokenDef("in");
-  inline const auto Contains = TokenDef("contains");
-  inline const auto Else = TokenDef("else");
-  inline const auto As = TokenDef("as");
-  inline const auto With = TokenDef("with");
-  inline const auto Every = TokenDef("every");
-  inline const auto ArrayCompr = TokenDef("array-compr");
-  inline const auto ObjectCompr = TokenDef("object-compr");
-  inline const auto SetCompr = TokenDef("set-compr");
-  inline const auto Membership = TokenDef("membership");
-
-  // intermediate tokens
-  inline const auto UnifyBody = TokenDef("unify-body");
-  inline const auto RuleRef = TokenDef("rule-ref");
-  inline const auto RefHead = TokenDef("ref-head");
-  inline const auto RefArgSeq = TokenDef("ref-arg-seq");
-  inline const auto ModuleSeq = TokenDef("module-seq");
-  inline const auto DataSeq = TokenDef("data-seq");
-  inline const auto DataItemSeq = TokenDef("data-item-seq");
-  inline const auto DataItem = TokenDef("data-item", flag::lookup);
-  inline const auto DataRule = TokenDef("data-rule", flag::lookup);
-  inline const auto DataObject = TokenDef("data-object");
-  inline const auto DataObjectItem = TokenDef("data-object-item");
-  inline const auto DataArray = TokenDef("data-array");
-  inline const auto DataSet = TokenDef("data-set");
-  inline const auto DataModule = TokenDef("data-module", flag::lookup);
-  inline const auto Submodule =
-    TokenDef("submodule", flag::lookdown | flag::lookup);
-  inline const auto DataTerm = TokenDef("data-term");
-  inline const auto ObjectItemSeq = TokenDef("object-item-seq");
-  inline const auto ImportSeq = TokenDef("import-seq");
-  inline const auto VarSeq = TokenDef("var-seq");
-  inline const auto ElseSeq = TokenDef("else-seq");
-  inline const auto WithSeq = TokenDef("with-seq");
-  inline const auto SkipSeq = TokenDef("skip-seq");
-  inline const auto ItemSeq = TokenDef("item-seq");
-  inline const auto ItemSeq1 = TokenDef("item-seq-1");
-
-  // data and input
-  inline const auto Input = TokenDef("input", flag::lookup);
-  inline const auto Data = TokenDef("data", flag::lookup);
-
-  // fused
-  inline const auto RuleComp = TokenDef(
-    "rule-comp",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleFunc = TokenDef(
-    "rule-func",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleSet = TokenDef(
-    "rule-set",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto RuleObj = TokenDef(
-    "rule-obj",
-    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
-  inline const auto DefaultRule =
-    TokenDef("default-rule", flag::lookup | flag::lookdown);
-  inline const auto Local = TokenDef("local", flag::lookup | flag::shadowing);
-  inline const auto ArithInfix = TokenDef("arith-infix");
-  inline const auto BinInfix = TokenDef("bin-infix");
-  inline const auto BoolInfix = TokenDef("bool-infix");
-  inline const auto AssignInfix = TokenDef("assign-infix");
-  inline const auto ArgVar = TokenDef("arg-var", flag::lookup);
-  inline const auto ArgVal = TokenDef("arg-val");
-  inline const auto LiteralWith = TokenDef("literal-with");
-  inline const auto LiteralEnum = TokenDef("literal-enum");
-  inline const auto LiteralInit = TokenDef("literal-init");
-  inline const auto LiteralNot = TokenDef("literal-not");
-
-  // utility
-  inline const auto Undefined = TokenDef("undefined");
-  inline const auto Rego = TokenDef("rego", flag::symtab);
-  inline const auto Math = TokenDef("math");
-  inline const auto Op = TokenDef("op");
-  inline const auto Comparison = TokenDef("comparison");
-  inline const auto Function = TokenDef("function");
-  inline const auto Arg = TokenDef("arg");
-  inline const auto ArgSeq = TokenDef("arg-seq");
-  inline const auto Val = TokenDef("value");
-  inline const auto Id = TokenDef("id");
-  inline const auto Head = TokenDef("head");
-  inline const auto Tail = TokenDef("tail");
-  inline const auto Head1 = TokenDef("head-1");
-  inline const auto Tail1 = TokenDef("tail-1");
-  inline const auto Head2 = TokenDef("head-2");
-  inline const auto Tail2 = TokenDef("tail-2");
-  inline const auto Lhs = TokenDef("lhs");
-  inline const auto Rhs = TokenDef("rhs");
-  inline const auto LhsVars = TokenDef("lhs-vars");
-  inline const auto RhsVars = TokenDef("rhs-vars");
-  inline const auto Key = TokenDef("key", flag::print);
-  inline const auto RefTerm = TokenDef("ref-term");
-  inline const auto NumTerm = TokenDef("num-term");
-  inline const auto ArithArg = TokenDef("arith-arg");
-  inline const auto BinArg = TokenDef("bin-arg");
-  inline const auto BoolArg = TokenDef("bool-arg");
-  inline const auto AssignArg = TokenDef("assign-arg");
-  inline const auto RuleHeadType = TokenDef("rule-head-type");
-  inline const auto UnifyExpr = TokenDef("unify-expr");
-  inline const auto UnifyExprWith = TokenDef("unify-expr-with");
-  inline const auto UnifyExprCompr = TokenDef("unify-expr-compr");
-  inline const auto UnifyExprEnum = TokenDef("unify-expr-enum");
-  inline const auto UnifyExprNot = TokenDef("unify-expr-not");
-  inline const auto TermSet = TokenDef("term-set");
-  inline const auto Empty = TokenDef("empty");
-  inline const auto SimpleRef = TokenDef("simple-ref");
-  inline const auto Binding = TokenDef("binding");
-  inline const auto Body = TokenDef("body");
+  inline const auto Module = TokenDef("rego-module", flag::symtab);
+  inline const auto Package = TokenDef("rego-package");
+  inline const auto Policy = TokenDef("rego-policy");
+  inline const auto Rule = TokenDef("rego-rule", flag::symtab);
+  inline const auto RuleHead = TokenDef("rego-rulehead");
+  inline const auto RuleHeadComp = TokenDef("rego-ruleheadcomp");
+  inline const auto RuleHeadFunc = TokenDef("rego-ruleheadfunc");
+  inline const auto RuleHeadSet = TokenDef("rego-ruleheadset");
+  inline const auto RuleHeadObj = TokenDef("rego-ruleheadobj");
+  inline const auto RuleArgs = TokenDef("rego-ruleargs");
+  inline const auto Query = TokenDef("rego-query");
+  inline const auto Literal = TokenDef("rego-literal");
+  inline const auto Expr = TokenDef("rego-expr");
+  inline const auto ExprInfix = TokenDef("rego-exprinfix");
+  inline const auto ExprCall = TokenDef("rego-exprcall");
+  inline const auto ExprEvery = TokenDef("rego-exprevery");
+  inline const auto ExprParens = TokenDef("rego-exprparens");
+  inline const auto UnaryExpr = TokenDef("rego-unaryexpr");
+  inline const auto NotExpr = TokenDef("rego-not-expr");
+  inline const auto Term = TokenDef("rego-term");
+  inline const auto InfixOperator = TokenDef("rego-infixoperator");
+  inline const auto BoolOperator = TokenDef("rego-booloperator");
+  inline const auto ArithOperator = TokenDef("rego-arithoperator");
+  inline const auto AssignOperator = TokenDef("rego-assignoperator");
+  inline const auto BinOperator = TokenDef("rego-binoperator");
+  inline const auto Ref = TokenDef("rego-ref");
+  inline const auto RefArgBrack = TokenDef("rego-refargbrack");
+  inline const auto RefArgDot = TokenDef("rego-refargdot");
+  inline const auto Var = TokenDef("rego-var", flag::print);
+  inline const auto Scalar = TokenDef("rego-scalar");
+  inline const auto String = TokenDef("rego-string");
+  inline const auto Array = TokenDef("rego-array");
+  inline const auto Object = TokenDef("rego-object");
+  inline const auto Set = TokenDef("rego-set");
+  inline const auto ObjectItem = TokenDef("rego-objectitem");
+  inline const auto RawString = TokenDef("rego-rawstring", flag::print);
+  inline const auto JSONString = TokenDef("rego-STRING", flag::print);
+  inline const auto Int = TokenDef("rego-INT", flag::print);
+  inline const auto Float = TokenDef("rego-FLOAT", flag::print);
+  inline const auto True = TokenDef("rego-true");
+  inline const auto False = TokenDef("rego-false");
+  inline const auto Null = TokenDef("rego-null");
+  inline const auto Equals = TokenDef("rego-equals");
+  inline const auto NotEquals = TokenDef("rego-notequals");
+  inline const auto LessThan = TokenDef("rego-lessthan");
+  inline const auto GreaterThan = TokenDef("rego-greaterthan");
+  inline const auto LessThanOrEquals = TokenDef("rego-lessthanorequals");
+  inline const auto GreaterThanOrEquals = TokenDef("rego-greaterthanorequals");
+  inline const auto Add = TokenDef("rego-add");
+  inline const auto Subtract = TokenDef("rego-subtract");
+  inline const auto Multiply = TokenDef("rego-multiply");
+  inline const auto Divide = TokenDef("rego-divide");
+  inline const auto Modulo = TokenDef("rego-modulo");
+  inline const auto And = TokenDef("rego-and");
+  inline const auto Or = TokenDef("rego-or");
+  inline const auto Assign = TokenDef("rego-assign");
+  inline const auto Unify = TokenDef("rego-unify");
+  inline const auto Default = TokenDef("rego-default");
+  inline const auto Some = TokenDef("rego-some");
+  inline const auto SomeDecl = TokenDef("rego-somedecl");
+  inline const auto If = TokenDef("rego-if");
+  inline const auto IsIn = TokenDef("rego-in");
+  inline const auto Contains = TokenDef("rego-contains");
+  inline const auto Else = TokenDef("rego-else");
+  inline const auto As = TokenDef("rego-as");
+  inline const auto With = TokenDef("rego-with");
+  inline const auto Every = TokenDef("rego-every");
+  inline const auto ArrayCompr = TokenDef("rego-arraycompr");
+  inline const auto ObjectCompr = TokenDef("rego-objectcompr");
+  inline const auto SetCompr = TokenDef("rego-setcompr");
+  inline const auto Membership = TokenDef("rego-membership");
+  inline const auto Not = TokenDef("rego-not");
   inline const auto Import =
-    TokenDef("import", flag::lookdown | flag::lookup | flag::shadowing);
-  inline const auto Keyword =
-    TokenDef("keyword", flag::lookdown | flag::lookup);
-  inline const auto Idx = TokenDef("idx");
-  inline const auto Idx1 = TokenDef("idx-1");
-  inline const auto Skip = TokenDef("skip", flag::lookup);
-  inline const auto NestedBody = TokenDef("nested-body", flag::symtab);
-  inline const auto BuiltInHook = TokenDef("builtin-hook", flag::lookup);
-  inline const auto Item = TokenDef("item");
-  inline const auto Item1 = TokenDef("item-1");
-  inline const auto Enumerate = TokenDef("enumerate");
-  inline const auto Compr = TokenDef("compr");
-  inline const auto Merge = TokenDef("merge");
-  inline const auto ImportRef = TokenDef("import-ref");
-  inline const auto WithExpr = TokenDef("with-expr");
-  inline const auto EverySeq = TokenDef("every-seq");
-  inline const auto ErrorCode = TokenDef("error-code", flag::print);
-  inline const auto ErrorSeq = TokenDef("error-seq");
-
-  // lists
-  inline const auto List = TokenDef("list");
-
-  // parser
-  inline const auto Brace = TokenDef("brace");
-  inline const auto Dot = TokenDef("dot");
-  inline const auto Colon = TokenDef("colon");
-  inline const auto Square = TokenDef("square");
-  inline const auto Paren = TokenDef("paren");
-  inline const auto Not = TokenDef("not");
-  inline const auto Comma = TokenDef("comma");
-  inline const auto Placeholder = TokenDef("_");
-
-  using namespace wf::ops;
+    TokenDef("rego-import", flag::lookdown | flag::lookup | flag::shadowing);
+  inline const auto Placeholder = TokenDef("rego-placeholder");
 
-  inline const auto wf_json = JSONString | Int | Float | True | False | Null;
+  // intermediate tokens
+  inline const auto RuleBodySeq = TokenDef("rego-rulebodyseq");
+  inline const auto ImportSeq = TokenDef("rego-importseq");
+  inline const auto RuleRef = TokenDef("rego-ruleref");
+  inline const auto RuleHeadType = TokenDef("rego-ruleheadtype");
+  inline const auto WithSeq = TokenDef("rego-withseq");
+  inline const auto TermSeq = TokenDef("rego-termseq");
+  inline const auto ExprSeq = TokenDef("rego-exprseq");
+  inline const auto VarSeq = TokenDef("rego-varseq");
+  inline const auto RefHead = TokenDef("rego-refhead");
+  inline const auto RefArgSeq = TokenDef("rego-refargseq");
+  inline const auto Key = TokenDef("rego-key", flag::print);
+  inline const auto Val = TokenDef("rego-value");
+  inline const auto Undefined = TokenDef("rego-undefined");
+
+  // other tokens
+  inline const auto Result = TokenDef("rego-result", flag::symtab);
+  inline const auto Binding = TokenDef("rego-binding", flag::lookdown);
+  inline const auto ErrorCode = TokenDef("rego-errorcode");
+  inline const auto ErrorSeq = TokenDef("rego-errorseq");
 
+  inline const auto wf_assign_op = Assign | Unify;
   inline const auto wf_arith_op = Add | Subtract | Multiply | Divide | Modulo;
-
   inline const auto wf_bin_op = And | Or | Subtract;
-
   inline const auto wf_bool_op = Equals | NotEquals | LessThan |
     LessThanOrEquals | GreaterThan | GreaterThanOrEquals | Not;
-
-  inline const auto wf_assign_op = Assign | Unify;
-
-  inline const auto wf_parse_tokens = wf_json | wf_arith_op | wf_bool_op |
-    wf_bin_op | Package | Var | Brace | Square | Dot | Paren | Assign | Unify |
-    EmptySet | Colon | RawString | Default | Some | Import | Else | As | With |
-    Placeholder;
+  inline const auto wf_exprs =
+    Term | ExprCall | ExprInfix | ExprEvery | ExprParens | UnaryExpr;
 
   // clang-format off
-  inline const auto wf_parser =
-      (Top <<= Rego)
-    | (Rego <<= Query * Input * DataSeq * ModuleSeq)
-    | (Query <<= Group++)
-    | (Input <<= File | Undefined)
-    | (ModuleSeq <<= File++)
-    | (DataSeq <<= File++)
-    | (File <<= Group++)
-    | (Brace <<= (List | Group)++)
-    | (Paren <<= (Group | List))
-    | (Square <<= (Group | List)++)
-    | (List <<= Group++)
-    | (Group <<= wf_parse_tokens++)
-    | (Some <<= (List | Group)++)
-    | (With <<= Group * Group)
-    | (Error <<= ErrorMsg * ErrorAst * ErrorCode)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_input_data =
-    wf_parser
-    | (DataSeq <<= Data++)
-    | (Input <<= Key * (Val >>= Group | Undefined))[Key]
-    | (Data <<= Brace)
-    ;
-  // clang-format on
-
-  inline const auto wf_modules_tokens =
-    wf_parse_tokens - (Package | Colon | Import | Placeholder);
-
-  // clang-format off
-  inline const auto wf_pass_modules =
-    wf_pass_input_data
-    | (ModuleSeq <<= Module++)
+  inline const auto wf =
+      (Top <<= Query | Module)
     | (Module <<= Package * ImportSeq * Policy)
-    | (Package <<= Group)
     | (ImportSeq <<= Import++)
-    | (Import <<= Group)
-    | (Keyword <<= Var)
-    | (Policy <<= Group++)
-    | (List <<= (Group | ObjectItem)++)
-    | (Brace <<= (List | Group)++)
-    | (ObjectItem <<= Group * Group)
-    | (Group <<= wf_modules_tokens++)
-    | (Square <<= (Group | List)++)
-    ;
-  // clang-format on
-
-  inline const auto wf_imports_tokens = wf_modules_tokens - As;
-
-  // clang-format off
-  inline const auto wf_pass_imports =
-    wf_pass_modules
-    | (ImportSeq <<= (Import | Keyword)++)
-    | (Keyword <<= Var)[Var]
-    | (Import <<= ImportRef * As * (Val >>= Var | Undefined))
-    | (ImportRef <<= Group)
-    | (With <<= RuleRef * WithExpr)
-    | (RuleRef <<= Group)
-    | (WithExpr <<= Group)
-    | (Group <<= wf_imports_tokens++)
-    ;
-  // clang-format off    
-
-  inline const auto wf_keywords_tokens = wf_imports_tokens | IsIn | Contains | Every | If;
-
-  // clang-format off
-  inline const auto wf_pass_keywords =
-    wf_pass_imports
-    | (Group <<= wf_keywords_tokens++)
-    ;
-  // clang-format on
-
-  inline const auto wf_lists_tokens =
-    (wf_keywords_tokens - (Some | Every | EmptySet | Brace | Square)) |
-    UnifyBody | ObjectItemSeq | Array | Object | Set | ExprEvery | SomeDecl |
-    ObjectCompr | ArrayCompr | SetCompr | Comma | Undefined;
-
-  // clang-format off
-  inline const auto wf_pass_lists =
-    wf_pass_keywords
-    | (Object <<= ObjectItem++)
-    | (ObjectItemSeq <<= ObjectItem++)
-    | (ObjectItem <<= Group * Group)
-    | (Array <<= Group++)
-    | (Set <<= Group++)
-    | (UnifyBody <<= (SomeDecl | Group)++)
-    | (Input <<= Key * (Val >>= Group | Undefined))[Key]
-    | (Data <<= ObjectItemSeq)
-    | (Group <<= wf_lists_tokens++)
-    | (List <<= Group++)
-    | (SomeDecl <<= VarSeq * Group)
-    | (ExprEvery <<= VarSeq * UnifyBody * EverySeq)
-    | (EverySeq <<= Group)
-    | (VarSeq <<= Group++)
-    | (ObjectCompr <<= Group * Group * UnifyBody)
-    | (ArrayCompr <<= Group * UnifyBody)
-    | (SetCompr <<= Group * UnifyBody)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_ifs = wf_pass_lists;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_elses =
-    wf_pass_ifs
-    | (Else <<= (Val >>= Group) * (Body >>= UnifyBody | Empty))
-    ;
-  // clang-format on
-
-  inline const auto wf_rules_tokens = wf_lists_tokens - (If | Else | Default);
-
-  // clang-format off
-  inline const auto wf_pass_rules =
-    wf_pass_elses
-    | (Policy <<= Rule++)
-    | (Rule <<= (Default >>= True | False) * RuleHead * (Body >>= UnifyBody | Empty) * ElseSeq)
-    | (RuleHead <<= RuleRef * (RuleHeadType >>= RuleHeadComp | RuleHeadFunc | RuleHeadSet | RuleHeadObj))
-    | (RuleRef <<= (Var | Array | Dot)++[1])
-    | (ElseSeq <<= Else++)
-    | (Else <<= (Val >>= Group) * (Body >>= UnifyBody | Empty))
-    | (RuleHeadComp <<= AssignOperator * Group)
-    | (RuleHeadFunc <<= RuleArgs * AssignOperator * Group)
-    | (RuleHeadSet <<= Group)
-    | (RuleHeadObj <<= Group * AssignOperator * Group)
-    | (RuleArgs <<= Group++)
-    | (AssignOperator <<= wf_assign_op)
-    | (Group <<= wf_rules_tokens++)
-    ;
-  // clang-format on
-
-  inline const auto wf_call_tokens = wf_rules_tokens | ExprCall;
-
-  // clang-format off
-  inline const auto wf_pass_build_calls =
-    wf_pass_rules
-    | (ExprCall <<= RuleRef * ArgSeq)
-    | (ArgSeq <<= Group++)
-    | (Group <<= wf_call_tokens++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_membership_tokens =
-    (wf_call_tokens - Comma) | Membership;
-
-  // clang-format off
-  inline const auto wf_pass_membership =
-    wf_pass_build_calls
-    | (Membership <<= (Idx >>= Group | Undefined) * (Item >>= Group) * Group)
-    | (Group <<= wf_membership_tokens++[1])
-    ;
-  // clang-format off
-
-  inline const auto wf_refs_tokens = wf_membership_tokens | Ref;
-
-  // clang-format off
-  inline const auto wf_pass_build_refs =
-    wf_pass_membership
-    | (Ref <<= RefHead * RefArgSeq)
-    | (RefHead <<= Var | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr | ExprCall)
-    | (RefArgSeq <<= (RefArgDot | RefArgBrack)++)
-    | (RefArgDot <<= Var)
-    | (RefArgBrack <<= Group)
-    | (RuleRef <<= (Var | Array | Dot | Ref)++[1])
-    | (Group <<= wf_refs_tokens++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_structure_exprs = Term | wf_arith_op | wf_bin_op |
-    wf_bool_op | wf_assign_op | Dot | ExprCall | ExprEvery | Membership | Expr;
-
-  // clang-format off
-  inline const auto wf_pass_structure =
-      (Top <<= Rego)
-    | (Rego <<= Query * Input * DataSeq * ModuleSeq)
-    | (Input <<= Key * (Val >>= Term | Undefined))[Key]
-    | (DataSeq <<= Data++)
-    | (Data <<= ObjectItemSeq)
-    | (ObjectItemSeq <<= ObjectItem++)
-    | (ModuleSeq <<= Module++)
-    | (Query <<= Literal++[1])
-    | (Error <<= ErrorMsg * ErrorAst * ErrorCode)
-    | (Module <<= Package * ImportSeq * Policy)
-    | (ImportSeq <<= (Import | Keyword)++)
-    | (Import <<= Ref * As * Var)
-    | (Keyword <<= Var)[Var]
+    | (Import <<= Ref * Var)
     | (Package <<= Ref)
     | (Policy <<= Rule++)
-    | (Rule <<= (Default >>= True | False) * RuleHead * (Body >>= UnifyBody | Empty) * ElseSeq)
-    | (RuleHead <<= RuleRef * (RuleHeadType >>= RuleHeadComp | RuleHeadFunc | RuleHeadSet | RuleHeadObj))
-    | (RuleRef <<= Ref | Var)
-    | (ElseSeq <<= Else++)
-    | (Else <<= (Val >>= Expr) * (Body >>= UnifyBody | Empty))
-    | (RuleHeadComp <<= AssignOperator * Expr)
-    | (RuleHeadFunc <<= RuleArgs * AssignOperator * Expr)
+    | (Rule <<= (Default >>= True | False) * RuleHead * RuleBodySeq)
+    | (RuleHead <<= RuleRef * (RuleHeadType >>= (RuleHeadSet | RuleHeadObj | RuleHeadFunc | RuleHeadComp)))
+    | (RuleRef <<= Var | Ref)
+    | (RuleHeadComp <<= Expr)
+    | (RuleHeadObj <<= Expr * Expr)
+    | (RuleHeadFunc <<= RuleArgs * Expr)
     | (RuleHeadSet <<= Expr)
-    | (RuleHeadObj <<= Expr * AssignOperator * Expr)
     | (RuleArgs <<= Term++)
-    | (UnifyBody <<= (Literal | LiteralWith)++[1])
-    | (Literal <<= Expr | SomeDecl | SomeExpr)
-    | (LiteralWith <<= UnifyBody * WithSeq)
+    | (RuleBodySeq <<= (Else | Query)++)
+    | (Else <<= Expr * Query)
+    | (Query <<= Literal++[1])
+    | (Literal <<= (Expr >>= SomeDecl | Expr | NotExpr) * WithSeq)
     | (WithSeq <<= With++)
-    | (With <<= RuleRef * Expr)
-    | (SomeDecl <<= VarSeq)
-    | (SomeExpr <<= (Idx >>= Expr | Undefined) * (Item >>= Expr) * IsIn)
-    | (VarSeq <<= Var++)
-    | (IsIn <<= Expr | Undefined)
-    | (Expr <<= wf_structure_exprs++[1])
-    | (ExprCall <<= RuleRef * ArgSeq)
-    | (ExprEvery <<= VarSeq * UnifyBody * IsIn)
-    | (ArgSeq <<= Expr++)
-    | (AssignOperator <<= wf_assign_op)
-    | (Term <<= Ref | Var | Scalar | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr)
+    | (With <<= Term * Expr)
+    | (SomeDecl <<= ExprSeq * (IsIn >>= Expr | Undefined))
+    | (NotExpr <<= Expr)
+    | (Expr <<= (Term | ExprCall | ExprInfix | ExprEvery | ExprParens | UnaryExpr))
+    | (ExprCall <<= Ref * ExprSeq)
+    | (ExprSeq <<= Expr++)
+    | (ExprInfix <<= Expr * InfixOperator * Expr)
+    | (ExprEvery <<= VarSeq * (IsIn >>= Term | ExprCall | ExprInfix) * Query)
+    | (VarSeq <<= Var++[1])
+    | (ExprParens <<= Expr)
+    | (UnaryExpr <<= Expr)
+    | (Membership <<= ExprSeq * Expr)
+    | (Term <<= Ref | Var | Scalar | Array | Object | Set | Membership | ArrayCompr | ObjectCompr | SetCompr)
+    | (ArrayCompr <<= Expr * Query)
+    | (SetCompr <<= Expr * Query)
+    | (ObjectCompr <<= Expr * Expr * Query)
+    | (InfixOperator <<= AssignOperator | BoolOperator | ArithOperator | BinOperator)
+    | (BoolOperator <<= Equals | NotEquals | LessThan | GreaterThan | LessThanOrEquals | GreaterThanOrEquals)
+    | (ArithOperator <<= Add | Subtract | Multiply | Divide | Modulo)
+    | (BinOperator <<= And | Or)
+    | (AssignOperator <<= Assign | Unify)
     | (Ref <<= RefHead * RefArgSeq)
     | (RefHead <<= Var | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr | ExprCall)
     | (RefArgSeq <<= (RefArgDot | RefArgBrack)++)
+    | (RefArgBrack <<= ExprInfix | Scalar | Var | Array | Object | Set | Placeholder)
     | (RefArgDot <<= Var)
-    | (RefArgBrack <<= Scalar | Var | Object | Array | Set | Expr)
     | (Scalar <<= String | Int | Float | True | False | Null)
     | (String <<= JSONString | RawString)
     | (Array <<= Expr++)
-    | (Set <<= Expr++)
     | (Object <<= ObjectItem++)
     | (ObjectItem <<= (Key >>= Expr) * (Val >>= Expr))
-    | (ObjectCompr <<= Expr * Expr * (Body >>= UnifyBody))
-    | (ArrayCompr <<= Expr * (Body >>= UnifyBody))
-    | (SetCompr <<= Expr * (Body >>= UnifyBody))
-    | (Membership <<= (Idx >>= Expr | Undefined) * (Item >>= Expr) * (ItemSeq >>= Expr))
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_strings =
-    wf_pass_structure
-    | (Scalar <<= JSONString | Int | Float | True | False | Null)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_merge_data =
-    wf_pass_strings
-    | (Rego <<= Query * Input * Data * ModuleSeq)
-    | (Input <<= Key * (Val >>= DataTerm | Undefined))[Key]
-    | (Data <<= Key * (Val >>= DataModule))[Key]
-    | (DataModule <<= (DataRule | Submodule)++)
-    | (DataRule <<= Var * (Val >>= DataTerm))
-    | (Submodule <<= Key * (Val >>= DataModule))[Key]
-    | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
-    | (DataArray <<= DataTerm++)
-    | (DataSet <<= DataTerm++)
-    | (DataObject <<= DataObjectItem++)
-    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
-    | (RuleArgs <<= (ArgVar | ArgVal)++)
-    | (ArgVar <<= Var * (Val >>= Undefined))[Var]
-    | (ArgVal <<= Scalar | Array | Object | Set)
-    ;
-  // clang-format on
-
-  inline const auto wf_pass_lift_refheads = wf_pass_merge_data;
-
-  inline const auto wf_symbols_exprs =
-    (wf_structure_exprs - (Assign | Dot | ExprEvery)) | RefTerm | NumTerm |
-    Set | SetCompr;
-
-  // clang-format off
-  inline const auto wf_pass_symbols =
-    wf_pass_lift_refheads
-    | (Module <<= Package * 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))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | Term) * (Val >>= Expr | Term))[Var]
-    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum)++[1])
-    | (LiteralEnum <<= (Item >>= Var) * (ItemSeq >>= Expr))
-    | (Query <<= (Body >>= UnifyBody))
-    | (Local <<= Var * (Val >>= Undefined))[Var]
-    | (Literal <<= Expr | SomeExpr)
-    | (Term <<= Scalar | Array | Object | Set | ArrayCompr | SetCompr | ObjectCompr)
-    | (ObjectCompr <<= Expr * Expr * (Body >>= NestedBody))
-    | (ArrayCompr <<= Expr * (Body >>= NestedBody))
-    | (SetCompr <<= Expr * (Body >>= NestedBody))
-    | (RefTerm <<= Ref | Var)
-    | (NumTerm <<= Int | Float)
-    | (RefArgBrack <<= RefTerm | Scalar | Object | Array | Set | Expr)
-    | (Expr <<= wf_symbols_exprs++[1])
-    | (Import <<= Var * Ref)[Var]
-    | (NestedBody <<= Key * (Val >>= UnifyBody))
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_replace_argvals =
-    wf_pass_symbols
-    | (RuleArgs <<= ArgVar++)
-    | (Literal <<= Expr)
-    ;
-  //
-
-  // clang-format off
-  inline const auto wf_pass_lift_query =
-    wf_pass_replace_argvals
-    | (Query <<= Var)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_expand_imports = wf_pass_lift_query;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_constants =
-    wf_pass_lift_query
-    | (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))[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Key >>= Expr | DataTerm) * (Val >>= Expr | DataTerm))[Var]
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_explicit_enums = 
-    wf_pass_constants
-    | (LiteralEnum <<= (Item >>= Var) * (ItemSeq >>= Var) * UnifyBody);
-  // clang-format on
-
-  inline const auto wf_pass_locals = wf_pass_explicit_enums;
-
-  // 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]
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_compr =
-    wf_pass_rules_to_compr
-    | (ObjectCompr <<= Var * NestedBody)
-    | (ArrayCompr <<= Var * NestedBody)
-    | (SetCompr <<= Var * NestedBody)
-    ;
-  // clang-format on
-
-  inline const auto wf_pass_absolute_refs = wf_pass_compr;
-
-  // clang-format off
-  inline const auto wf_pass_merge_modules =
-    wf_pass_absolute_refs
-    | (Rego <<= Query * Input * Data)
-    | (DataModule <<= (DataRule | RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
-    | (Submodule <<= Key * (Val >>= DataModule))[Key]
-    | (DataItem <<= Key * (Val >>= DataModule))[Key]
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_datarule = 
-    wf_pass_merge_modules
-    | (DataModule <<= (RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
-    | (Rego <<= Query * Input * Data)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_skips = 
-    wf_pass_datarule
-    | (Rego <<= Query * Input * Data * SkipSeq)
-    | (SkipSeq <<= Skip++)
-    | (Skip <<= Key * (Val >>= VarSeq | BuiltInHook | Undefined))[Key]
-    ;
-  // clang-format on
-
-  inline const auto wf_unary_exprs = wf_symbols_exprs | UnaryExpr;
-
-  // clang-format off
-  inline const auto wf_pass_unary =
-    wf_pass_skips
-    | (UnaryExpr <<= ArithArg)
-    | (ArithArg <<= (Expr | RefTerm | NumTerm | UnaryExpr | ExprCall))
-    | (Expr <<= wf_unary_exprs++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_math_tokens =
-    RefTerm | NumTerm | UnaryExpr | ArithInfix | ExprCall;
-
-  inline const auto wf_bin_tokens =
-    RefTerm | Set | SetCompr | ExprCall | BinInfix;
-
-  inline const auto wf_multiply_divide_exprs =
-    (wf_unary_exprs - (Multiply | Divide | Modulo | And)) | ArithInfix |
-    BinInfix;
-
-  // clang-format off
-  inline const auto wf_pass_multiply_divide =
-    wf_pass_unary
-    | (ArithInfix <<= ArithArg * (Op >>= Multiply | Divide | Modulo) * ArithArg)
-    | (ArithArg <<= Expr | wf_math_tokens)
-    | (BinInfix <<= BinArg * (Op >>= And) * BinArg)
-    | (BinArg <<= (Expr | wf_bin_tokens)++[1])
-    | (UnaryExpr <<= ArithArg)
-    | (Expr <<= wf_multiply_divide_exprs++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_add_subtract_exprs =
-    wf_multiply_divide_exprs - (Add | Subtract | Or);
-
-  // clang-format off
-  inline const auto wf_pass_add_subtract =
-    wf_pass_multiply_divide
-    | (ArithInfix <<= ArithArg * (Op >>= wf_arith_op) * ArithArg)
-    | (ArithArg <<= Expr | wf_math_tokens)
-    | (BinInfix <<= BinArg * (Op >>= wf_bin_op) * BinArg)
-    | (BinArg <<= Expr | wf_bin_tokens)
-    | (Expr <<= wf_add_subtract_exprs++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_comparison_exprs =
-    (wf_add_subtract_exprs - wf_bool_op) | BoolInfix;
-
-  // clang-format off
-  inline const auto wf_pass_comparison =
-    wf_pass_add_subtract
-    | (BoolInfix <<= BoolArg * (Op >>= wf_bool_op) * BoolArg)
-    | (BoolArg <<= Term | BinInfix | wf_math_tokens)
-    | (ArithArg <<= wf_math_tokens)
-    | (BinArg <<= wf_bin_tokens)
-    | (Expr <<= wf_comparison_exprs++[1])
-    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot)++[1])
-    | (LiteralNot <<= UnifyBody)
-    ;
-  // clang-format on
-
-  inline const auto wf_assign_exprs =
-    (wf_comparison_exprs - (Unify | Expr | Set | SetCompr)) | AssignInfix;
-
-  // clang-format off
-  inline const auto wf_pass_assign =
-    wf_pass_comparison
-    | (AssignInfix <<= AssignArg * AssignArg)
-    | (AssignArg <<= wf_math_tokens | Term | BinInfix | BoolInfix | Membership)
-    | (Expr <<= wf_assign_exprs++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_pass_skip_refs = wf_pass_assign;
-
-  // clang-format off
-  inline const auto wf_pass_simple_refs =
-    wf_pass_skip_refs
-    | (RefTerm <<= Var | SimpleRef)
-    | (SimpleRef <<= Var * (Op >>= RefArgDot | RefArgBrack))
-    | (Expr <<= wf_assign_exprs)
-    | (ExprCall <<= Var * ArgSeq)
-    | (RefHead <<= Var)
-    | (RuleRef <<= Var)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_init =
-    wf_pass_simple_refs
-    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot | LiteralInit)++[1])
-    | (LiteralInit <<= (Lhs >>= VarSeq) * (Rhs >>= VarSeq) * AssignInfix)
-    ;
-  // clang-format on
-
-  inline const auto wf_pass_implicit_enums = wf_pass_init;
-  inline const auto wf_pass_enum_locals = wf_pass_implicit_enums;
-
-  inline const auto wf_rulebody_exprs = (wf_assign_exprs - AssignInfix);
-
-  // clang-format off
-  inline const auto wf_pass_rulebody =
-    wf_pass_enum_locals
-    | (Module <<= (Import | RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
-    | (UnifyExpr <<= Var * (Val >>= Expr))
-    | (Expr <<= wf_rulebody_exprs)
-    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprCompr | UnifyExprEnum | UnifyExprNot)++[1])
-    | (UnifyExprWith <<= UnifyBody * WithSeq)
-    | (UnifyExprCompr <<= Var * (Val >>= ArrayCompr | SetCompr | ObjectCompr) * NestedBody)
-    | (UnifyExprEnum <<= Var * (Item >>= Var) * (ItemSeq >>= Var) * UnifyBody)
-    | (UnifyExprNot <<= UnifyBody)
-    | (ArrayCompr <<= Var)
-    | (SetCompr <<= Var)
-    | (ObjectCompr <<= Var)
-    | (With <<= RuleRef * Var)
-    ;
-  // clang-format on
-
-  const inline auto wf_lift_to_rule_exprs =
-    wf_rulebody_exprs | Enumerate | ArrayCompr | SetCompr | ObjectCompr | Merge;
-
-  // clang-format off
-  inline const auto wf_pass_lift_to_rule =
-    wf_pass_rulebody
-    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprNot)++[1])
-    | (Expr <<= wf_lift_to_rule_exprs)
-    | (Merge <<= Var)
-    | (Enumerate <<= Expr)
+    | (Set <<= Expr++)
     ;
   // clang-format on
 
   // clang-format off
-  inline const auto wf_pass_functions =
-    wf_pass_lift_to_rule
-    | (UnifyExpr <<= Var * (Val >>= Var | Scalar | Array | Set | Object | Function))
-    | (Function <<= JSONString * ArgSeq)
-    | (ArgSeq <<= (Scalar | Object | Array | Set | Var | wf_arith_op | wf_bin_op | wf_bool_op | NestedBody | VarSeq)++)
-    | (Input <<= Key * (Val >>= Term | Undefined))[Key]
+  inline const auto wf_result =
+    (Top <<= Result)
+    | (Result <<= (Binding | Term)++)
+    | (Binding <<= Var * Term)[Var]
+    | (Term <<= Scalar | Array | Object | Set)
     | (Array <<= Term++)
     | (Set <<= Term++)
     | (Object <<= ObjectItem++)
     | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
-    | (DataItem <<= Key * (Val >>= DataModule | Term))[Key]
-    | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int) * Key)[Var]
-    | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * (Idx >>= Int) * Key)[Var]
-    | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Key)[Var]
-    | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * Key)[Var]
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_unify =
-    wf_pass_functions
-    | (Query <<= (Term | Binding)++)
-    | (Binding <<= Var * Term)[Var]
-    | (Term <<= Scalar | Array | Object | Set)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_query =
-    wf_pass_unify
-    | (Top <<= (Binding | Term)++)
+    | (Scalar <<= JSONString | Int | Float | True | False | Null)
+    | (Error <<= ErrorMsg * ErrorAst * ErrorCode)
     ;
   // clang-format on
 
@@ -1015,6 +426,16 @@ namespace rego
    */
   double get_double(const Node& node);
 
+  /**
+   * Attempts to extract the value of a node as an integer.
+   * In the case that the node is a double, it will check if the
+   * double is integral, i.e. 5.0 will be considered the integer 5.
+   *
+   * @param node The node to extract from.
+   * @return The integer value of the node
+   */
+  std::optional<BigInt> try_get_int(const Node& node);
+
   /**
    * Extracts the value of a node as a string.
    *
@@ -1195,11 +616,11 @@ namespace rego
    * Manages the set of builtins used by an interpreter to resolve built-in
    * calls.
    */
-  class BuiltIns
+  class BuiltInsDef
   {
   public:
     /** Constructor. */
-    BuiltIns() noexcept;
+    BuiltInsDef() noexcept;
 
     /**
      * Determines whether the provided name refers to a built-in.
@@ -1223,7 +644,7 @@ namespace rego
      *
      * @param built_in The built-in to register.
      */
-    BuiltIns& register_builtin(const BuiltIn& built_in);
+    BuiltInsDef& register_builtin(const BuiltIn& built_in);
 
     /**
      * Gets the built-in with the provided name.
@@ -1237,7 +658,7 @@ namespace rego
      * built-in errors will result in Undefined nodes.
      */
     bool strict_errors() const;
-    BuiltIns& strict_errors(bool strict_errors);
+    BuiltInsDef& strict_errors(bool strict_errors);
 
     /**
      * Registers a set of built-ins.
@@ -1245,7 +666,7 @@ namespace rego
      * @param built_ins The built-ins to register.
      */
     template <typename T>
-    BuiltIns& register_builtins(const T& built_ins)
+    BuiltInsDef& register_builtins(const T& built_ins)
     {
       for (auto& built_in : built_ins)
       {
@@ -1266,7 +687,12 @@ namespace rego
      * rego-cpp supports the following built-ins as standard:
      *
      */
-    BuiltIns& register_standard_builtins();
+    BuiltInsDef& register_standard_builtins();
+
+    /**
+     * Creates the standard builtin set.
+     */
+    static std::shared_ptr<BuiltInsDef> create();
 
     std::map<Location, BuiltIn>::const_iterator begin() const;
     std::map<Location, BuiltIn>::const_iterator end() const;
@@ -1276,6 +702,8 @@ namespace rego
     bool m_strict_errors;
   };
 
+  using BuiltIns = std::shared_ptr<BuiltInsDef>;
+
   const std::string EvalTypeError = "eval_type_error";
   const std::string EvalBuiltInError = "eval_builtin_error";
   const std::string RegoTypeError = "rego_type_error";
@@ -1307,20 +735,6 @@ namespace rego
     const std::string& msg,
     const std::string& code = WellFormedError);
 
-  using PassCheck = std::tuple<std::string, Pass, const wf::Wellformed*>;
-
-  /**
-   * Creates the parser object, which turns a document into tokens.
-   *
-   * @return The parser.
-   */
-  Parse parser();
-
-  /**
-   * @return std::vector<Pass> that is used to process a rego query.
-   */
-  std::vector<Pass> passes(const BuiltIns& builtins);
-
   /**
    * Returns a node representing the version of the library.
    *
@@ -1332,13 +746,12 @@ namespace rego
    */
   Node version();
 
-  /** Converts a node to JSON.
+  /** Converts a node to a unique key representation.
    *
    * @param node The node to convert.
-   * @param set_as_array Whether to represent a set using array notation.
-   * Otherwise, sets are denoted using "<>" delimiters.
+   * @param set_as_array Whether to represent sets as arrays.
    */
-  std::string to_json(
+  std::string to_key(
     const trieste::Node& node,
     bool set_as_array = false,
     bool sort_arrays = false);
@@ -1364,6 +777,15 @@ namespace rego
    */
   void set_log_level(LogLevel level);
 
+  /**
+   * Sets the logging level for the library.
+   *
+   * @param level The logging level. One of "None", "Error", "Output", "Warn",
+   *              "Info", "Debug", or "Trace".
+   * @return Error message if the level is invalid, otherwise empty string.
+   */
+  std::string set_log_level_from_string(const std::string& level);
+
   /**
    * This class forms the main interface to the Rego library.
    *
@@ -1384,18 +806,9 @@ namespace rego
   public:
     /**
      * Constructor.
-     *
-     * Pushes the default WF context.
      */
     Interpreter();
 
-    /**
-     * Destructor.
-     *
-     * Pops the default WF context.
-     */
-    ~Interpreter();
-
     /**
      * Adds a module (i.e. virtual document) file to the interpreter.
      *
@@ -1404,7 +817,7 @@ namespace rego
      *
      * @param path The path to the module file.
      */
-    void add_module_file(const std::filesystem::path& path);
+    bool add_module_file(const std::filesystem::path& path);
 
     /**
      * Adds a module (i.e. virtual document) to the interpreter.
@@ -1414,7 +827,7 @@ namespace rego
      * @param name The name of the module.
      * @param contents The contents of the module.
      */
-    void add_module(const std::string& name, const std::string& contents);
+    bool add_module(const std::string& name, const std::string& contents);
 
     /**
      * Adds a base document to the interpreter.
@@ -1424,7 +837,7 @@ namespace rego
      *
      * @param module The module to add.
      */
-    void add_data_json_file(const std::filesystem::path& path);
+    bool add_data_json_file(const std::filesystem::path& path);
 
     /**
      * Adds a base document to the interpreter.
@@ -1434,7 +847,7 @@ namespace rego
      *
      * @param json The contents of the document.
      */
-    void add_data_json(const std::string& json);
+    bool add_data_json(const std::string& json);
 
     /**
      * Adds a base document to the interpreter.
@@ -1444,7 +857,7 @@ namespace rego
      *
      * @param node The contents of the document.
      */
-    void add_data(const Node& node);
+    bool add_data(const Node& node);
 
     /**
      * Sets the input document to the interpreter.
@@ -1454,18 +867,16 @@ namespace rego
      *
      * @param path The path to the input file.
      */
-    void set_input_json_file(const std::filesystem::path& path);
+    bool set_input_json_file(const std::filesystem::path& path);
 
     /**
-     * Sets the input document to the interpreter.
+     * Sets the input term of the interpreter.
      *
-     * The document must contain a single JSON-encoded value, and will be parsed
-     * and set as the interpreter's input. This can be performed multiple times
-     * and will affect the result of any subsequent queries.
+     * The string must contain a single valid Rego data term.
      *
-     * @param json The contents of the document.
+     * @param term The contents of the term.
      */
-    void set_input_json(const std::string& json);
+    bool set_input_term(const std::string& term);
 
     /**
      * Sets the input document to the interpreter.
@@ -1475,7 +886,7 @@ namespace rego
      *
      * @param node The contents of the document.
      */
-    void set_input(const Node& node);
+    bool set_input(const Node& node);
 
     /**
      * Executes a query against the interpreter.
@@ -1486,7 +897,7 @@ namespace rego
      * @param query_expr The query expression.
      * @return The result of the query.
      */
-    std::string query(const std::string& query_expr) const;
+    std::string query(const std::string& query_expr);
 
     /**
      * Executes a query against the interpreter.
@@ -1498,7 +909,7 @@ namespace rego
      * @param query_expr The query expression.
      * @return The result of the query.
      */
-    Node raw_query(const std::string& query_expr) const;
+    Node raw_query(const std::string& query_expr);
 
     /**
      * The path to the debug directory.
@@ -1525,8 +936,8 @@ namespace rego
      * If true, then the interpreter will perform well-formedness checks after
      * each compiler pass using the well-formedness definitions.
      */
-    Interpreter& well_formed_checks_enabled(bool enabled);
-    bool well_formed_checks_enabled() const;
+    Interpreter& wf_check_enabled(bool enabled);
+    bool wf_check_enabled() const;
 
     /**
      * The built-ins used by the interpreter.
@@ -1534,13 +945,7 @@ namespace rego
      * This object can be used to register custom built-ins created using
      * BuiltInDef::create.
      */
-    BuiltIns& builtins();
-    const BuiltIns& builtins() const;
-
-    /**
-     * The well-formedness of the output Ast from the interpreter.
-     */
-    const trieste::wf::Wellformed& output_wf() const;
+    BuiltIns builtins() const;
 
   private:
     friend const char* ::regoGetError(regoInterpreter* rego);
@@ -1548,17 +953,48 @@ namespace rego
     friend regoOutput* ::regoQuery(
       regoInterpreter* rego, const char* query_expr);
 
-    void insert_module(const Node& module);
+    void merge(const Node& ast);
     std::string output_to_string(const Node& output) const;
-    Parse m_parser;
-    Node m_module_seq;
-    Node m_data_seq;
-    Node m_input;
+    Reader m_reader;
+    Node m_ast;
     std::filesystem::path m_debug_path;
-    bool m_debug_enabled;
-    bool m_well_formed_checks_enabled;
     BuiltIns m_builtins;
+    Rewriter m_unify;
+    Reader m_json;
+    Rewriter m_from_json;
+    Rewriter m_to_input;
+    std::size_t m_data_count;
 
     std::string m_c_error;
   };
+
+  /**
+   * Parses Rego queries and virtual documents.
+   */
+  Reader reader();
+
+  /**
+   * Rewrites a Query AST to an input term.
+   */
+  Rewriter to_input();
+
+  /**
+   * Unifies a set of base and virtual documents, an input term, and a query.
+   */
+  Rewriter unify(BuiltIns builtins = BuiltInsDef::create());
+
+  /**
+   * Rewrites a JSON AST to a Rego data input AST.
+   */
+  Rewriter from_json(bool as_term = false);
+
+  /**
+   * Rewrites a Rego binding term to a JSON AST.
+   */
+  Rewriter to_json();
+
+  /**
+   * Rewrites a Rego binding term to a YAML AST.
+   */
+  Rewriter to_yaml();
 }
\ No newline at end of file
diff --git a/include/rego/rego_c.h b/include/rego/rego_c.h
index b075eed7..e6035150 100644
--- a/include/rego/rego_c.h
+++ b/include/rego/rego_c.h
@@ -19,6 +19,7 @@ typedef unsigned int regoSize;
 #define REGO_OK 0
 #define REGO_ERROR 1
 #define REGO_ERROR_BUFFER_TOO_SMALL 2
+#define REGO_ERROR_INVALID_LOG_LEVEL 3
 
 // term node types
 #define REGO_NODE_BINDING 1000
@@ -72,8 +73,21 @@ extern "C"
    *              REGO_LOG_LEVEL_ERROR, REGO_LOG_LEVEL_OUTPUT,
    *              REGO_LOG_LEVEL_WARN, REGO_LOG_LEVEL_INFO,
    *              REGO_LOG_LEVEL_DEBUG, REGO_LOG_LEVEL_TRACE.
+   * @return REGO_OK if successful, REGO_ERROR_INVALID_LOG_LEVEL otherwise.
    */
-  void regoSetLogLevel(regoEnum level);
+  regoEnum regoSetLogLevel(regoEnum level);
+
+  /**
+   * Sets the level of logging.
+   *
+   * This setting controls the amount of logging that will be output to stdout.
+   * The default level is REGO_LOG_LEVEL_NONE.
+   *
+   * @param level One of the following strings: "None", "Error", "Output",
+   *              "Warn", "Info", "Debug", "Trace".
+   * @return REGO_OK if successful, REGO_ERROR_INVALID_LOG_LEVEL otherwise.
+   */
+  regoEnum regoSetLogLevelFromString(const char* level);
 
   /**
    * Allocates and initializes a new Rego interpreter.
@@ -174,12 +188,30 @@ extern "C"
    * If an error code is returned, more error information can be
    * obtained by calling regoGetError.
    *
+   * @deprecated This method's name is misleading, as the input term can be any
+   * valid Rego (i.e. not just JSON). Please use the updated version
+   * `regoSetInputTerm`.
    * @param rego The interpreter.
    * @param contents The contents of the JSON value.
    * @return REGO_OK if successful, REGO_ERROR otherwise.
    */
   regoEnum regoSetInputJSON(regoInterpreter* rego, const char* contents);
 
+  /**
+   * Sets the current input document from the specified string.
+   *
+   * The string should contain a single Rego data term. The value will be
+   * parsed and set as the interpreter's input document.
+   *
+   * If an error code is returned, more error information can be
+   * obtained by calling regoGetError.
+   *
+   * @param rego The interpreter.
+   * @param contents The contents of the Rego data term.
+   * @return REGO_OK if successful, REGO_ERROR otherwise.
+   */
+  regoEnum regoSetInputTerm(regoInterpreter* rego, const char* contents);
+
   /**
    * Sets the debug mode of the interpreter.
    *
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7acd000f..e39bcb22 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -15,47 +15,40 @@ builtins.cc
 internal.cc
 rego_c.cc
 encoding.cc
-passes/input_data.cc
-passes/modules.cc
-passes/lists.cc
-passes/imports.cc
-passes/keywords.cc
-passes/membership.cc
-passes/if_else.cc
-passes/rules.cc
-passes/build_calls.cc
-passes/build_refs.cc
-passes/structure.cc
-passes/strings.cc
-passes/merge_data.cc
-passes/lift_refheads.cc
-passes/symbols.cc
-passes/replace_argvals.cc
-passes/lift_query.cc
-passes/expand_imports.cc
-passes/constants.cc
-passes/enumerate.cc
-passes/locals.cc
-passes/compr.cc
-passes/absolute_refs.cc
-passes/merge_modules.cc
-passes/skips.cc
-passes/arithmetic.cc
-passes/comparison.cc
-passes/assign.cc
-passes/skip_refs.cc
-passes/simple_refs.cc
-passes/init.cc
-passes/rulebody.cc
-passes/lift_to_rule.cc
-passes/functions.cc
-passes/unify.cc
-passes/query.cc
+json.cc
+yaml.cc
+reader.cc
+unify.cc
+unify/strings.cc
+unify/merge_data.cc
+unify/lift_refheads.cc
+unify/symbols.cc
+unify/replace_argvals.cc
+unify/lift_query.cc
+unify/expand_imports.cc
+unify/constants.cc
+unify/enumerate.cc
+unify/locals.cc
+unify/compr.cc
+unify/absolute_refs.cc
+unify/merge_modules.cc
+unify/skips.cc
+unify/infix.cc
+unify/assign.cc
+unify/skip_refs.cc
+unify/simple_refs.cc
+unify/init.cc
+unify/rulebody.cc
+unify/lift_to_rule.cc
+unify/functions.cc
+unify/unify.cc
+unify/result.cc
 builtins/aggregates.cc
 builtins/arrays.cc
 builtins/bits.cc
 builtins/casts.cc
 builtins/encoding.cc
+builtins/graph.cc
 builtins/numbers.cc
 builtins/objects.cc
 builtins/regex.cc
@@ -65,7 +58,6 @@ builtins/strings.cc
 builtins/time.cc
 builtins/types.cc
 builtins/units.cc
-builtins/utf8.cc
 # third-party
 builtins/base64/base64.cpp
 )
@@ -81,13 +73,11 @@ endif()
 target_link_libraries(rego
   PUBLIC
     trieste::trieste
+    trieste::json
+    trieste::yaml
 )
 
 if(MSVC)
-  target_precompile_headers(rego
-    PRIVATE
-      internal.hh
-  )
   target_compile_options(rego PUBLIC "/Zc:__cplusplus")
   target_compile_definitions(rego PUBLIC "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING")
 endif()
diff --git a/src/args.cc b/src/args.cc
index 10b304b6..73810398 100644
--- a/src/args.cc
+++ b/src/args.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
@@ -57,7 +57,7 @@ namespace rego
       std::string sub_separator = "";
       for (auto& value : values)
       {
-        os << sub_separator << to_json(value->node());
+        os << sub_separator << to_key(value->node());
         sub_separator = ", ";
       }
       os << "}";
diff --git a/src/bigint.cc b/src/bigint.cc
index f75aa6bd..2cd9e4fc 100644
--- a/src/bigint.cc
+++ b/src/bigint.cc
@@ -358,7 +358,7 @@ namespace rego
   {
     if (BigInt::less_than(lhs.digits(), rhs.digits()))
     {
-      return BigInt();
+      return lhs;
     }
 
     if (rhs.is_zero())
@@ -367,7 +367,7 @@ namespace rego
     }
 
     auto remainder = BigInt::divide(lhs.digits(), rhs.digits()).remainder;
-    if (lhs.is_negative())
+    if (lhs.is_negative() && remainder != "0")
     {
       remainder.insert(remainder.begin(), '-');
     }
diff --git a/src/builtins.cc b/src/builtins.cc
index 9ea1444a..0bded41f 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -1,5 +1,7 @@
 #include "builtins/builtins.h"
 
+#include "internal.hh"
+
 namespace
 {
   using namespace rego;
@@ -9,11 +11,6 @@ namespace
     return version();
   }
 
-  Node json_marshal(const Nodes& args)
-  {
-    return JSONString ^ to_json(args[0], true);
-  }
-
   Node print(const Nodes& args)
   {
     for (auto arg : args)
@@ -30,7 +27,7 @@ namespace
       args.end(),
       " ",
       [](std::ostream& stream, const Node& n) {
-        stream << to_json(n, true);
+        stream << to_key(n);
         return true;
       })
       << std::endl;
@@ -47,25 +44,25 @@ namespace rego
     return std::make_shared<BuiltInDef>(BuiltInDef{name, arity, behavior});
   }
 
-  BuiltIns::BuiltIns() noexcept : m_strict_errors(false) {}
+  BuiltInsDef::BuiltInsDef() noexcept : m_strict_errors(false) {}
 
-  bool BuiltIns::strict_errors() const
+  bool BuiltInsDef::strict_errors() const
   {
     return m_strict_errors;
   }
 
-  BuiltIns& BuiltIns::strict_errors(bool strict_errors)
+  BuiltInsDef& BuiltInsDef::strict_errors(bool strict_errors)
   {
     m_strict_errors = strict_errors;
     return *this;
   }
 
-  bool BuiltIns::is_builtin(const Location& name) const
+  bool BuiltInsDef::is_builtin(const Location& name) const
   {
     return contains(m_builtins, name);
   }
 
-  Node BuiltIns::call(const Location& name, const Nodes& args) const
+  Node BuiltInsDef::call(const Location& name, const Nodes& args) const
   {
     if (!is_builtin(name))
     {
@@ -102,17 +99,16 @@ namespace rego
     return result;
   }
 
-  BuiltIns& BuiltIns::register_builtin(const BuiltIn& built_in)
+  BuiltInsDef& BuiltInsDef::register_builtin(const BuiltIn& built_in)
   {
     m_builtins[built_in->name] = built_in;
     return *this;
   }
 
-  BuiltIns& BuiltIns::register_standard_builtins()
+  BuiltInsDef& BuiltInsDef::register_standard_builtins()
   {
     register_builtins<std::initializer_list<BuiltIn>>({
       BuiltInDef::create(Location("print"), AnyArity, ::print),
-      BuiltInDef::create(Location("json.marshal"), 1, ::json_marshal),
       BuiltInDef::create(Location("opa.runtime"), 0, ::opa_runtime),
     });
 
@@ -121,6 +117,7 @@ namespace rego
     register_builtins(builtins::bits());
     register_builtins(builtins::casts());
     register_builtins(builtins::encoding());
+    register_builtins(builtins::graph());
     register_builtins(builtins::numbers());
     register_builtins(builtins::objects());
     register_builtins(builtins::regex());
@@ -134,17 +131,24 @@ namespace rego
     return *this;
   }
 
-  std::map<Location, BuiltIn>::const_iterator BuiltIns::begin() const
+  BuiltIns BuiltInsDef::create()
+  {
+    BuiltIns builtins = std::make_shared<BuiltInsDef>();
+    builtins->register_standard_builtins();
+    return builtins;
+  }
+
+  std::map<Location, BuiltIn>::const_iterator BuiltInsDef::begin() const
   {
     return m_builtins.begin();
   }
 
-  std::map<Location, BuiltIn>::const_iterator BuiltIns::end() const
+  std::map<Location, BuiltIn>::const_iterator BuiltInsDef::end() const
   {
     return m_builtins.end();
   }
 
-  const BuiltIn& BuiltIns::at(const Location& name) const
+  const BuiltIn& BuiltInsDef::at(const Location& name) const
   {
     return m_builtins.at(name);
   }
diff --git a/src/builtins/aggregates.cc b/src/builtins/aggregates.cc
index 50f67124..a9fe4076 100644
--- a/src/builtins/aggregates.cc
+++ b/src/builtins/aggregates.cc
@@ -1,8 +1,10 @@
 #include "builtins.h"
+#include "trieste/utf8.h"
 
 namespace
 {
   using namespace rego;
+  using namespace trieste::utf8;
 
   Node count(const Nodes& args)
   {
@@ -39,7 +41,7 @@ namespace
 
     auto it = std::max_element(
       collection->begin(), collection->end(), [](const Node& a, const Node& b) {
-        return to_json(a) < to_json(b);
+        return to_key(a) < to_key(b);
       });
 
     return *it;
@@ -61,7 +63,7 @@ namespace
 
     auto it = std::min_element(
       collection->begin(), collection->end(), [](const Node& a, const Node& b) {
-        return to_json(a) < to_json(b);
+        return to_key(a) < to_key(b);
       });
 
     return *it;
@@ -83,7 +85,7 @@ namespace
     }
 
     std::sort(items.begin(), items.end(), [](const Node& a, const Node& b) {
-      return to_json(a) < to_json(b);
+      return to_key(a) < to_key(b);
     });
 
     return collection->type() << NodeRange{items.begin(), items.end()};
diff --git a/src/builtins/base64/base64.cpp b/src/builtins/base64/base64.cpp
index ced57d2b..f9898626 100644
--- a/src/builtins/base64/base64.cpp
+++ b/src/builtins/base64/base64.cpp
@@ -112,7 +112,7 @@ std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, b
 
     size_t len_encoded = (in_len +2) / 3 * 4;
 
-    unsigned char trailing_char = url ? '.' : '=';
+    unsigned char trailing_char = '=';
 
  //
  // Choose set of base64 characters. They differ
diff --git a/src/builtins/builtins.h b/src/builtins/builtins.h
index 3dbad057..9503deda 100644
--- a/src/builtins/builtins.h
+++ b/src/builtins/builtins.h
@@ -11,6 +11,7 @@ namespace rego
     std::vector<BuiltIn> bits();
     std::vector<BuiltIn> casts();
     std::vector<BuiltIn> encoding();
+    std::vector<BuiltIn> graph();
     std::vector<BuiltIn> numbers();
     std::vector<BuiltIn> objects();
     std::vector<BuiltIn> regex();
@@ -21,17 +22,4 @@ namespace rego
     std::vector<BuiltIn> types();
     std::vector<BuiltIn> units();
   }
-
-  struct rune
-  {
-    std::uint32_t value;
-    std::string_view source;
-  };
-
-  using runestring = std::basic_string<std::uint32_t>;
-  using runestring_view = std::basic_string_view<std::uint32_t>;
-
-  std::vector<rune> utf8_to_runes(const std::string_view& utf8);
-  runestring utf8_to_runestring(const std::string_view& utf8);
-  std::string runestring_to_utf8(const runestring_view& runes);
 }
\ No newline at end of file
diff --git a/src/builtins/encoding.cc b/src/builtins/encoding.cc
index 3c43702e..d4aa8403 100644
--- a/src/builtins/encoding.cc
+++ b/src/builtins/encoding.cc
@@ -1,5 +1,9 @@
 #include "base64/base64.h"
 #include "builtins.h"
+#include "rego.hh"
+#include "trieste/json.h"
+#include "trieste/wf.h"
+#include "trieste/yaml.h"
 
 namespace
 {
@@ -30,6 +34,484 @@ namespace
     std::string decoded = ::base64_decode(x_string);
     return JSONString ^ decoded;
   }
+
+  Node base64_is_valid(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return False ^ "false";
+    }
+
+    std::string x_string = get_string(x_string_node);
+    for (auto c : x_string)
+    {
+      if (!(std::isalnum(c) || c == '+' || c == '/' || c == '='))
+        return False ^ "false";
+    }
+
+    return True ^ "true";
+  }
+
+  Node base64url_encode(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return x_string_node;
+    }
+
+    std::string x_string = get_string(x_string_node);
+    std::string encoded = ::base64_encode(x_string, true);
+    return JSONString ^ encoded;
+  }
+
+  Node base64url_encode_no_pad(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return x_string_node;
+    }
+
+    std::string x_string = get_string(x_string_node);
+    std::string encoded = ::base64_encode(x_string, true);
+    while (encoded.back() == '=')
+    {
+      encoded.pop_back();
+    }
+    return JSONString ^ encoded;
+  }
+
+  Node base64url_decode(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return x_string_node;
+    }
+
+    std::string x_string = get_string(x_string_node);
+    std::string decoded = ::base64_decode(x_string);
+    return JSONString ^ decoded;
+  }
+
+  Node hex_encode(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return x_string_node;
+    }
+
+    std::string x_string = get_string(x_string_node);
+    std::ostringstream encoded;
+    for (auto c : x_string)
+    {
+      encoded << std::hex << std::setw(2) << std::setfill('0') << (int)c;
+    }
+    return JSONString ^ encoded.str();
+  }
+
+  std::optional<int> hexdigit(char hex)
+  {
+    if (hex >= '0' && hex <= '9')
+    {
+      return (hex - '0');
+    }
+
+    if (hex >= 'A' && hex <= 'F')
+    {
+      return (hex - 'A' + 10);
+    }
+
+    if (hex >= 'a' && hex <= 'f')
+    {
+      return (hex - 'a' + 10);
+    }
+
+    return std::nullopt;
+  }
+
+  Node hex_decode(const Nodes& args)
+  {
+    Node x_string_node = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x_string_node->type() == Error)
+    {
+      return x_string_node;
+    }
+
+    std::string x_string = get_string(x_string_node);
+    std::ostringstream decoded;
+    for (std::size_t i = 0; i < x_string.size(); i += 2)
+    {
+      int test = 0;
+      for (std::size_t j = i; j < i + 2; ++j)
+      {
+        char hex = x_string[j];
+        auto maybe_digit = hexdigit(hex);
+        if (maybe_digit.has_value())
+        {
+          test = (test << 4) | maybe_digit.value();
+          continue;
+        }
+
+        std::ostringstream error;
+        error << "invalid byte: U+" << std::hex << std::setw(4)
+              << std::setfill('0') << (int)hex << " '" << hex << "'";
+        return err(x_string_node, error.str(), EvalBuiltInError);
+      }
+
+      decoded << (char)test;
+    }
+    return JSONString ^ decoded.str();
+  }
+
+  Node json_marshal(const Nodes& args)
+  {
+    Node term = args[0];
+    if (term != Term)
+    {
+      if (term->in({Int, Float, True, False, Null, JSONString}))
+      {
+        term = Term << (Scalar << term);
+      }
+      else if (term->in({Array, Object, Set, Scalar}))
+      {
+        term = Term << term;
+      }
+      else
+      {
+        return err(args[0], "failed to marshal JSON", EvalBuiltInError);
+      }
+    }
+
+    auto result = to_json().wf_check_enabled(true).rewrite(Top << term);
+    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_unmarshal(const Nodes& args)
+  {
+    Node x = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_json = get_string(x);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    auto result =
+      json::reader().synthetic(x_raw).wf_check_enabled(true) >> from_json(true);
+    if (!result.ok)
+    {
+      logging::Error log;
+      result.print_errors(log);
+      return err(x, "failed to parse JSON", EvalBuiltInError);
+    }
+
+    return result.ast->front();
+  }
+
+  Node json_is_valid(const Nodes& args)
+  {
+    auto maybe_x = unwrap(args[0], {JSONString});
+    if (!maybe_x.success)
+    {
+      return False ^ "false";
+    }
+
+    std::string x_json = get_string(maybe_x.node);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    auto result = json::reader().synthetic(x_raw).wf_check_enabled(true).read();
+    return result.ok ? True ^ "true" : False ^ "false";
+  }
+
+  Node yaml_marshal(const Nodes& args)
+  {
+    Node term = args[0];
+    if (term != Term)
+    {
+      if (term->in({Int, Float, True, False, Null, JSONString}))
+      {
+        term = Term << (Scalar << term);
+      }
+      else if (term->in({Array, Object, Set, Scalar}))
+      {
+        term = Term << term;
+      }
+      else
+      {
+        return err(args[0], "failed to marshal YAML", EvalBuiltInError);
+      }
+    }
+
+    auto result = to_yaml().wf_check_enabled(true).rewrite(Top << term);
+    if (!result.ok)
+    {
+      logging::Error log;
+      result.print_errors(log);
+      return err(args[0], "failed to marshal YAML", EvalBuiltInError);
+    }
+
+    std::string yaml = yaml::to_string(result.ast);
+    return JSONString ^ ('"' + json::escape(yaml) + '"');
+  }
+
+  Node yaml_unmarshal(const Nodes& args)
+  {
+    Node x = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_json = get_string(x);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    auto result = yaml::reader().synthetic(x_raw).wf_check_enabled(true) >>
+      yaml::to_json() >> from_json(true);
+    if (!result.ok)
+    {
+      logging::Error log;
+      result.print_errors(log);
+      return err(x, "failed to parse YAML", EvalBuiltInError);
+    }
+
+    return result.ast->front();
+  }
+
+  Node yaml_is_valid(const Nodes& args)
+  {
+    auto maybe_x = unwrap(args[0], {JSONString});
+    if (!maybe_x.success)
+    {
+      return False ^ "false";
+    }
+
+    std::string x_json = get_string(maybe_x.node);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    auto result = yaml::reader().synthetic(x_raw).wf_check_enabled(true).read();
+    return result.ok ? True ^ "true" : False ^ "false";
+  }
+
+  void urlquery_encode(std::ostringstream& encoded, Node x)
+  {
+    std::string x_json = get_string(x);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    for (auto c : x_raw)
+    {
+      if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~')
+      {
+        encoded << c;
+      }
+      else
+      {
+        encoded << '%' << std::uppercase << std::hex << std::setw(2)
+                << std::setfill('0') << (int)(unsigned char)c;
+      }
+    }
+  }
+
+  Node urlquery_encode(const Nodes& args)
+  {
+    auto x = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::ostringstream encoded;
+    urlquery_encode(encoded, x);
+
+    return JSONString ^ ('"' + json::escape(encoded.str()) + '"');
+  }
+
+  Node urlquery_encode_object(const Nodes& args)
+  {
+    auto object = unwrap_arg(args, UnwrapOpt(0).type(Object));
+    if (object->type() == Error)
+    {
+      return object;
+    }
+
+    std::ostringstream encoded;
+    for (std::size_t i = 0; i < object->size(); ++i)
+    {
+      auto maybe_key = unwrap(object->at(i) / Key, {JSONString});
+      if (!maybe_key.success)
+      {
+        return err(maybe_key.node, "key must be a string", EvalBuiltInError);
+      }
+
+      std::ostringstream key_buf;
+      urlquery_encode(key_buf, maybe_key.node);
+      std::string key_str = key_buf.str();
+
+      auto maybe_value = unwrap(object->at(i) / Val, {JSONString, Array, Set});
+      if (!maybe_value.success)
+      {
+        return err(
+          maybe_value.node,
+          "value must be a string or array of strings",
+          EvalBuiltInError);
+      }
+
+      if (i > 0)
+      {
+        encoded << '&';
+      }
+
+      if (maybe_value.node == JSONString)
+      {
+        encoded << key_str << '=';
+        urlquery_encode(encoded, maybe_value.node);
+      }
+      else if (maybe_value.node == Array || maybe_value.node == Set)
+      {
+        Node array = maybe_value.node;
+        for (std::size_t j = 0; j < array->size(); ++j)
+        {
+          maybe_value = unwrap(array->at(j), {JSONString});
+          if (!maybe_value.success)
+          {
+            return err(
+              maybe_value.node, "value must be a string", EvalBuiltInError);
+          }
+
+          if (j > 0)
+          {
+            encoded << '&';
+          }
+          encoded << key_str << '=';
+          urlquery_encode(encoded, maybe_value.node);
+        }
+      }
+    }
+
+    return JSONString ^ ('"' + json::escape(encoded.str()) + '"');
+  }
+
+  std::optional<std::string> urlquery_decode(const std::string& encoded)
+  {
+    std::ostringstream decoded;
+    for (auto it = encoded.begin(); it != encoded.end(); ++it)
+    {
+      if (*it == '%')
+      {
+        if (std::distance(it, encoded.end()) < 3)
+        {
+          return nullptr;
+        }
+
+        auto hex0 = hexdigit(it[1]);
+        auto hex1 = hexdigit(it[2]);
+        if (!hex0.has_value() || !hex1.has_value())
+        {
+          return nullptr;
+        }
+
+        int value = (hex0.value() << 4) | hex1.value();
+        decoded << static_cast<char>(value);
+        it += 2;
+      }
+      else
+      {
+        decoded << *it;
+      }
+    }
+
+    return decoded.str();
+  }
+
+  Node urlquery_decode(const Nodes& args)
+  {
+    auto x = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_json = get_string(x);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    auto maybe_result = urlquery_decode(x_raw);
+    if (maybe_result.has_value())
+    {
+      return JSONString ^ ('"' + json::escape(maybe_result.value()) + '"');
+    }
+
+    return err(x, "invalid URL query string", EvalBuiltInError);
+  }
+
+  Node urlquery_decode_object(const Nodes& args)
+  {
+    auto x = unwrap_arg(args, UnwrapOpt(0).type(JSONString));
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_json = get_string(x);
+    std::string x_raw = json::unescape(strip_quotes(x_json));
+    std::map<std::string, Nodes> items;
+    size_t start = 0;
+    size_t end = x_raw.find('&');
+    while (start < x_raw.size())
+    {
+      std::string item_value = x_raw.substr(start, end - start);
+      size_t equal = item_value.find('=');
+      auto maybe_key = urlquery_decode(item_value.substr(0, equal));
+      if (!maybe_key.has_value())
+      {
+        return err(x, "invalid URL query string", EvalBuiltInError);
+      }
+
+      std::string key = maybe_key.value();
+      if (!contains(items, key))
+      {
+        items[key] = Nodes();
+      }
+
+      if (equal != std::string::npos)
+      {
+        auto maybe_value = urlquery_decode(item_value.substr(equal + 1));
+
+        if (!maybe_value.has_value())
+        {
+          return err(x, "invalid URL query string", EvalBuiltInError);
+        }
+
+        items[key].push_back(
+          JSONString ^ ('"' + json::escape(maybe_value.value()) + '"'));
+      }
+      else
+      {
+        items[key].push_back(JSONString ^ "\"\"");
+      }
+
+      start = end + 1;
+      end = x_raw.find('&', start);
+      if (end == std::string::npos)
+      {
+        end = x_raw.size();
+      }
+    }
+
+    Node argseq = NodeDef::create(ArgSeq);
+    for (auto& [key, value] : items)
+    {
+      argseq->push_back(JSONString ^ ('"' + json::escape(key) + '"'));
+      argseq->push_back(Resolver::array(ArgSeq << value));
+    }
+
+    return Resolver::object(argseq, false);
+  }
 }
 
 namespace rego
@@ -40,7 +522,27 @@ namespace rego
     {
       return {
         BuiltInDef::create(Location("base64.encode"), 1, base64_encode_),
-        BuiltInDef::create(Location("base64.decode"), 1, base64_decode_)};
+        BuiltInDef::create(Location("base64.decode"), 1, base64_decode_),
+        BuiltInDef::create(Location("base64.is_valid"), 1, base64_is_valid),
+        BuiltInDef::create(Location("base64url.encode"), 1, base64url_encode),
+        BuiltInDef::create(
+          Location("base64url.encode_no_pad"), 1, base64url_encode_no_pad),
+        BuiltInDef::create(Location("base64url.decode"), 1, base64url_decode),
+        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.unmarshal"), 1, json_unmarshal),
+        BuiltInDef::create(Location("json.is_valid"), 1, json_is_valid),
+        BuiltInDef::create(Location("urlquery.decode"), 1, urlquery_decode),
+        BuiltInDef::create(
+          Location("urlquery.decode_object"), 1, urlquery_decode_object),
+        BuiltInDef::create(Location("urlquery.encode"), 1, urlquery_encode),
+        BuiltInDef::create(
+          Location("urlquery.encode_object"), 1, urlquery_encode_object),
+        BuiltInDef::create(Location("yaml.marshal"), 1, yaml_marshal),
+        BuiltInDef::create(Location("yaml.unmarshal"), 1, yaml_unmarshal),
+        BuiltInDef::create(Location("yaml.is_valid"), 1, yaml_is_valid),
+      };
     }
   }
 }
\ No newline at end of file
diff --git a/src/builtins/graph.cc b/src/builtins/graph.cc
new file mode 100644
index 00000000..f3b06686
--- /dev/null
+++ b/src/builtins/graph.cc
@@ -0,0 +1,297 @@
+#include "builtins.h"
+
+namespace
+{
+  using namespace rego;
+
+  using NodeIndex = std::map<std::string, Node>;
+  using Graph = std::map<std::string_view, std::set<std::string_view>>;
+
+  std::string_view lookup(NodeIndex& index, Node node)
+  {
+    std::string key = to_key(node);
+    if (!contains(index, key))
+    {
+      index[key] = node;
+    }
+
+    return index.find(key)->first;
+  }
+
+  void index_nodes(NodeIndex& index, Node array)
+  {
+    for (auto& node : *array)
+    {
+      std::string key = to_key(node);
+      index[key] = node;
+    }
+  }
+
+  Node build_graph(Graph& graph, Node edges, NodeIndex& index)
+  {
+    for (auto& edge : *edges)
+    {
+      std::string_view src = lookup(index, edge / Key);
+
+      auto maybe_dst = unwrap(edge / Val, {Set, Array, Null});
+      if (!maybe_dst.success)
+      {
+        return err(maybe_dst.node, "graph.reachable: expected a set/array");
+      }
+
+      if (maybe_dst.node == Null)
+      {
+        graph[src] = {};
+        continue;
+      }
+
+      std::set<std::string_view> dsts = {};
+      for (auto& node : *maybe_dst.node)
+      {
+        dsts.insert(lookup(index, node));
+      }
+
+      graph[src] = dsts;
+    }
+
+    return nullptr;
+  }
+
+  Node reachable(const Nodes& args)
+  {
+    Node graph_object =
+      unwrap_arg(args, UnwrapOpt(0).type(Object).func("graph.reachable"));
+    if (graph_object->type() == Error)
+    {
+      return graph_object;
+    }
+
+    Node initial_nodes = unwrap_arg(
+      args, UnwrapOpt(1).types({Set, Array}).func("graph.reachable"));
+    if (initial_nodes->type() == Error)
+    {
+      return initial_nodes;
+    }
+
+    NodeIndex nodes;
+    index_nodes(nodes, initial_nodes);
+
+    std::vector<std::string_view> initial_set;
+    for (auto& [key, _] : nodes)
+    {
+      initial_set.push_back(key);
+    }
+
+    Graph graph;
+    Node err = build_graph(graph, graph_object, nodes);
+    if (err != nullptr)
+    {
+      return err;
+    }
+
+    std::set<std::string_view> visited;
+    std::vector<std::string_view> frontier;
+    frontier.insert(frontier.end(), initial_set.begin(), initial_set.end());
+
+    while (!frontier.empty())
+    {
+      std::string_view current = frontier.back();
+      frontier.pop_back();
+      if (contains(visited, current))
+      {
+        continue;
+      }
+
+      if (graph.find(current) == graph.end())
+      {
+        continue;
+      }
+
+      visited.insert(current);
+      frontier.insert(
+        frontier.end(), graph[current].begin(), graph[current].end());
+    }
+
+    Node argseq = NodeDef::create(ArgSeq);
+    for (auto& key : visited)
+    {
+      argseq->push_back(nodes[std::string(key)]);
+    }
+
+    return Resolver::set(argseq);
+  }
+
+  class Path
+  {
+  private:
+    std::vector<std::string_view> m_order;
+    std::set<std::string_view> m_visited;
+
+  public:
+    Path(const Path& path) :
+      m_order(path.m_order.begin(), path.m_order.end()),
+      m_visited(path.m_visited.begin(), path.m_visited.end())
+    {}
+
+    Path(Path&& other) :
+      m_order(std::move(other.m_order)), m_visited(std::move(other.m_visited))
+    {}
+
+    Path(std::string_view key)
+    {
+      m_order.push_back(key);
+      m_visited.insert(key);
+    }
+
+    Path operator+(std::string_view key)
+    {
+      Path new_path = *this;
+      new_path.m_order.push_back(key);
+      new_path.m_visited.insert(key);
+      return new_path;
+    }
+
+    Path& operator=(const Path& path)
+    {
+      m_order =
+        std::vector<std::string_view>(path.m_order.begin(), path.m_order.end());
+      m_visited = std::set<std::string_view>(
+        path.m_visited.begin(), path.m_visited.end());
+      return *this;
+    }
+
+    Path& operator=(Path&& other)
+    {
+      m_order = std::move(other.m_order);
+      m_visited = std::move(other.m_visited);
+      return *this;
+    }
+
+    std::string_view back() const
+    {
+      return m_order.back();
+    }
+
+    const std::set<std::string_view>& visited() const
+    {
+      return m_visited;
+    }
+
+    Node to_node(const NodeIndex& index) const
+    {
+      Node argseq = NodeDef::create(ArgSeq);
+      for (auto& key : m_order)
+      {
+        std::string key_str(key);
+        argseq->push_back(index.at(key_str));
+      }
+
+      return Resolver::array(argseq);
+    }
+  };
+
+  Node reachable_paths(const Nodes& args)
+  {
+    Node graph_object =
+      unwrap_arg(args, UnwrapOpt(0).type(Object).func("graph.reachable"));
+    if (graph_object->type() == Error)
+    {
+      return graph_object;
+    }
+
+    Node initial_nodes = unwrap_arg(
+      args, UnwrapOpt(1).types({Set, Array}).func("graph.reachable"));
+    if (initial_nodes->type() == Error)
+    {
+      return initial_nodes;
+    }
+
+    NodeIndex nodes;
+    index_nodes(nodes, initial_nodes);
+
+    std::vector<std::string_view> initial_set;
+    for (auto& [key, _] : nodes)
+    {
+      initial_set.push_back(key);
+    }
+
+    Graph graph;
+    Node err = build_graph(graph, graph_object, nodes);
+    if (err != nullptr)
+    {
+      return err;
+    }
+
+    std::vector<Path> paths;
+    std::vector<Path> frontier;
+    std::transform(
+      initial_set.begin(),
+      initial_set.end(),
+      std::back_inserter(frontier),
+      [](std::string_view key) { return Path{key}; });
+
+    while (!frontier.empty())
+    {
+      Path path = std::move(frontier.back());
+      frontier.pop_back();
+
+      auto current = path.back();
+
+      if (graph.find(current) == graph.end())
+      {
+        continue;
+      }
+
+      std::vector<std::string_view> unvisited;
+      std::set_difference(
+        graph[current].begin(),
+        graph[current].end(),
+        path.visited().begin(),
+        path.visited().end(),
+        std::back_inserter(unvisited));
+
+      std::vector<std::string_view> neighbors;
+      std::copy_if(
+        unvisited.begin(),
+        unvisited.end(),
+        std::back_inserter(neighbors),
+        [&graph](std::string_view key) { return contains(graph, key); });
+
+      if (neighbors.empty())
+      {
+        paths.push_back(std::move(path));
+      }
+      else
+      {
+        std::transform(
+          neighbors.begin(),
+          neighbors.end(),
+          std::back_inserter(frontier),
+          [&path](std::string_view key) { return path + key; });
+      }
+    }
+
+    Node argseq = NodeDef::create(ArgSeq);
+    for (auto& path : paths)
+    {
+      argseq->push_back(path.to_node(nodes));
+    }
+
+    return Resolver::set(argseq);
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> graph()
+    {
+      return {
+        BuiltInDef::create(Location("graph.reachable"), 2, reachable),
+        BuiltInDef::create(
+          Location("graph.reachable_paths"), 2, reachable_paths),
+      };
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/objects.cc b/src/builtins/objects.cc
index 5397e73d..4d1e3755 100644
--- a/src/builtins/objects.cc
+++ b/src/builtins/objects.cc
@@ -11,14 +11,14 @@ namespace
     {
       for (auto& item : *collection)
       {
-        keys.push_back(to_json(item));
+        keys.push_back(to_key(item));
       }
     }
     else if (collection->type() == Object)
     {
       for (auto& item : *collection)
       {
-        keys.push_back(to_json(item / Key));
+        keys.push_back(to_key(item / Key));
       }
     }
     else
@@ -55,7 +55,7 @@ namespace
     Node filtered = NodeDef::create(Object);
     for (auto& item : *object)
     {
-      std::string key_str = to_json(item / Key);
+      std::string key_str = to_key(item / Key);
       if (contains(keys, key_str))
       {
         filtered->push_back(item->clone());
@@ -83,10 +83,10 @@ namespace
     if (collection->type() == Object)
     {
       Node key = keys->at(index);
-      std::string key_str = to_json(key);
+      std::string key_str = to_key(key);
       for (auto& item : *collection)
       {
-        if (to_json(item / Key) == key_str)
+        if (to_key(item / Key) == key_str)
         {
           return get_key(item / Val, keys, index + 1);
         }
@@ -170,7 +170,7 @@ namespace
     Node output = NodeDef::create(Object);
     for (auto& item : *object)
     {
-      std::string key_str = to_json(item / Key);
+      std::string key_str = to_key(item / Key);
       if (!contains(keys, key_str))
       {
         output->push_back(item->clone());
@@ -185,7 +185,7 @@ namespace
     std::map<std::string, Node> map;
     for (auto& item : *object)
     {
-      map[to_json(item / Key)] = (item / Val)->front();
+      map[to_key(item / Key)] = (item / Val)->front();
     }
 
     return map;
@@ -310,7 +310,7 @@ namespace
 
     if (super->type() == sub->type())
     {
-      return to_json(super) == to_json(sub);
+      return to_key(super) == to_key(sub);
     }
 
     return false;
@@ -379,7 +379,7 @@ namespace
     auto rhs_keys = get_key_set(rhs);
     for (auto& item : *lhs)
     {
-      std::string key_str = to_json(item / Key);
+      std::string key_str = to_key(item / Key);
       if (!contains(rhs_keys, key_str))
       {
         output->push_back(item->clone());
diff --git a/src/builtins/regex.cc b/src/builtins/regex.cc
index 0fabfa98..0534be24 100644
--- a/src/builtins/regex.cc
+++ b/src/builtins/regex.cc
@@ -1,4 +1,5 @@
 #include "builtins.h"
+#include "trieste/json.h"
 
 #include <regex>
 
@@ -6,66 +7,6 @@ namespace
 {
   using namespace rego;
 
-  std::string json_to_raw(const std::string& json_string)
-  {
-    std::string raw_string;
-    raw_string.reserve(json_string.size());
-    auto pos = json_string.begin();
-    while (pos != json_string.end())
-    {
-      char c0 = *pos;
-      ++pos;
-
-      if (c0 != '\\')
-      {
-        raw_string.push_back(c0);
-        continue;
-      }
-
-      char c1 = *pos;
-      ++pos;
-      switch (c1)
-      {
-        case '"':
-          raw_string.push_back('"');
-          break;
-
-        case '\\':
-          raw_string.push_back('\\');
-          break;
-
-        case '/':
-          raw_string.push_back('/');
-          break;
-
-        case 'b':
-          raw_string.push_back('\b');
-          break;
-
-        case 'f':
-          raw_string.push_back('\f');
-          break;
-
-        case 'n':
-          raw_string.push_back('\n');
-          break;
-
-        case 'r':
-          raw_string.push_back('\r');
-          break;
-
-        case 't':
-          raw_string.push_back('\t');
-          break;
-
-        default:
-          throw std::runtime_error("invalid escape sequence");
-      }
-    }
-
-    return raw_string;
-  }
-
   Node error(
     const Node& pattern_node,
     const std::regex_constants::error_type& error_code)
@@ -189,7 +130,7 @@ namespace
       return value_node;
     }
 
-    std::string pattern = json_to_raw(get_string(pattern_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
     std::string value = get_string(value_node);
 
     try
@@ -213,7 +154,7 @@ namespace
       return Resolver::scalar(false);
     }
 
-    std::string pattern = json_to_raw(get_string(pattern_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
 
     try
     {
@@ -250,8 +191,8 @@ namespace
     }
 
     std::string s = get_string(s_node);
-    std::string pattern = json_to_raw(get_string(pattern_node));
-    std::string value = json_to_raw(get_string(value_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
+    std::string value = json::unescape(get_string(value_node));
 
     std::ostringstream os;
 
@@ -290,7 +231,7 @@ namespace
       return number_node;
     }
 
-    std::string pattern = json_to_raw(get_string(pattern_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
     std::string value = get_string(value_node);
     std::size_t number = get_int(number_node).to_size();
 
@@ -344,7 +285,7 @@ namespace
       return number_node;
     }
 
-    std::string pattern = json_to_raw(get_string(pattern_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
     std::string value = get_string(value_node);
     std::size_t number = get_int(number_node).to_size();
 
@@ -397,7 +338,7 @@ namespace
       return value_node;
     }
 
-    std::string pattern = json_to_raw(get_string(pattern_node));
+    std::string pattern = json::unescape(get_string(pattern_node));
     std::string value = get_string(value_node);
 
     std::regex re;
@@ -532,7 +473,7 @@ namespace
       return delimiter_end_node;
     }
 
-    std::string template_ = json_to_raw(get_string(template_node));
+    std::string template_ = json::unescape(get_string(template_node));
     std::string value = get_string(value_node);
     std::string delimiter_start = get_string(delimiter_start_node);
     std::string delimiter_end = get_string(delimiter_end_node);
diff --git a/src/builtins/strings.cc b/src/builtins/strings.cc
index a3ac05ed..4b9e34e0 100644
--- a/src/builtins/strings.cc
+++ b/src/builtins/strings.cc
@@ -1,10 +1,13 @@
 #include "builtins.h"
+#include "trieste/json.h"
+#include "trieste/utf8.h"
 
 #include <bitset>
 
 namespace
 {
   using namespace rego;
+  using namespace trieste::utf8;
 
   Node unwrap_strings(const Node& collection, std::vector<std::string>& items)
   {
@@ -473,7 +476,7 @@ namespace
       switch (verb.type)
       {
         case PrintVerbType::Value:
-          result << to_json(node);
+          result << json::escape(to_key(node));
           break;
 
         case PrintVerbType::Boolean:
@@ -717,13 +720,10 @@ namespace
     }
 
     std::string x_str = get_string(x);
-    std::vector<rune> x_runes = utf8_to_runes(x_str);
+    runestring x_runes = utf8_to_runestring(x_str);
     std::reverse(x_runes.begin(), x_runes.end());
     std::ostringstream y;
-    for (auto& rune : x_runes)
-    {
-      y << rune.source;
-    }
+    y << x_runes;
     return JSONString ^ y.str();
   }
 
@@ -747,7 +747,7 @@ namespace
     }
 
     std::string value_str = get_string(value);
-    std::vector<rune> value_runes = utf8_to_runes(value_str);
+    runestring value_runes = utf8_to_runestring(value_str);
     std::int64_t offset_int = get_int(offset).to_int();
     if (offset_int < 0)
     {
@@ -776,23 +776,16 @@ namespace
       length_size = value_runes.size() - offset_size;
     }
 
-    std::vector<rune> output_runes(
-      value_runes.begin() + offset_size,
-      value_runes.begin() + offset_size + length_size);
     std::ostringstream output;
-    for (auto& rune : output_runes)
-    {
-      output << rune.source;
-    }
-
+    output << value_runes.substr(offset_size, length_size);
     return JSONString ^ output.str();
   }
 
   std::string trim(
     const std::string& value, const std::string& cutset, bool left, bool right)
   {
-    runestring value_runes = utf8_to_runestring(value);
-    runestring cutset_runes = utf8_to_runestring(cutset);
+    runestring value_runes = utf8_to_runestring(json::unescape(value));
+    runestring cutset_runes = utf8_to_runestring(json::unescape(cutset));
 
     std::size_t start, end;
     if (left)
@@ -818,8 +811,9 @@ namespace
       return "";
     }
 
-    runestring output_runes = value_runes.substr(start, end - start + 1);
-    return runestring_to_utf8(output_runes);
+    std::ostringstream output;
+    output << value_runes.substr(start, end - start + 1);
+    return output.str();
   }
 
   Node trim(const Nodes& args)
diff --git a/src/builtins/units.cc b/src/builtins/units.cc
index 7e16dcf0..1c406ae7 100644
--- a/src/builtins/units.cc
+++ b/src/builtins/units.cc
@@ -14,14 +14,14 @@ namespace
     {"ei", kb* kb* kb* kb* kb* kb},
   };
 
-  const BigInt k = static_cast<std::size_t>(1000);
+  const BigInt kd = static_cast<std::size_t>(1000);
   std::map<std::string, BigInt> big_units = {
-    {"k", k},
-    {"m", k* k},
-    {"g", k* k* k},
-    {"t", k* k* k* k},
-    {"p", k* k* k* k* k},
-    {"e", k* k* k* k* k* k},
+    {"k", kd},
+    {"m", kd* kd},
+    {"g", kd* kd* kd},
+    {"t", kd* kd* kd* kd},
+    {"p", kd* kd* kd* kd* kd},
+    {"e", kd* kd* kd* kd* kd* kd},
   };
 
   std::map<std::string, double> small_units = {
diff --git a/src/builtins/utf8.cc b/src/builtins/utf8.cc
deleted file mode 100644
index 31198fbd..00000000
--- a/src/builtins/utf8.cc
+++ /dev/null
@@ -1,255 +0,0 @@
-#include "builtins.h"
-
-namespace
-{
-  const std::uint8_t MaskX = 0b11000000;
-  const std::uint8_t Mask1 = 0b10000000;
-  const std::uint8_t Mask2 = 0b11100000;
-  const std::uint8_t Mask3 = 0b11110000;
-  const std::uint8_t Mask4 = 0b11111000;
-
-  const std::uint8_t MarkX = 0b10000000;
-  const std::uint8_t Mark1 = 0b00000000;
-  const std::uint8_t Mark2 = 0b11000000;
-  const std::uint8_t Mark3 = 0b11100000;
-  const std::uint8_t Mark4 = 0b11110000;
-
-  const std::uint8_t ValueX = 0b00111111;
-  const std::uint8_t Value1 = 0b01111111;
-  const std::uint8_t Value2 = 0b00011111;
-  const std::uint8_t Value3 = 0b00001111;
-  const std::uint8_t Value4 = 0b00000111;
-
-  const std::uint32_t Max1 = 0x007F;
-  const std::uint32_t Max2 = 0x07FF;
-  const std::uint32_t Max3 = 0xFFFF;
-  const std::uint32_t Max4 = 0x10FFFF;
-
-  const std::size_t ShiftX = 6;
-
-  const std::uint32_t Bad = 0xFFFD;
-
-  void to_utf8(std::uint32_t value, std::string& utf8)
-  {
-    if (value <= Max1)
-    {
-      switch (value)
-      {
-        case '\f':
-          utf8.push_back('\\');
-          utf8.push_back('f');
-          break;
-        case '\n':
-          utf8.push_back('\\');
-          utf8.push_back('n');
-          break;
-        case '\r':
-          utf8.push_back('\\');
-          utf8.push_back('r');
-          break;
-        case '\t':
-          utf8.push_back('\\');
-          utf8.push_back('t');
-          break;
-        case '\v':
-          utf8.push_back('\\');
-          utf8.push_back('v');
-          break;
-        case '\\':
-          utf8.push_back('\\');
-          utf8.push_back('\\');
-          break;
-        case '\'':
-          utf8.push_back('\\');
-          utf8.push_back('\'');
-          break;
-        case '\"':
-          utf8.push_back('\\');
-          utf8.push_back('\"');
-          break;
-        default:
-          char c0 = static_cast<char>(Mark1 | value);
-          utf8.push_back(c0);
-          break;
-      }
-    }
-    else if (value <= Max2)
-    {
-      char c1 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c0 = static_cast<char>(Mark2 | value);
-      utf8.push_back(c0);
-      utf8.push_back(c1);
-    }
-    else if (value <= Max3)
-    {
-      char c2 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c1 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c0 = static_cast<char>(Mark3 | value);
-      utf8.push_back(c0);
-      utf8.push_back(c1);
-      utf8.push_back(c2);
-    }
-    else if (value <= Max4)
-    {
-      char c3 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c2 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c1 = static_cast<char>((value & ValueX) | MarkX);
-      value >>= ShiftX;
-      char c0 = static_cast<char>(Mark4 | value);
-      utf8.push_back(c0);
-      utf8.push_back(c1);
-      utf8.push_back(c2);
-      utf8.push_back(c3);
-    }
-    else
-    {
-      // bad
-      to_utf8(Bad, utf8);
-    }
-  }
-
-  rego::rune utf8_to_rune(const std::string_view& utf8)
-  {
-    std::uint8_t c0 = static_cast<std::uint8_t>(utf8[0]);
-    if (c0 == '\\')
-    {
-      if (utf8.size() >= 1)
-      {
-        switch (utf8[1])
-        {
-          case 'f':
-            return {'\f', utf8.substr(0, 2)};
-          case 'n':
-            return {'\n', utf8.substr(0, 2)};
-          case 'r':
-            return {'\r', utf8.substr(0, 2)};
-          case 't':
-            return {'\t', utf8.substr(0, 2)};
-          case 'v':
-            return {'\v', utf8.substr(0, 2)};
-          case '\\':
-            return {'\\', utf8.substr(0, 2)};
-          case '\'':
-            return {'\'', utf8.substr(0, 2)};
-          case '\"':
-            return {'\"', utf8.substr(0, 2)};
-        }
-      }
-      if (utf8.size() >= 3 && utf8[1] == 'x')
-      {
-        std::string hex = std::string(utf8.substr(2, 2));
-        std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, utf8.substr(0, 4)};
-      }
-      if (utf8.size() >= 5 && utf8[1] == 'u')
-      {
-        std::string hex = std::string(utf8.substr(2, 4));
-        std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, utf8.substr(0, 6)};
-      }
-      if (utf8.size() >= 9 && utf8[1] == 'U')
-      {
-        std::string hex = std::string(utf8.substr(2, 8));
-        std::uint32_t value = std::stoul(hex, nullptr, 16);
-        return {value, utf8.substr(0, 10)};
-      }
-    }
-
-    if ((c0 & Mask1) == Mark1)
-    {
-      std::uint32_t value = c0 & Value1;
-      return {value, utf8.substr(0, 1)};
-    }
-
-    if ((c0 & Mask2) == Mark2 && utf8.size() >= 2)
-    {
-      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
-      if ((c1 & MaskX) == MarkX)
-      {
-        std::uint32_t value = c0 & Value2;
-        value = (value << ShiftX) | (c1 & ValueX);
-        return {value, utf8.substr(0, 2)};
-      }
-    }
-    else if ((c0 & Mask3) == Mark3 && utf8.size() >= 3)
-    {
-      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
-      std::uint8_t c2 = static_cast<std::uint8_t>(utf8[2]);
-      if ((c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX)
-      {
-        std::uint32_t value = c0 & Value3;
-        value = (value << ShiftX) | (c1 & ValueX);
-        value = (value << ShiftX) | (c2 & ValueX);
-        return {value, utf8.substr(0, 3)};
-      }
-    }
-    else if ((c0 & Mask4) == Mark4 && utf8.size() >= 4)
-    {
-      std::uint8_t c1 = static_cast<std::uint8_t>(utf8[1]);
-      std::uint8_t c2 = static_cast<std::uint8_t>(utf8[2]);
-      std::uint8_t c3 = static_cast<std::uint8_t>(utf8[3]);
-      if (
-        (c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX && (c3 & MaskX) == MarkX)
-      {
-        std::uint32_t value = c0 & Value4;
-        value = (value << ShiftX) | (c1 & ValueX);
-        value = (value << ShiftX) | (c2 & ValueX);
-        value = (value << ShiftX) | (c3 & ValueX);
-        return {value, utf8.substr(0, 4)};
-      }
-    }
-
-    // bad
-    return {Bad, utf8.substr(0, 1)};
-  }
-
-}
-
-namespace rego
-{
-  std::vector<rune> utf8_to_runes(const std::string_view& utf8)
-  {
-    std::vector<rune> runes;
-    runes.reserve(utf8.size());
-    std::size_t pos = 0;
-    while (pos < utf8.size())
-    {
-      rune r = utf8_to_rune(utf8.substr(pos));
-      runes.push_back(r);
-      pos += r.source.size();
-    }
-
-    return runes;
-  }
-
-  runestring utf8_to_runestring(const std::string_view& utf8)
-  {
-    runestring runes;
-    runes.reserve(utf8.size());
-    std::size_t pos = 0;
-    while (pos < utf8.size())
-    {
-      rune r = utf8_to_rune(utf8.substr(pos));
-      runes.push_back(r.value);
-      pos += r.source.size();
-    }
-
-    return runes;
-  }
-
-  std::string runestring_to_utf8(const runestring_view& runes)
-  {
-    std::string utf8;
-    utf8.reserve(runes.size());
-    for (std::uint32_t r : runes)
-    {
-      to_utf8(r, utf8);
-    }
-    return utf8;
-  }
-}
\ No newline at end of file
diff --git a/src/encoding.cc b/src/encoding.cc
index 4d884a67..76d4e5b7 100644
--- a/src/encoding.cc
+++ b/src/encoding.cc
@@ -1,4 +1,8 @@
-#include "internal.hh"
+#include "trieste/json.h"
+#include "unify.hh"
+
+#include <sstream>
+#include <string_view>
 
 namespace
 {
@@ -21,7 +25,7 @@ namespace
         node = node->front();
       }
 
-      key = to_json(node, false);
+      key = to_key(node);
     }
 
     bool operator<(const NodeKey& other) const
@@ -98,7 +102,7 @@ namespace
 
 namespace rego
 {
-  std::string to_json(const Node& node, bool set_as_array, bool sort_arrays)
+  std::string to_key(const Node& node, bool set_as_array, bool sort_arrays)
   {
     std::ostringstream buf;
     if (node->type() == Int)
@@ -164,14 +168,14 @@ namespace rego
         std::sort(keys.begin(), keys.end());
         std::transform(
           keys.begin(), keys.end(), std::back_inserter(items), [&](auto& key) {
-            return to_json(key.node, set_as_array, sort_arrays);
+            return to_key(key.node, set_as_array, sort_arrays);
           });
       }
       else
       {
         for (const auto& child : *node)
         {
-          items.push_back(to_json(child, set_as_array, sort_arrays));
+          items.push_back(to_key(child, set_as_array, sort_arrays));
         }
       }
 
@@ -213,7 +217,7 @@ namespace rego
         ", ",
         [set_as_array,
          sort_arrays](std::ostream& stream, const NodeKey& node_key) {
-          stream << to_json(node_key.node, set_as_array, sort_arrays);
+          stream << to_key(node_key.node, set_as_array, sort_arrays);
           return true;
         });
 
@@ -233,12 +237,12 @@ namespace rego
       {
         auto key = child / Key;
         auto value = child / Val;
-        std::string key_str = to_json(key, set_as_array, sort_arrays);
+        std::string key_str = to_key(key, set_as_array, sort_arrays);
         if (!is_quoted(key_str))
         {
-          key_str = '"' + key_str + '"';
+          key_str = '"' + json::escape(key_str) + '"';
         }
-        items.insert({key_str, to_json(value, set_as_array, sort_arrays)});
+        items.insert({key_str, to_key(value, set_as_array, sort_arrays)});
       }
 
       buf << "{";
@@ -257,12 +261,12 @@ namespace rego
       node->type() == Scalar || node->type() == Term ||
       node->type() == DataTerm)
     {
-      return to_json(node->front(), set_as_array, sort_arrays);
+      return to_key(node->front(), set_as_array, sort_arrays);
     }
     else if (node->type() == Binding)
     {
       buf << (node / Var)->location().view() << " = "
-          << to_json(node / Term, set_as_array, sort_arrays);
+          << to_key(node / Term, set_as_array, sort_arrays);
     }
     else if (node->type() == TermSet)
     {
@@ -273,7 +277,7 @@ namespace rego
         node->end(),
         ", ",
         [set_as_array, sort_arrays](std::ostream& stream, const Node& n) {
-          stream << to_json(n, set_as_array, sort_arrays);
+          stream << to_key(n, set_as_array, sort_arrays);
           return true;
         });
       buf << "}";
diff --git a/src/internal.cc b/src/internal.cc
index 66d46eb0..cf108f23 100644
--- a/src/internal.cc
+++ b/src/internal.cc
@@ -68,65 +68,12 @@ namespace
 
   double get_double(const Node& node)
   {
-    return std::stod(to_json(node));
+    return std::stod(to_key(node));
   }
 }
 
 namespace rego
 {
-  bool contains_ref(const Node& node)
-  {
-    if (node->type() == NestedBody)
-    {
-      return false;
-    }
-
-    if (node->type() == Ref || node->type() == Var)
-    {
-      return true;
-    }
-
-    for (auto& child : *node)
-    {
-      if (contains_ref(child))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool contains_local(const Node& node)
-  {
-    if (node->type() == NestedBody)
-    {
-      return false;
-    }
-
-    if (node->type() == Var)
-    {
-      Nodes defs = node->lookup();
-      if (defs.size() == 0)
-      {
-        // check if it is a temporary variable
-        // (which may not yet be in the symbol table)
-        return node->location().view().find('$') != std::string::npos;
-      }
-      return defs.size() == 1 && defs[0]->type() == Local;
-    }
-
-    for (auto& child : *node)
-    {
-      if (contains_local(child))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
   bool is_in(const Node& node, const std::set<Token>& types)
   {
     if (contains(types, node->type()))
@@ -134,7 +81,7 @@ namespace rego
       return true;
     }
 
-    if (node->type() == Rego)
+    if (node->type() == Top)
     {
       return false;
     }
@@ -248,23 +195,7 @@ namespace rego
     return std::string(str);
   }
 
-  bool in_query(const Node& node)
-  {
-    if (node->type() == Rego)
-    {
-      return false;
-    }
-
-    if (node->type() == RuleComp)
-    {
-      std::string name = std::string((node / Var)->location().view());
-      return name.find("query$") != std::string::npos;
-    }
-
-    return in_query(node->parent()->shared_from_this());
-  }
-
-  Node err(const NodeRange& r, const std::string& msg, const std::string& code)
+  Node err(NodeRange& r, const std::string& msg, const std::string& code)
   {
     return Error << (ErrorMsg ^ msg) << (ErrorAst << r) << (ErrorCode ^ code);
   }
@@ -392,6 +323,26 @@ namespace rego
     return ::get_double(node);
   }
 
+  std::optional<BigInt> try_get_int(const Node& node)
+  {
+    if (node == Int)
+    {
+      return ::get_int(node);
+    }
+
+    if (node == Float)
+    {
+      double value = ::get_double(node);
+      double floor = std::floor(value);
+      if (value == floor)
+      {
+        return BigInt(static_cast<size_t>(floor));
+      }
+    }
+
+    return std::nullopt;
+  }
+
   std::string get_string(const Node& node)
   {
     Node value = node;
@@ -419,53 +370,6 @@ namespace rego
     return node->type() == True;
   }
 
-  bool is_truthy(const Node& node)
-  {
-    assert(node->type() == Term || node->type() == TermSet);
-    if (node->type() == TermSet)
-    {
-      return true;
-    }
-
-    Node value = node->front();
-    if (value->type() == Scalar)
-    {
-      value = value->front();
-      return value->type() != False;
-    }
-
-    if (
-      value->type() == Object || value->type() == Array || value->type() == Set)
-    {
-      return true;
-    }
-
-    return false;
-  }
-
-  bool is_undefined(const Node& node)
-  {
-    if (node->type() == DataModule)
-    {
-      return false;
-    }
-
-    if (node->type() == Undefined)
-    {
-      return true;
-    }
-
-    for (auto& child : *node)
-    {
-      if (is_undefined(child))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
   bool is_falsy(const Node& node)
   {
     Node value = node;
@@ -522,7 +426,13 @@ namespace rego
       return "boolean";
     }
 
-    return std::string(type.str());
+    std::string name(type.str());
+    if (starts_with(name, "rego-"))
+    {
+      name = name.substr(5);
+    }
+
+    return name;
   }
 
   std::string type_name(const Node& node, bool specify_number)
@@ -791,11 +701,6 @@ namespace rego
     return Set << set_members;
   }
 
-  bool is_module(const Node& var)
-  {
-    return var->type().in({Submodule, DataItem, Data});
-  }
-
   ActionMetrics::ActionMetrics(const char* file, std::size_t line) :
     m_key{file, line}, m_start(ActionMetrics::clock::now())
   {}
diff --git a/src/internal.hh b/src/internal.hh
index 2043ad8a..66c9c4a0 100644
--- a/src/internal.hh
+++ b/src/internal.hh
@@ -9,34 +9,9 @@
 
 namespace rego
 {
-  const inline auto ScalarToken =
-    T(Int) / T(Float) / T(True) / T(False) / T(Null);
-  const inline auto ArithToken =
-    T(Add) / T(Subtract) / T(Multiply) / T(Divide) / T(Modulo);
-  const inline auto ArithInfixArg = T(Expr) / T(NumTerm) / T(Ref) /
-    T(UnaryExpr) / T(ArithInfix) / T(RefTerm) / T(ExprCall);
-  const inline auto BinInfixArg = T(Expr) / T(Ref) / T(RefTerm) / T(ExprCall) /
-    T(Set) / T(SetCompr) / T(BinInfix);
-  const auto inline RefArg = T(RefArgDot) / T(RefArgBrack);
-  const inline auto BoolToken = T(Equals) / T(NotEquals) / T(GreaterThan) /
-    T(LessThan) / T(GreaterThanOrEquals) / T(LessThanOrEquals);
-  const inline auto TermToken = T(Var) / T(Ref) / T(Array) / T(Object) /
-    T(Set) / T(ArrayCompr) / T(ObjectCompr) / T(SetCompr);
-  const inline auto StringToken = T(JSONString) / T(RawString);
-  const inline auto ExprToken = T(Term) / ArithToken / BoolToken / StringToken /
-    T(Expr) / ScalarToken / TermToken / T(JSONString) / T(Array) / T(Set) /
-    T(Object) / T(Paren) / T(Not) / T(Dot) / T(And) / T(Or) / T(ExprCall);
-  const auto inline MembershipToken = ScalarToken / T(JSONString) /
-    T(RawString) / T(Var) / T(Object) / T(Array) / T(Set) / T(Dot) / T(Paren) /
-    ArithToken / BoolToken / T(And) / T(Or) / T(ExprCall);
-  const auto inline RuleRefToken = T(Var) / T(Dot) / T(Array);
-
   inline const std::set<std::string> Keywords(
     {"if", "in", "contains", "every"});
 
-  inline const std::set<Token> RuleTypes(
-    {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
-
   bool all_alnum(const std::string_view& str);
   bool contains_local(const Node& node);
   bool contains_ref(const Node& node);
@@ -78,7 +53,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, const BuiltIns& builtins);
+    static Node 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);
@@ -123,51 +98,59 @@ namespace rego
       Node& object, const std::string& path, const Node& value);
   };
 
-  PassDef input_data();
-  PassDef modules();
-  PassDef imports();
-  PassDef keywords();
-  PassDef lists();
-  PassDef ifs();
-  PassDef elses();
-  PassDef rules();
-  PassDef build_calls();
-  PassDef membership();
-  PassDef build_refs();
-  PassDef structure();
-  PassDef strings();
-  PassDef merge_data();
-  PassDef lift_refheads();
-  PassDef symbols();
-  PassDef replace_argvals();
-  PassDef lift_query();
-  PassDef expand_imports();
-  PassDef constants();
-  PassDef explicit_enums();
-  PassDef body_locals(const BuiltIns& builtins);
-  PassDef value_locals(const BuiltIns& builtins);
-  PassDef compr_locals(const BuiltIns& builtins);
-  PassDef rules_to_compr();
-  PassDef compr();
-  PassDef absolute_refs();
-  PassDef merge_modules();
-  PassDef datarule();
-  PassDef skips();
-  PassDef unary();
-  PassDef multiply_divide();
-  PassDef add_subtract();
-  PassDef comparison();
-  PassDef assign(const BuiltIns& builtins);
-  PassDef skip_refs(const BuiltIns& builtins);
-  PassDef simple_refs();
-  PassDef init();
-  PassDef implicit_enums();
-  PassDef enum_locals();
-  PassDef rulebody();
-  PassDef lift_to_rule();
-  PassDef functions();
-  PassDef unify(const BuiltIns& builtins);
-  PassDef query();
+  inline const auto NewLine = TokenDef("rego-newline");
+  inline const auto Brace = TokenDef("rego-brace");
+  inline const auto Dot = TokenDef("rego-dot");
+  inline const auto Colon = TokenDef("rego-colon");
+  inline const auto Square = TokenDef("rego-square");
+  inline const auto Paren = TokenDef("rego-paren");
+  inline const auto Comma = TokenDef("rego-comma");
+  inline const auto EmptySet = TokenDef("rego-emptyset");
+
+  // Utility tokens
+  inline const auto Op = TokenDef("rego-op");
+  inline const auto Id = TokenDef("rego-id");
+  inline const auto Head = TokenDef("rego-head");
+  inline const auto Tail = TokenDef("rego-tail");
+  inline const auto Lhs = TokenDef("rego-lhs");
+  inline const auto Rhs = TokenDef("rego-rhs");
+  inline const auto LhsVars = TokenDef("rego-lhsvars");
+  inline const auto RhsVars = TokenDef("rego-rhsvars");
+  inline const auto RefTerm = TokenDef("rego-refterm");
+  inline const auto NumTerm = TokenDef("rego-numterm");
+  inline const auto AssignArg = TokenDef("rego-assignarg");
+  inline const auto Idx = TokenDef("rego-idx");
+  inline const auto ArgSeq = TokenDef("rego-argseq");
+
+  inline const auto wf_json = JSONString | Int | Float | True | False | Null;
+  inline const auto wf_parse_tokens = Query | Module | wf_json | wf_arith_op |
+    wf_bool_op | wf_bin_op | Package | Var | Brace | Square | Dot | Paren |
+    Assign | Unify | EmptySet | Colon | RawString | Default | Some | Import |
+    Else | As | With | NewLine | Comma;
+
+  // clang-format off
+  inline const auto wf_parser =
+    (Top <<= File)
+    | (File <<= Group)
+    | (Module <<= Group++[1])
+    | (Query <<= Group)
+    | (Brace <<= Group++)
+    | (Square <<= Group++)
+    | (Paren <<= Group++)
+    | (Group <<= wf_parse_tokens++)
+    ;
+  // clang-format on
+
+  Parse parser();
+
+  inline const auto Input = TokenDef("rego-input", flag::lookup);
+  inline const auto DataModule = TokenDef("rego-datamodule", flag::lookup);
+  inline const auto DataTerm = TokenDef("rego-dataterm");
+  inline const auto DataObject = TokenDef("rego-dataobject");
+  inline const auto DataObjectItem = TokenDef("rego-dataobjectitem");
+  inline const auto DataArray = TokenDef("rego-dataarray");
+  inline const auto DataSet = TokenDef("rego-dataset");
+  inline const auto DataRule = TokenDef("rego-datarule", flag::lookup);
 
   template <typename I, typename S, typename F>
   S& join(S& stream, const I& begin, const I& end, const char* sep, F&& writer)
@@ -212,298 +195,6 @@ namespace rego
   template <typename K, typename V>
   MapValuesStr(const std::map<K, V>&) -> MapValuesStr<K, V>;
 
-  class ValueDef;
-  using rank_t = std::size_t;
-  using Value = std::shared_ptr<ValueDef>;
-  using Values = std::vector<Value>;
-  using RankedNode = std::pair<rank_t, Node>;
-
-  class ValueDef
-  {
-  public:
-    void mark_as_valid();
-    void mark_as_invalid();
-    void reduce_set();
-
-    const std::string& str() const;
-    const std::string& json() const;
-    bool depends_on(const Value& value) const;
-    bool invalid() const;
-    const Location& var() const;
-    const Node& node() const;
-    const Values& sources() const;
-    Node to_term() const;
-    rank_t rank() const;
-    friend std::ostream& operator<<(std::ostream& os, const Value& value);
-    friend std::ostream& operator<<(std::ostream& os, const ValueDef& value);
-    friend bool operator==(const Value& lhs, const Value& rhs);
-    friend bool operator<(const Value& lhs, const Value& rhs);
-
-    static Value create(const RankedNode& value);
-    static Value create(const Location& var, const RankedNode& value);
-    static Value create(
-      const Location& var, const RankedNode& value, const Values& sources);
-    static Value create(const Node& value);
-    static Value create(const Location& var, const Node& value);
-    static Value create(
-      const Location& var, const Node& value, const Values& sources);
-    static Value copy_to(const Value& value, const Location& var);
-    static Values filter_by_rank(const Values& values);
-    static rank_t get_rank(const Node& node);
-
-  private:
-    static void build_string(
-      std::ostream& buf,
-      const ValueDef& current,
-      const Location& root,
-      bool first);
-    Location m_var;
-    Node m_node;
-    Values m_sources;
-    bool m_invalid;
-    rank_t m_rank;
-    std::string m_str;
-    std::string m_json;
-    ValueDef(
-      const Location& var,
-      const Node& value,
-      const Values& sources,
-      rank_t rank);
-    ValueDef(const Node& value);
-    ValueDef(const RankedNode& value);
-    ValueDef(const Location& var, const Node& value);
-    ValueDef(const Location& var, const RankedNode& value);
-    ValueDef(const Location& var, const Node& value, const Values& sources);
-    ValueDef(
-      const Location& var, const RankedNode& value, const Values& sources);
-  };
-
-  class ValueMap
-  {
-  public:
-    ValueMap();
-    ValueMap(
-      const Values::const_iterator& begin, const Values::const_iterator& end);
-    bool contains(const Value& value) const;
-    bool intersect_with(const Values& values);
-    bool remove_values_not_contained_in(const Values& values);
-    void clear();
-    bool insert(const Value& value);
-    bool erase(const std::string& json);
-    bool empty() const;
-    Nodes to_terms() const;
-    Values valid_values() const;
-    bool remove_invalid_values();
-    void mark_valid_values(bool include_falsy);
-    void mark_invalid_values();
-
-    friend std::ostream& operator<<(std::ostream& os, const ValueMap& values);
-
-  private:
-    std::multimap<std::string, Value> m_map;
-    std::set<std::pair<std::string, std::string>> m_values;
-    std::set<std::string> m_keys;
-  };
-
-  /**
-   * The Args class provides an interable interface over the Cartesian product
-   * of all sets of arguments to a function. It will contain N sets of argument
-   * sources, and will produce s_0 * s_1 * ... * s_n arguments, where s_i is the
-   * size of the i-th argument source.
-   */
-  class Args
-  {
-  public:
-    Args();
-    void push_back_source(const Values& values);
-    void mark_invalid(const std::set<Value>& active) const;
-    void mark_invalid_except(const std::set<Value>& active) const;
-    Values at(std::size_t index) const;
-    std::size_t size() const;
-    std::string str() const;
-    std::size_t source_size() const;
-    const Values& source_at(std::size_t index) const;
-    Args subargs(std::size_t start) const;
-    friend std::ostream& operator<<(std::ostream& os, const Args& args);
-
-  private:
-    std::vector<Values> m_values;
-    std::vector<size_t> m_stride;
-    std::size_t m_size;
-  };
-
-  class Variable
-  {
-  public:
-    Variable(const Node& local, std::size_t id);
-    bool unify(const Values& others);
-    std::string str() const;
-    bool remove_invalid_values();
-    void mark_invalid_values();
-    void mark_valid_values();
-    Values valid_values() const;
-    Node to_term() const;
-    Node bind();
-    void reset();
-
-    bool is_unify() const;
-    bool is_user_var() const;
-    Location name() const;
-    std::size_t id() const;
-
-    static bool is_unify(const std::string_view& name);
-    static bool is_user_var(const std::string_view& name);
-
-    friend std::ostream& operator<<(std::ostream& os, const Variable& variable);
-    friend std::ostream& operator<<(
-      std::ostream& os, const std::map<Location, Variable>& variables);
-
-  private:
-    bool intersect_with(const Values& others);
-    bool initialize(const Values& others);
-
-    Node m_local;
-    ValueMap m_values;
-    bool m_unify;
-    bool m_user_var;
-    bool m_initialized;
-    std::size_t m_id;
-  };
-
-  enum class UnifierType
-  {
-    RuleBody,
-    RuleValue
-  };
-
-  struct UnifierKey
-  {
-    Location key;
-    UnifierType type;
-    bool operator<(const UnifierKey& other) const;
-  };
-
-  class UnifierDef;
-  using Unifier = std::shared_ptr<UnifierDef>;
-  using CallStack = std::shared_ptr<std::vector<Location>>;
-  using ValuesLookup = std::map<std::string, Values>;
-  using WithStack = std::shared_ptr<std::vector<ValuesLookup>>;
-  using UnifierCache = std::shared_ptr<std::map<UnifierKey, Unifier>>;
-
-  class UnifierDef
-  {
-  public:
-    UnifierDef(
-      const Location& rule,
-      const Node& rulebody,
-      CallStack call_stack,
-      WithStack with_stack,
-      const BuiltIns& builtins,
-      UnifierCache unifier_cache);
-    Node unify();
-    Nodes expressions() const;
-    Nodes bindings() const;
-    std::string str() const;
-    std::string dependency_str() const;
-    static Unifier create(
-      const UnifierKey& key,
-      const Location& rule,
-      const Node& rulebody,
-      const CallStack& call_stack,
-      const WithStack& with_stack,
-      const BuiltIns& builtins,
-      const UnifierCache& unifier_cache);
-    std::optional<Node> resolve_rule(const Nodes& defs) const;
-    std::optional<Node> resolve_rulecomp(const Nodes& rulecomp) const;
-    std::optional<Node> resolve_ruleset(const Nodes& ruleset) const;
-    std::optional<Node> resolve_ruleobj(const Nodes& ruleobj) const;
-    std::optional<RankedNode> resolve_rulefunc(
-      const Node& rulefunc, const Nodes& args) const;
-    Node resolve_module(const Node& module) const;
-
-    struct Dependency
-    {
-      std::string name;
-      std::set<std::size_t> dependencies;
-      std::size_t score;
-    };
-
-    struct Statement
-    {
-      std::size_t id;
-      Node node;
-    };
-
-  private:
-    Unifier rule_unifier(
-      const UnifierKey& key, const Location& rule, const Node& rulebody) const;
-    void init_from_body(
-      const Node& rulebody,
-      std::vector<Statement>& statements,
-      std::size_t root);
-    std::size_t add_variable(const Node& local);
-    std::size_t add_unifyexpr(const Node& unifyexpr);
-    void add_withpush(const Node& withpush);
-    void add_withpop(const Node& withpop);
-    void reset();
-
-    void compute_dependency_scores();
-    std::size_t compute_dependency_score(
-      std::size_t index, std::set<size_t>& visited);
-    std::size_t dependency_score(const Variable& var) const;
-    std::size_t dependency_score(const Statement& stmt) const;
-    std::size_t detect_cycles() const;
-    bool has_cycle(std::size_t id) const;
-
-    Values evaluate(const Location& var, const Node& value);
-    Values evaluate_function(
-      const Location& var, const std::string& func_name, const Args& args);
-    Values resolve_var(const Node& var, bool exclude_with = false);
-    Values enumerate(const Location& var, const Node& container);
-    Values resolve_skip(const Node& skip);
-    Values check_with(const Node& var, bool bypass_recurse_check = false);
-    Values apply_access(const Location& var, const Values& args);
-    std::optional<Value> call_function(
-      const Location& var, const Values& args) const;
-    std::optional<Value> call_named_function(
-      const Location& var, const std::string& name, const Values& args);
-    bool is_local(const Node& var);
-    std::size_t scan_vars(const Node& expr, std::vector<Location>& locals);
-    void pass();
-    void execute_statements(
-      std::vector<Statement>::iterator begin,
-      std::vector<Statement>::iterator end);
-    void remove_invalid_values();
-    void mark_invalid_values();
-    Variable& get_variable(const Location& name);
-    bool is_variable(const Location& name) const;
-    Node bind_variables();
-    Args create_args(const Node& args);
-    bool would_recurse(const Node& node);
-
-    bool push_rule(const Location& rule);
-    void pop_rule(const Location& rule);
-
-    void push_with(const Node& withseq);
-    void pop_with();
-
-    void push_not();
-    void pop_not();
-
-    Location m_rule;
-    std::map<Location, Variable> m_variables;
-    std::vector<Statement> m_statements;
-    std::map<std::size_t, std::vector<Statement>> m_nested_statements;
-    CallStack m_call_stack;
-    WithStack m_with_stack;
-    const BuiltIns& m_builtins;
-    UnifierCache m_cache;
-    std::size_t m_retries;
-    Token m_parent_type;
-    std::vector<Dependency> m_dependency_graph;
-    bool m_negate;
-  };
-
   class ActionMetrics
   {
   public:
@@ -637,60 +328,6 @@ namespace rego
 
 }
 
-// Use ADL to extend Trieste logging capabilities with additional types.
-namespace trieste::logging
-{
-  inline void append(
-    Log& log, const std::vector<rego::UnifierDef::Dependency>& deps)
-  {
-    for (auto it = deps.begin(); it != deps.end(); ++it)
-    {
-      auto& dep = *it;
-      log << "[" << dep.name << "](" << dep.score << ") -> {";
-
-      logging::Sep sep{", "};
-      for (auto& idx : dep.dependencies)
-      {
-        log << sep << deps[idx].name;
-      }
-      log << "}" << std::endl;
-    }
-  }
-
-  template <typename T>
-  inline void append(Log& log, const std::vector<T>& values)
-  {
-    log << "[";
-    logging::Sep sep{", "};
-    for (auto& value : values)
-    {
-      log << sep << value;
-    }
-    log << "]" << std::endl;
-  }
-
-  inline void append(Log& log, const Location& loc)
-  {
-    log << loc.view();
-  }
-
-  inline void append(Log& log, const rego::UnifierDef::Statement& statement)
-  {
-    rego::Resolver::stmt_str(log, statement.node);
-  }
-
-  template <typename K, typename V>
-  inline void append(Log& log, const rego::MapValuesStr<K, V>& map)
-  {
-    log << "{" << std::endl;
-    for (auto& [_, value] : map.values)
-    {
-      log << value << std::endl;
-    }
-    log << "}" << std::endl;
-  }
-} // trieste::logging
-
 #ifdef REGOCPP_ACTION_METRICS
 #define ACTION() rego::ActionMetrics __action_metrics(__FILE__, __LINE__)
 #define PRINT_ACTION_METRICS() rego::ActionMetrics::print()
diff --git a/src/interpreter.cc b/src/interpreter.cc
index fab4991e..54c34b9d 100644
--- a/src/interpreter.cc
+++ b/src/interpreter.cc
@@ -1,4 +1,14 @@
-#include "internal.hh"
+#include "rego.hh"
+#include "trieste/json.h"
+#include "trieste/wf.h"
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+  using namespace wf::ops;
+  const auto wf_errors = rego::wf | (Error <<= ErrorMsg * ErrorAst * ErrorCode);
+}
 
 namespace rego
 {
@@ -7,40 +17,59 @@ namespace rego
   using timestamp = std::chrono::high_resolution_clock::time_point;
 
   Interpreter::Interpreter() :
-    m_parser(parser()),
-    m_module_seq(NodeDef::create(ModuleSeq)),
-    m_data_seq(NodeDef::create(DataSeq)),
-    m_input(NodeDef::create(Input)),
+    m_reader(reader()),
     m_debug_path("."),
-    m_debug_enabled(false),
-    m_well_formed_checks_enabled(false)
+    m_builtins(BuiltInsDef::create()),
+    m_unify(unify(m_builtins)),
+    m_json(json::reader()),
+    m_from_json(from_json()),
+    m_to_input(to_input()),
+    m_data_count(0)
   {
-    wf::push_back(wf_parser);
-    m_builtins.register_standard_builtins();
+    m_ast = Top << (Rego << Query << Input << DataSeq << ModuleSeq);
   }
 
-  Interpreter::~Interpreter()
+  void Interpreter::merge(const Node& node)
   {
-    wf::pop_front();
-  }
+    WFContext context(wf_unify_input);
+    Node rego = m_ast / Rego;
+    if (node == Input)
+    {
+      rego->replace(rego / Input, node);
+    }
+    else if (node == Data)
+    {
+      rego / DataSeq << node;
+    }
+    else if (node == Module)
+    {
+      // insert the module to maintain a sorted list (by package name).
+      // This allows us to merge modules with the same name after their
+      // imports are resolved.
+      Node moduleseq = rego / ModuleSeq;
+      auto pos = std::upper_bound(
+        moduleseq->begin(), moduleseq->end(), node, [](auto& a, auto& b) {
+          auto a_pkg = a->front();
+          auto b_pkg = b->front();
+          auto a_str = std::string(a_pkg->location().view());
+          auto b_str = std::string(b_pkg->location().view());
+          return a_pkg->location() < b_pkg->location();
+        });
 
-  void Interpreter::insert_module(const Node& module)
-  {
-    // sort the modules by their package name. This allows us to merge
-    // modules with the same name after their imports are resolved.
-    auto pos = std::upper_bound(
-      m_module_seq->begin(), m_module_seq->end(), module, [](auto& a, auto& b) {
-        auto a_pkg = a->front();
-        auto b_pkg = b->front();
-        auto a_str = std::string(a_pkg->location().view());
-        auto b_str = std::string(b_pkg->location().view());
-        return a_pkg->location() < b_pkg->location();
-      });
-
-    m_module_seq->insert(pos, module);
+      moduleseq->insert(pos, node);
+    }
+    else if (node == Query)
+    {
+      rego->replace(rego / Query, node);
+    }
+    else
+    {
+      logging::Error() << node;
+      throw std::runtime_error("Invalid node type");
+    }
   }
 
-  void Interpreter::add_module_file(const std::filesystem::path& path)
+  bool Interpreter::add_module_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -48,21 +77,39 @@ namespace rego
     }
 
     logging::Info() << "Adding module file: " << path;
-    auto file_ast = m_parser.sub_parse(path);
-    insert_module(file_ast);
+    std::string debug = "module" + std::to_string(m_data_count++);
+    auto result = m_reader.file(path).debug_path(m_debug_path / debug).read();
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(result.ast->front());
+    return true;
   }
 
-  void Interpreter::add_module(
+  bool Interpreter::add_module(
     const std::string& name, const std::string& contents)
   {
-    auto module_source = SourceDef::synthetic(contents);
-    auto module = m_parser.sub_parse(name, File, module_source);
-    insert_module(module);
+    std::string debug = "module" + std::to_string(m_data_count++);
+    auto result =
+      m_reader.synthetic(contents).debug_path(m_debug_path / debug).read();
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(result.ast->front());
     logging::Info() << "Adding module: " << name << "(" << contents.size()
                     << " bytes)";
+    return true;
   }
 
-  void Interpreter::add_data_json_file(const std::filesystem::path& path)
+  bool Interpreter::add_data_json_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -70,25 +117,55 @@ namespace rego
     }
 
     logging::Info() << "Adding data file: " << path;
-    auto file_ast = m_parser.sub_parse(path);
-    m_data_seq->push_back(file_ast);
+    std::string debug = "data" + std::to_string(m_data_count++);
+    auto result =
+      m_json.file(path) >> m_from_json.debug_path(m_debug_path / debug);
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(Data << result.ast->front());
+    return true;
   }
 
-  void Interpreter::add_data_json(const std::string& json)
+  bool Interpreter::add_data_json(const std::string& json)
   {
-    auto data_source = SourceDef::synthetic(json);
-    auto data = m_parser.sub_parse("data", File, data_source);
-    m_data_seq->push_back(data);
     logging::Info() << "Adding data (" << json.size() << " bytes)";
+    std::string debug = "data" + std::to_string(m_data_count++);
+    auto result =
+      m_json.synthetic(json) >> m_from_json.debug_path(m_debug_path / debug);
+
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(Data << result.ast->front());
+    return true;
   }
 
-  void Interpreter::add_data(const Node& node)
+  bool Interpreter::add_data(const Node& node)
   {
-    m_data_seq->push_back(node);
-    logging::Info() << "Adding data AST";
+    logging::Info() << "Adding data from JSON AST";
+    std::string debug = "data" + std::to_string(m_data_count++);
+    auto result = node >> m_from_json.debug_path(m_debug_path / debug);
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(Data << result.ast->front());
+    return true;
   }
 
-  void Interpreter::set_input_json_file(const std::filesystem::path& path)
+  bool Interpreter::set_input_json_file(const std::filesystem::path& path)
   {
     if (!std::filesystem::exists(path))
     {
@@ -96,90 +173,85 @@ namespace rego
     }
 
     logging::Info() << "Setting input from file: " << path;
-    auto file_ast = m_parser.sub_parse(path);
-    m_input = Input << file_ast;
+    auto result =
+      m_json.file(path) >> m_from_json.debug_path(m_debug_path / "input");
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(Input << result.ast->front());
+    return true;
   }
 
-  void Interpreter::set_input_json(const std::string& json)
+  bool Interpreter::set_input_term(const std::string& term)
   {
-    logging::Info() << "Setting input (" << json.size() << " bytes)";
-    auto input_source = SourceDef::synthetic(json);
-    auto ast = m_parser.sub_parse("input", File, input_source);
-    m_input = Input << ast;
+    logging::Info() << "Setting input (" << term.size() << " bytes)";
+    auto result = m_reader.synthetic(term) >> m_to_input;
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(result.ast->front());
+    return true;
   }
 
-  void Interpreter::set_input(const Node& node)
+  bool Interpreter::set_input(const Node& node)
   {
-    logging::Info() << "Setting input AST";
-    m_input = Input << node;
+    logging::Info() << "Setting input from JSON AST";
+    auto result = node >> m_from_json.debug_path(m_debug_path / "input");
+    if (!result.ok)
+    {
+      logging::Error err;
+      result.print_errors(err);
+      return false;
+    }
+
+    merge(Input << result.ast->front());
+    return true;
   }
 
-  Node Interpreter::raw_query(const std::string& query_expr) const
+  Node Interpreter::raw_query(const std::string& query_expr)
   {
     logging::Info() << "Query: " << query_expr;
-    auto ast = NodeDef::create(Top);
-    auto rego = NodeDef::create(rego::Rego);
     auto query_src = SourceDef::synthetic(query_expr);
-    auto query = m_parser.sub_parse("query", rego::Query, query_src);
-    if (m_input->size() == 0)
+    auto result =
+      m_reader.synthetic(query_expr).debug_path(m_debug_path / "query").read();
+    if (!result.ok)
     {
-      m_input->push_back(NodeDef::create(Undefined));
+      logging::Error err;
+      result.print_errors(err);
+      return ErrorSeq << result.errors;
     }
 
-    rego->push_back(query);
-    rego->push_back(m_input->clone());
-    rego->push_back(m_data_seq->clone());
-    rego->push_back(m_module_seq->clone());
-    ast->push_back(rego);
-
-    auto passes = rego::passes(m_builtins);
-    PassRange pass_range(passes, m_parser.wf(), "parse");
-    bool ok;
-    Nodes error_nodes;
-    std::string failed_pass;
-    {
-      logging::Info summary;
-      summary << "---------" << std::endl;
-      std::filesystem::path debug_path;
-      if (m_debug_enabled)
-        debug_path = m_debug_path;
-      auto p = default_process(
-        summary, m_well_formed_checks_enabled, "rego-cpp", debug_path);
-
-      p.set_error_pass(
-        [&error_nodes, &failed_pass](Nodes& errors, std::string pass_name) {
-          error_nodes = errors;
-          failed_pass = pass_name;
-        });
+    merge(result.ast->front());
 
-      ok = p.build(ast, pass_range);
-      summary << "---------" << std::endl;
-    }
-
-    if (ok)
     {
-      logging::Info() << "Query result: " << ast;
-      PRINT_ACTION_METRICS();
-      return ast;
+      WFContext context(wf_unify_input);
+      Node input = m_ast / Rego / Input;
+      if (input->empty())
+      {
+        input << Undefined;
+      }
     }
 
-    logging::Trace() << "Query failed: " << failed_pass;
-    if (error_nodes.empty())
+    result = m_ast >> m_unify;
+    if (!result.ok)
     {
-      logging::Trace() << "No error nodes so assuming wf error";
-      error_nodes.push_back(err(
-        ast->clone(), "Failed at pass " + failed_pass, "well_formed_error"));
+      logging::Error err;
+      result.print_errors(err);
+      return ErrorSeq << result.errors;
     }
 
-    Node error_result = NodeDef::create(ErrorSeq);
-    for (auto& error : error_nodes)
-    {
-      error_result->push_back(error);
-    }
-    return error_result;
+    return result.ast->front();
   }
 
-  std::string Interpreter::query(const std::string& query_expr) const
+  std::string Interpreter::query(const std::string& query_expr)
   {
     return output_to_string(raw_query(query_expr));
   }
@@ -189,6 +261,7 @@ namespace rego
     std::ostringstream output_buf;
     if (ast->type() == ErrorSeq)
     {
+      WFContext context(wf_errors);
       output_buf << "errors:" << std::endl;
 
       for (auto& error : *ast)
@@ -204,12 +277,13 @@ 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_json(result, true); });
+        [](auto& result) { return rego::to_key(result); });
       std::sort(results.begin(), results.end());
       join(
         output_buf,
@@ -237,6 +311,11 @@ namespace rego
 
       std::filesystem::create_directory(m_debug_path);
     }
+
+    m_json.debug_path(m_debug_path / "json");
+    m_unify.debug_path(m_debug_path / "unify");
+    m_to_input.debug_path(m_debug_path / "input");
+
     return *this;
   }
 
@@ -247,38 +326,36 @@ namespace rego
 
   Interpreter& Interpreter::debug_enabled(bool enabled)
   {
-    m_debug_enabled = enabled;
+    m_reader.debug_enabled(enabled);
+    m_json.debug_enabled(enabled);
+    m_unify.debug_enabled(enabled);
+    m_from_json.debug_enabled(enabled);
+    m_to_input.debug_enabled(enabled);
     return *this;
   }
 
   bool Interpreter::debug_enabled() const
   {
-    return m_debug_enabled;
+    return m_reader.debug_enabled();
   }
 
-  Interpreter& Interpreter::well_formed_checks_enabled(bool enabled)
+  Interpreter& Interpreter::wf_check_enabled(bool enabled)
   {
-    m_well_formed_checks_enabled = enabled;
+    m_reader.wf_check_enabled(enabled);
+    m_json.wf_check_enabled(enabled);
+    m_unify.wf_check_enabled(enabled);
+    m_from_json.wf_check_enabled(enabled);
+    m_to_input.wf_check_enabled(enabled);
     return *this;
   }
 
-  bool Interpreter::well_formed_checks_enabled() const
+  bool Interpreter::wf_check_enabled() const
   {
-    return m_well_formed_checks_enabled;
+    return m_reader.wf_check_enabled();
   }
 
-  BuiltIns& Interpreter::builtins()
+  BuiltIns Interpreter::builtins() const
   {
     return m_builtins;
   }
-
-  const BuiltIns& Interpreter::builtins() const
-  {
-    return m_builtins;
-  }
-
-  const wf::Wellformed& Interpreter::output_wf() const
-  {
-    return rego::passes(m_builtins).back()->wf();
-  }
 }
diff --git a/src/json.cc b/src/json.cc
new file mode 100644
index 00000000..6054be34
--- /dev/null
+++ b/src/json.cc
@@ -0,0 +1,245 @@
+#include "trieste/json.h"
+
+#include "internal.hh"
+#include "rego.hh"
+#include "unify.hh"
+
+namespace
+{
+  using namespace trieste;
+  using namespace rego;
+  using namespace wf::ops;
+
+  // clang-format off
+    const auto wf_from_json_dataterm =
+    (Top <<= DataTerm)
+    | (DataObject <<= DataObjectItem++)
+    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
+    | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
+    | (DataArray <<= DataTerm++)
+    | (DataSet <<= DataTerm++)
+    | (Scalar <<= String | Int | Float | True | False | Null)
+    | (String <<= JSONString)
+    ;
+  // clang-format on
+
+  // clang-format off
+    const auto wf_from_json_term =
+    (Top <<= Term)
+    | (Object <<= ObjectItem++)
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
+    | (Term <<= Scalar | Array | Object | Set)
+    | (Array <<= Term++)
+    | (Set <<= Term++)
+    | (Scalar <<= JSONString | Int | Float | True | False | Null)
+    ;
+  // clang-format on
+
+  const char* FloatRE =
+    R"(\-?[[:digit:]]+\.[[:digit:]]+(?:e[+-]?[[:digit:]]+)?)";
+  const char* IntRE = R"(\-?[[:digit:]]+)";
+
+  PassDef from_json_to_dataterm()
+  {
+    return {
+      "from_json",
+      wf_from_json_dataterm,
+      dir::bottomup | dir::once,
+      {
+        T(json::String)[String] >>
+          [](Match& _) {
+            return DataTerm << (Scalar << (String << (JSONString ^ _(String))));
+          },
+
+        T(json::Key)[Key] >>
+          [](Match& _) {
+            return DataTerm << (Scalar << (String << (JSONString ^ _(Key))));
+          },
+
+        T(json::Number, FloatRE)[Float] >>
+          [](Match& _) { return DataTerm << (Scalar << (Float ^ _(Float))); },
+
+        T(json::Number, IntRE)[Int] >>
+          [](Match& _) { return DataTerm << (Scalar << (Int ^ _(Int))); },
+
+        T(json::True)[True] >>
+          [](Match& _) { return DataTerm << (Scalar << (True ^ _(True))); },
+
+        T(json::False)[False] >>
+          [](Match& _) { return DataTerm << (Scalar << (False ^ _(False))); },
+
+        T(json::Null)[Null] >>
+          [](Match& _) { return DataTerm << (Scalar << (Null ^ _(Null))); },
+
+        T(json::Member)[DataObjectItem] >>
+          [](Match& _) { return DataObjectItem << *_[DataObjectItem]; },
+
+        T(json::Object)[DataObject] >>
+          [](Match& _) { return DataTerm << (DataObject << *_[DataObject]); },
+
+        T(json::Array)[DataArray] >>
+          [](Match& _) { return DataTerm << (DataArray << *_[DataArray]); },
+      }};
+  }
+
+  PassDef from_json_to_term()
+  {
+    return {
+      "from_json",
+      wf_from_json_term,
+      dir::bottomup | dir::once,
+      {
+        T(json::String)[String] >>
+          [](Match& _) { return Term << (Scalar << (JSONString ^ _(String))); },
+
+        T(json::Key)[Key] >>
+          [](Match& _) { return Term << (Scalar << (JSONString ^ _(Key))); },
+
+        T(json::Number, FloatRE)[Float] >>
+          [](Match& _) { return Term << (Scalar << (Float ^ _(Float))); },
+
+        T(json::Number, IntRE)[Int] >>
+          [](Match& _) { return Term << (Scalar << (Int ^ _(Int))); },
+
+        T(json::True)[True] >>
+          [](Match& _) { return Term << (Scalar << (True ^ _(True))); },
+
+        T(json::False)[False] >>
+          [](Match& _) { return Term << (Scalar << (False ^ _(False))); },
+
+        T(json::Null)[Null] >>
+          [](Match& _) { return Term << (Scalar << (Null ^ _(Null))); },
+
+        T(json::Member)[ObjectItem] >>
+          [](Match& _) { return ObjectItem << *_[ObjectItem]; },
+
+        T(json::Object)[Object] >>
+          [](Match& _) { return Term << (Object << *_[Object]); },
+
+        T(json::Array)[Array] >>
+          [](Match& _) { return Term << (Array << *_[Array]); },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_binding_term =
+    (Top <<= Term)
+    | (Term <<= Scalar | Array | Object | Set)
+    | (Scalar <<= Int | Float | True | False | Null | JSONString)
+    | (Array <<= Term++)
+    | (Object <<= ObjectItem++)
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
+    | (Set <<= Term++)
+    ;
+  // clang-format on
+
+  PassDef to_json_()
+  {
+    return {
+      "to_json",
+      json::wf,
+      dir::bottomup | dir::once,
+      {
+        (T(Term) << (T(Scalar) << T(Int, Float)[json::Number])) >>
+          [](Match& _) { return json::Number ^ _(json::Number); },
+
+        (T(Term) << (T(Scalar) << T(JSONString)[json::String])) >>
+          [](Match& _) { return json::String ^ _(json::String); },
+
+        (T(Term) << (T(Scalar) << T(True)[json::True])) >>
+          [](Match& _) { return json::True ^ _(json::True); },
+
+        (T(Term) << (T(Scalar) << T(False)[json::False])) >>
+          [](Match& _) { return json::False ^ _(json::False); },
+
+        (T(Term) << (T(Scalar) << T(Null)[json::Null])) >>
+          [](Match& _) { return json::Null ^ _(json::Null); },
+
+        (T(Term) << (T(Array, Set)[json::Array])) >>
+          [](Match& _) {
+            Node array = _(json::Array);
+            Nodes children(array->begin(), array->end());
+            if (array == Set)
+            {
+              NodeMap<std::string> strs;
+              std::sort(
+                children.begin(), children.end(), [&strs](Node a, Node b) {
+                  std::string lhs;
+                  if (contains(strs, a))
+                  {
+                    lhs = strs[a];
+                  }
+                  else
+                  {
+                    lhs = json::to_string(a);
+                    strs[a] = lhs;
+                  }
+
+                  std::string rhs;
+                  if (contains(strs, b))
+                  {
+                    rhs = strs[b];
+                  }
+                  else
+                  {
+                    rhs = json::to_string(b);
+                    strs[b] = rhs;
+                  }
+
+                  return lhs < rhs;
+                });
+            }
+            return json::Array << children;
+          },
+
+        (T(Term) << (T(Object)[json::Object])) >>
+          [](Match& _) {
+            Node object = _(json::Object);
+            std::vector<std::pair<std::string, Node>> members;
+            for (auto& child : *object)
+            {
+              std::string key = strip_quotes(json::to_string(child / Key));
+              members.push_back({key, child / Val});
+            }
+
+            std::sort(members.begin(), members.end(), [](auto& a, auto& b) {
+              return a.first < b.first;
+            });
+
+            Nodes children;
+            std::transform(
+              members.begin(),
+              members.end(),
+              std::back_inserter(children),
+              [](auto& member) {
+                return json::Member << (json::Key ^ member.first)
+                                    << member.second;
+              });
+
+            return json::Object << children;
+          },
+      }};
+  }
+}
+
+namespace rego
+{
+  Rewriter from_json(bool as_term)
+  {
+    auto pass = as_term ? from_json_to_term() : from_json_to_dataterm();
+    return {
+      "from_json",
+      {pass},
+      json::wf,
+    };
+  }
+
+  Rewriter to_json()
+  {
+    return {
+      "to_json",
+      {to_json_()},
+      wf_binding_term,
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/parse.cc b/src/parse.cc
index ffec4988..da4959ac 100644
--- a/src/parse.cc
+++ b/src/parse.cc
@@ -1,6 +1,6 @@
 #include "internal.hh"
 
-namespace rego
+namespace
 {
   const std::string alpha =
     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -38,242 +38,196 @@ namespace rego
     return buf.str();
   }
 
+  enum class NewlineMode
+  {
+    Terminate,
+    Ignore,
+    Token,
+  };
+}
+
+namespace rego
+{
   Parse parser()
   {
     Parse p(depth::file, wf_parser);
 
+    std::shared_ptr<NewlineMode> newline_mode =
+      std::make_shared<NewlineMode>(NewlineMode::Ignore);
+
+    // Our starting path tries to determine if the input is a module or a query
     p("start",
       {
-        // A newline sometimes terminates.
-        "\r*\n([[:blank:]]*)" >>
+        R"(\s+)" >>
+          [](auto&) {
+            // ignore leading whitespace
+          },
+
+        R"(package)" >>
+          [newline_mode](auto& m) {
+            m.push(Module);
+            m.add(Package);
+            m.mode("main");
+            *newline_mode = NewlineMode::Terminate;
+          },
+
+        "^" >>
           [](auto& m) {
-            if (m.in(List))
-            {
-              return;
-            }
+            m.push(Query);
+            m.mode("main");
+          },
+      });
 
-            m.term({List});
-            if (m.in(Some))
-            {
-              m.pop(Some);
-              m.term();
-            }
-            if (m.in(With))
-            {
-              m.pop(With);
-              m.term();
-            }
+    p("main",
+      {
+        R"(import\b)" >>
+          [newline_mode](auto& m) {
+            m.add(Import);
+            *newline_mode = NewlineMode::Terminate;
           },
 
-        // Whitespace between tokens.
-        "[[:blank:]]+" >> [](auto&) {},
+        R"(default\b)" >> [](auto& m) { m.add(Default); },
 
-        // terminator
-        ";" >> [](auto& m) { m.term(); },
+        R"(not\b)" >> [](auto& m) { m.add(Not); },
 
-        // Assign.
-        ":=" >> [](auto& m) { m.add(Assign); },
+        R"(with\b)" >> [](auto& m) { m.add(With); },
 
-        // Key/Value
-        ":" >> [](auto& m) { m.add(Colon); },
+        R"(else\b)" >> [](auto& m) { m.add(Else); },
 
-        // List
-        "," >> [](auto& m) { m.seq(List); },
+        R"(some\b)" >> [](auto& m) { m.add(Some); },
 
-        // Or
-        "\\|" >> [](auto& m) { m.add(Or); },
+        R"(true\b)" >> [](auto& m) { m.add(True); },
 
-        // And
-        "&" >> [](auto& m) { m.add(And); },
+        R"(false\b)" >> [](auto& m) { m.add(False); },
 
-        // OPA local variable.
-        R"(__[[:alnum:]]+__\b)" >> [](auto& m) { m.add(Var); },
+        R"(null\b)" >> [](auto& m) { m.add(Null); },
 
-        // single underscore variable
-        R"(_[[:alnum:]_]+\b)" >> [](auto& m) { m.add(Var); },
+        R"(as\b)" >> [](auto& m) { m.add(As); },
 
-        // Placeholder
-        "_" >> [](auto& m) { m.add(Placeholder); },
+        R"(set\(\))" >> [](auto& m) { m.add(EmptySet); },
 
-        // Brace.
-        R"((\{)[[:blank:]]*)" >> [](auto& m) { m.push(Brace, 1); },
+        "==" >> [](auto& m) { m.add(Equals); },
 
-        R"(\})" >>
-          [](auto& m) {
-            m.term({List});
-            if (m.in(Some))
-            {
-              m.pop(Some);
-              m.term();
-            }
-            if (m.in(With))
-            {
-              m.pop(With);
-              m.term();
-            }
-            m.pop(Brace);
-          },
+        "!=" >> [](auto& m) { m.add(NotEquals); },
 
-        // Square.
-        R"((\[)[[:blank:]]*)" >> [](auto& m) { m.push(Square, 1); },
+        "<=" >> [](auto& m) { m.add(LessThanOrEquals); },
 
-        R"(\])" >>
-          [](auto& m) {
-            m.term({List});
-            if (m.in(Some))
-            {
-              m.pop(Some);
-              m.term();
-            }
-            if (m.in(With))
-            {
-              m.pop(With);
-              m.term();
-            }
-            m.pop(Square);
-          },
+        "<" >> [](auto& m) { m.add(LessThan); },
 
-        // Parens.
-        R"((\()[[:blank:]]*)" >>
-          [](auto& m) {
-            m.push(Paren, 1);
-            // deal with empty parens
-            m.push(Group);
-          },
+        ">=" >> [](auto& m) { m.add(GreaterThanOrEquals); },
 
-        R"(\))" >>
-          [](auto& m) {
-            m.term({List});
-            if (m.in(Some))
-            {
-              m.pop(Some);
-              m.term();
-            }
-            if (m.in(With))
-            {
-              m.pop(With);
-              m.term();
-            }
-            m.pop(Paren);
-          },
+        ">" >> [](auto& m) { m.add(GreaterThan); },
 
-        // Float.
-        R"([[:digit:]]+\.[[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
-          [](auto& m) { m.add(Float); },
+        R"(\+)" >> [](auto& m) { m.add(Add); },
 
-        // String.
-        R"("(?:\\(?:["\\\/bfnrtx]|u[a-fA-F0-9]{4})|[^"\\\0-\x1F\x7F]+)*")" >>
-          [](auto& m) { m.add(JSONString); },
+        R"(\*)" >> [](auto& m) { m.add(Multiply); },
 
-        // Raw string.
-        R"(`[^`]*`)" >> [](auto& m) { m.add(RawString); },
+        "/" >> [](auto& m) { m.add(Divide); },
 
-        // Int.
-        R"([[:digit:]]+\b)" >> [](auto& m) { m.add(Int); },
+        "%" >> [](auto& m) { m.add(Modulo); },
 
-        // Float with exponent but no decimal.
-        R"([[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
-          [](auto& m) { m.add(Float); },
+        "&" >> [](auto& m) { m.add(And); },
 
-        // True.
-        "true\\b" >> [](auto& m) { m.add(True); },
+        R"(\|)" >> [](auto& m) { m.add(Or); },
 
-        // False.
-        "false\\b" >> [](auto& m) { m.add(False); },
+        ":=" >> [](auto& m) { m.add(Assign); },
 
-        // Null.
-        "null\\b" >> [](auto& m) { m.add(Null); },
+        "=" >> [](auto& m) { m.add(Unify); },
 
-        // Default.
-        "default\\b" >> [](auto& m) { m.add(Default); },
+        "," >> [](auto& m) { m.add(Comma); },
 
-        // Some.
-        "some\\b" >> [](auto& m) { m.push(Some); },
+        ":" >> [](auto& m) { m.add(Colon); },
 
-        // Else.
-        "else\\b" >> [](auto& m) { m.add(Else); },
+        R"(\.)" >> [](auto& m) { m.add(Dot); },
 
-        // Import
-        "import\\b" >> [](auto& m) { m.add(Import); },
+        R"(\[)" >> [](auto& m) { m.push(Square); },
 
-        // As
-        "as\\b" >>
+        "]" >>
           [](auto& m) {
             m.term();
-            m.add(As);
+            m.pop(Square);
           },
 
-        // With
-        "with\\b" >>
+        R"({(?:\r?\n)?)" >> [](auto& m) { m.push(Brace); },
+
+        "}" >>
           [](auto& m) {
             m.term();
-            if (m.in(With))
-            {
-              m.pop(With);
-            }
-            m.push(With);
+            m.pop(Brace);
           },
 
-        // Empty set.
-        R"(set\(\))" >> [](auto& m) { m.add(EmptySet); },
-
-        // Not
-        "not\\b" >> [](auto& m) { m.add(Not); },
-
-        // Dot.
-        R"(\.)" >> [](auto& m) { m.add(Dot); },
-
-        // Line comment.
-        "#[^\n]*" >> [](auto&) {},
+        R"(\()" >> [](auto& m) { m.push(Paren); },
 
-        // Package.
-        R"(package\b)" >> [](auto& m) { m.add(Package); },
+        R"(\))" >>
+          [](auto& m) {
+            m.term();
+            m.pop(Paren);
+          },
 
-        // Query.
-        R"(query\b)" >> [](auto& m) { m.add(Query); },
+        // RE for a JSON number (float):
+        // -? : optional minus sign
+        // (?:0|[1-9][0-9]*) : either a single 0, or 1-9 followed by any digits
+        // (?:\.[0-9]+)? : a single period followed by one or more digits
+        // (?:[eE][-+]?[0-9]+)? : optionally, an exponent. This can start with
+        // e or E, have +/-/nothing, and then 1 or more digits
+        R"(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)(?:[eE][-+]?[0-9]+)?)" >>
+          [](auto& m) { m.add(Float); },
 
-        // Identifier.
-        R"([_[:alpha:]][_[:alnum:]]*\b)" >> [](auto& m) { m.add(Var); },
+        // RE for an integer with an exponent (float):
+        // -? : optional minus sign
+        // (?:0|[1-9][0-9]*) : either a single 0, or 1-9 followed by any digits
+        // (?:[eE][-+]?[0-9]+)? : an exponent. This can start with
+        // e or E, have +/-/nothing, and then 1 or more digits
+        R"(-?(?:0|[1-9][0-9]*)[eE][-+]?[0-9]+)" >>
+          [](auto& m) { m.add(Float); },
 
-        // Add ('+' is a reserved RegEx character)
-        R"(\+)" >> [](auto& m) { m.add(Add); },
+        // RE for a JSON number (int):
+        // -? : optional minus sign
+        // (?:0|[1-9][0-9]*) : either a single 0, or 1-9 followed by any digits
+        R"(-?(?:0|[1-9][0-9]*))" >> [](auto& m) { m.add(Int); },
 
-        // Subtract
         "-" >> [](auto& m) { m.add(Subtract); },
 
-        // Multiply ('*' is a reserved RegEx character)
-        R"(\*)" >> [](auto& m) { m.add(Multiply); },
-
-        // Divide
-        "/" >> [](auto& m) { m.add(Divide); },
-
-        // Modulo
-        "%" >> [](auto& m) { m.add(Modulo); },
-
-        // Equals
-        "==" >> [](auto& m) { m.add(Equals); },
-
-        // NotEquals
-        "!=" >> [](auto& m) { m.add(NotEquals); },
+        // RE for a JSON string:
+        // " : a double quote followed by either:
+        // 1. [^"\\\x00-\x1F]+ : one or more characters that are not a double
+        // quote, backslash, or a control character from 00-1f
+        // 2. \\["\\\/bfnrt] : a backslash followed by one of the characters ",
+        // \, /, b, f, n, r, or t
+        // 3. \\u[[:xdigit:]]{4} : a backslash followed by u, followed by 4 hex
+        // digits zero or more times and then " : a double quote
+        R"("(?:[^"\\\x00-\x1F]+|\\["\\\/bfnrt]|\\u[[:xdigit:]]{4})*")" >>
+          [](auto& m) { m.add(JSONString); },
 
-        // LessThanOrEquals
-        "<=" >> [](auto& m) { m.add(LessThanOrEquals); },
+        R"(\`[^\`]*\`)" >> [](auto& m) { m.add(RawString); },
 
-        // Less than
-        "<" >> [](auto& m) { m.add(LessThan); },
+        R"((?:[[:alpha:]]|_)(?:[[:alnum:]]|_)*\b)" >>
+          [](auto& m) { m.add(Var); },
 
-        // GreaterThanOrEquals
-        ">=" >> [](auto& m) { m.add(GreaterThanOrEquals); },
+        ";" >> [](auto& m) { m.add(NewLine); },
 
-        // GreaterThan
-        ">" >> [](auto& m) { m.add(GreaterThan); },
+        R"(\r?\n)" >>
+          [newline_mode](auto& m) {
+            switch (*newline_mode)
+            {
+              case NewlineMode::Terminate:
+                m.term();
+                break;
+              case NewlineMode::Ignore:
+                break;
+              case NewlineMode::Token:
+                m.add(NewLine);
+                break;
+            }
+            *newline_mode = NewlineMode::Ignore;
+          },
 
-        // Unify
-        "=" >> [](auto& m) { m.add(Unify); },
+        R"(#[^\r\n]*\r?\n)" >> [](auto&) {},
 
+        R"(\s+)" >> [](auto&) {},
       });
 
-    p.done([](auto& m) { m.term({List, Some, With}); });
+    p.done([](auto& m) { m.term({Module, Query}); });
 
     p.gen({
       Int >> [](auto& rnd) { return std::to_string(rnd() % 100); },
diff --git a/src/passes/arithmetic.cc b/src/passes/arithmetic.cc
deleted file mode 100644
index cebe9202..00000000
--- a/src/passes/arithmetic.cc
+++ /dev/null
@@ -1,177 +0,0 @@
-#include "internal.hh"
-
-namespace rego
-{
-  const inline auto AllOps =
-    T(Add) / T(Subtract) / T(Multiply) / T(Divide) / T(Modulo) / T(Unify);
-
-  // Finds unary expressions by looking for patterns in which
-  // there is a stray Subtract token.
-  PassDef unary()
-  {
-    return {
-      "unary",
-      wf_pass_unary,
-      dir::topdown,
-      {
-        In(Expr) * (Start * T(Subtract) * ArithInfixArg[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return UnaryExpr << (ArithArg << _(Val));
-          },
-
-        In(Expr) * (AllOps[Op] * T(Subtract) * ArithInfixArg[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _(Op) << (UnaryExpr << (ArithArg << _(Val)));
-          },
-      }};
-  }
-
-  const inline auto Ops = T(Multiply) / T(Divide) / T(Modulo);
-
-  // Processes multiply, divide, and modulo operations into ArithInfix nodes.
-  // Also handles set intersection.
-  PassDef multiply_divide()
-  {
-    return {
-      "multiply_divide",
-      wf_pass_multiply_divide,
-      dir::topdown,
-      {
-        In(Expr) * (ArithInfixArg[Lhs] * Ops[Op] * ArithInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
-                              << (ArithArg << _(Rhs));
-          },
-
-        T(Expr) << (T(Expr)[Expr] * End) >>
-          [](Match& _) {
-            ACTION();
-            return _(Expr);
-          },
-
-        In(Expr) * (BinInfixArg[Lhs] * T(And) * BinInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return BinInfix << (BinArg << _(Lhs)) << And << (BinArg << _(Rhs));
-          },
-
-        // errors
-
-        In(Expr) * Ops[Op] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Op), "Invalid multiply/divide");
-          },
-
-        In(Expr) * T(And)[And] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(And), "Invalid and");
-          },
-      }};
-  }
-
-  // Transforms addition, subtraction, and unary negation into ArithInfix Nodes.
-  // Also, handles set union and difference.
-  PassDef add_subtract()
-  {
-    return {
-      "add_subtract",
-      wf_pass_add_subtract,
-      dir::topdown,
-      {
-        In(Expr) *
-            (ArithInfixArg[Lhs] * (T(Add) / T(Subtract))[Op] *
-             ArithInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
-                              << (ArithArg << _(Rhs));
-          },
-
-        In(Expr) * (ArithInfixArg * T(Subtract) * BinInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return err(
-              _(Rhs), "operand 2 must be number but got set", EvalTypeError);
-          },
-
-        In(Expr) * (BinInfixArg * T(Subtract) * ArithInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return err(
-              _(Rhs), "operand 2 must be set but got number", EvalTypeError);
-          },
-
-        In(Expr) * (T(Subtract) * ArithInfixArg[Val] * End) >>
-          [](Match& _) {
-            ACTION();
-            return UnaryExpr << (ArithArg << _(Val));
-          },
-
-        In(Expr) * (T(UnaryExpr) << (T(UnaryExpr) << Any[Val])) >>
-          [](Match& _) {
-            ACTION();
-            return _(Val);
-          },
-
-        In(ArithArg) *
-            (T(Expr)
-             << ((T(RefTerm) / T(NumTerm) / T(ArithInfix) / T(UnaryExpr) /
-                  T(ExprCall))[Val] *
-                 End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Val);
-          },
-
-        In(Expr) *
-            (BinInfixArg[Lhs] * (T(Subtract) / T(Or))[Op] * BinInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return BinInfix << (BinArg << _(Lhs)) << _(Op)
-                            << (BinArg << _(Rhs));
-          },
-
-        In(BinArg) *
-            (T(Expr)
-             << ((T(Ref) / T(RefTerm) / T(ExprCall) / T(Set) / T(SetCompr) /
-                  T(BinInfix))[Val] *
-                 End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Val);
-          },
-
-        // errors
-
-        In(Expr) * (T(Add) / T(Subtract))[Op] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Op), "Invalid add/subtract");
-          },
-
-        In(Expr) * T(Or)[Op] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Op), "Invalid set union");
-          },
-
-        T(ArithArg)[ArithArg] << (Any * Any) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ArithArg), "Argument can only have one element");
-          },
-
-        T(BinArg)[BinArg] << (Any * Any) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(BinArg), "Argument can only have one element");
-          },
-      }};
-  }
-
-}
\ No newline at end of file
diff --git a/src/passes/assign.cc b/src/passes/assign.cc
deleted file mode 100644
index 20d4a35c..00000000
--- a/src/passes/assign.cc
+++ /dev/null
@@ -1,239 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  using FunctionArity = std::shared_ptr<std::map<std::string, std::size_t>>;
-
-  std::string concat(const Node& refterm)
-  {
-    std::ostringstream path;
-    if (refterm->front()->type() == Var)
-    {
-      path << refterm->front()->location().view();
-    }
-    else
-    {
-      Node ref = refterm->front();
-      path << (ref / RefHead)->front()->location().view();
-      Node refargseq = ref / RefArgSeq;
-      for (auto& refarg : *refargseq)
-      {
-        if (refarg->type() == RefArgDot)
-        {
-          path << "." << refarg->front()->location().view();
-        }
-        else if (refarg->type() == RefArgBrack)
-        {
-          path << "[" << strip_quotes(to_json(refarg->front())) << "]";
-        }
-      }
-    }
-
-    return path.str();
-  }
-
-  bool needs_rewrite(const NodeRange& n, const FunctionArity& func_arity)
-  {
-    Node ruleref = n.first[0];
-    Node argseq = n.first[1];
-
-    if (!is_in(ruleref, {UnifyBody}))
-    {
-      return false;
-    }
-
-    std::string path = concat(ruleref);
-    if (!contains(func_arity, path))
-    {
-      return false;
-    }
-
-    bool result = func_arity->at(path) == argseq->size() - 1;
-    return result;
-  }
-}
-
-namespace rego
-{
-  const inline auto AssignInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
-    T(ArithInfix) / T(BinInfix) / T(Term) / T(BoolInfix) / T(ExprCall) /
-    T(Membership);
-
-  // Transforms unification expressions into AssignInfix nodes.
-  PassDef assign(const BuiltIns& builtins)
-  {
-    FunctionArity func_arity =
-      std::make_shared<std::map<std::string, std::size_t>>();
-
-    PassDef assign{
-      "assign",
-      wf_pass_assign,
-      {
-        In(Expr) *
-            (AssignInfixArg[Head] * In(UnifyBody)++ * T(Unify) *
-             AssignInfixArg[Lhs] * T(Unify) * AssignInfixArg[Rhs] * End) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << (Lift
-                           << UnifyBody
-                           << (Literal
-                               << (Expr
-                                   << (AssignInfix << (AssignArg << _(Lhs))
-                                                   << (AssignArg << _(Rhs))))))
-                       << (AssignInfix << (AssignArg << _(Head))
-                                       << (AssignArg << _(Lhs)->clone()));
-          },
-
-        In(Expr) * (AssignInfixArg[Lhs] * T(Unify) * AssignInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return AssignInfix << (AssignArg << _(Lhs))
-                               << (AssignArg << _(Rhs));
-          },
-
-        In(Expr) *
-            (AssignInfixArg[Lhs] * T(Unify) * (T(Set) / T(SetCompr))[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return AssignInfix << (AssignArg << _(Lhs))
-                               << (AssignArg << (Term << _(Rhs)));
-          },
-
-        In(Expr) *
-            ((T(Set) / T(SetCompr))[Lhs] * T(Unify) * AssignInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return AssignInfix << (AssignArg << (Term << _(Lhs)))
-                               << (AssignArg << _(Rhs));
-          },
-
-        In(Literal) *
-            (T(Expr)
-             << (T(ExprCall) << (T(RuleRef)[RuleRef] * T(ArgSeq)[ArgSeq])(
-                   [func_arity](auto& n) {
-                     // tests if this is a function call that has an extra
-                     // argument that should be unified with the result.
-                     return needs_rewrite(n, func_arity);
-                   }))) >>
-          [](Match& _) {
-            ACTION();
-            // A convention in the Golang implementation of Rego is
-            // that you can call any function with an additional argument
-            // that will be unified with its result. This is not documented
-            // anywhere but is expected behavior used in tests. This
-            // rewrite turns these back into the documented form which we
-            // expect.
-            Node ruleref = RuleRef << (Var ^ concat(_(RuleRef)));
-            Node argseq = _(ArgSeq);
-            Node var = argseq->pop_back();
-            return Expr
-              << (AssignInfix
-                  << (AssignArg << var->front())
-                  << (AssignArg << (ExprCall << ruleref << argseq)));
-          },
-
-        In(Literal) *
-            (T(Expr)
-             << (T(AssignInfix)
-                 << (T(AssignArg)[Lhs] *
-                     (T(AssignArg)
-                      << (T(ExprCall)
-                          << (T(RuleRef)[RuleRef] * T(ArgSeq)[ArgSeq] *
-                              In(UnifyBody)++)([func_arity](auto& n) {
-                               return needs_rewrite(n, func_arity);
-                             })))))) >>
-          [](Match& _) {
-            ACTION();
-            // see above comment.
-            Node ruleref = RuleRef << (Var ^ concat(_(RuleRef)));
-            Node argseq = _(ArgSeq);
-            Node var = argseq->pop_back();
-            return Seq << (Lift << UnifyBody
-                                << (Literal
-                                    << (Expr
-                                        << (AssignInfix
-                                            << (AssignArg << var->front())
-                                            << (AssignArg
-                                                << (ExprCall << ruleref
-                                                             << argseq))))))
-                       << (Expr
-                           << (AssignInfix
-                               << _(Lhs)
-                               << (AssignArg << var->front()->clone())));
-          },
-
-        In(Literal) * (T(Expr) << (AssignInfixArg[Arg] * End)) >>
-          [](Match& _) {
-            ACTION();
-            // bare expressions are treated as unification requirements
-            // in non-query rules.
-            std::string prefix = in_query(_(Arg)) ? "value" : "unify";
-            Location temp = _.fresh({prefix});
-            return Seq << (Lift << UnifyBody
-                                << (Local << (Var ^ temp) << Undefined))
-                       << (Expr
-                           << (AssignInfix
-                               << (AssignArg << (RefTerm << (Var ^ temp)))
-                               << (AssignArg << _(Arg))));
-          },
-
-        // errors
-        In(Expr) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Syntax error");
-          },
-
-        In(AssignArg) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Invalid assignment argument");
-          },
-
-        In(Expr) * T(Unify)[Unify] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Unify), "Invalid assignment");
-          },
-
-        In(RuleComp, RuleFunc, RuleSet, RuleObj) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Invalid rule value");
-          },
-
-        In(Expr) * (T(Set) / T(SetCompr))[Set] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Set), "Invalid set in expression");
-          },
-      }};
-
-    assign.pre(Rego, [func_arity, builtins](Node node) {
-      if (!func_arity->empty())
-      {
-        return 0;
-      }
-
-      Nodes rules;
-      node->get_symbols(rules, [](Node n) { return n->type() == RuleFunc; });
-      for (auto rule : rules)
-      {
-        std::string path = std::string((rule / Var)->location().view());
-        std::size_t arity = (rule / RuleArgs)->size();
-        func_arity->insert({path, arity});
-      }
-
-      for (auto& [loc, builtin] : builtins)
-      {
-        func_arity->insert({std::string(loc.view()), builtin->arity});
-      }
-
-      return 0;
-    });
-
-    return assign;
-  }
-}
\ No newline at end of file
diff --git a/src/passes/build_calls.cc b/src/passes/build_calls.cc
deleted file mode 100644
index 5b9b862c..00000000
--- a/src/passes/build_calls.cc
+++ /dev/null
@@ -1,77 +0,0 @@
-#include "internal.hh"
-
-namespace rego
-{
-  // Builds function calls into ExprCall nodes.
-  PassDef build_calls()
-  {
-    return {
-      "build_calls",
-      wf_pass_build_calls,
-      dir::topdown,
-      {
-        In(Group) * (T(Contains)[Contains] * T(Paren)[Paren]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << (Var ^ _(Contains)) << _(Paren);
-          },
-
-        In(Group) *
-            (RuleRefToken[Head] * RuleRefToken++[Tail] * T(Paren)[Paren]) >>
-          [](Match& _) {
-            ACTION();
-            Node argseq = NodeDef::create(ArgSeq);
-            Node paren = _(Paren);
-            if (paren->front()->type() == List)
-            {
-              for (const auto& arg : *paren->front())
-              {
-                argseq->push_back(arg);
-              }
-            }
-            else
-            {
-              // check if this was marked as a potential membership statement
-              // As calls have precedence, we may have to split up the group.
-              Node group = paren->front();
-              auto comma = group->begin();
-              for (; comma != group->end(); ++comma)
-              {
-                Node node = *comma;
-                if (node->type() == Comma)
-                {
-                  break;
-                }
-              }
-
-              if (comma != group->end())
-              {
-                // split the group into two groups
-                Node head = NodeDef::create(Group);
-                Node tail = NodeDef::create(Group);
-                head->insert(head->end(), group->begin(), comma);
-                tail->insert(tail->end(), comma + 1, group->end());
-                argseq << head << tail;
-              }
-              else
-              {
-                if (group->size() > 0)
-                {
-                  argseq << group;
-                }
-              }
-            }
-
-            return ExprCall << (RuleRef << _(Head) << _[Tail]) << argseq;
-          },
-
-        // errors
-
-        T(Group)[Group] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Syntax error: empty group");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/build_refs.cc b/src/passes/build_refs.cc
deleted file mode 100644
index b81b305e..00000000
--- a/src/passes/build_refs.cc
+++ /dev/null
@@ -1,95 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  const inline auto RefHeadToken = T(Var) / T(Array) / T(Object) / T(Set) /
-    T(ArrayCompr) / T(ObjectCompr) / T(SetCompr) / T(ExprCall);
-}
-
-namespace rego
-{
-  // Builts RefTerm and RuleRef nodes up from their tokens.
-  PassDef build_refs()
-  {
-    return {
-      "build_refs",
-      wf_pass_build_refs,
-      dir::topdown,
-      {
-        In(Group) * (RefHeadToken[RefHead] * T(Dot) * T(Var)[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << (RefHead << _(RefHead))
-                       << (RefArgSeq << (RefArgDot << _(Rhs)));
-          },
-
-        In(Group) * (RefHeadToken[RefHead] * T(Array)[Array]) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << (RefHead << _(RefHead))
-                       << (RefArgSeq << (RefArgBrack << *_[Array]));
-          },
-
-        In(RuleRef) * (T(Var)[RefHead] * T(Dot) * T(Var)[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << (RefHead << _(RefHead))
-                       << (RefArgSeq << (RefArgDot << _(Rhs)));
-          },
-
-        In(RuleRef) *
-            (T(Var)[RefHead] *
-             (T(Array) << ((T(Group)[Arg] << StringToken) * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << (RefHead << _(RefHead))
-                       << (RefArgSeq << (RefArgBrack << _(Arg)));
-          },
-
-        In(Group, RuleRef) *
-            ((T(Ref) << (T(RefHead)[RefHead] * T(RefArgSeq)[RefArgSeq])) *
-             T(Dot) * T(Var)[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << _(RefHead)
-                       << (RefArgSeq << *_[RefArgSeq] << (RefArgDot << _(Rhs)));
-          },
-
-        In(Group) *
-            ((T(Ref) << (T(RefHead)[RefHead] * T(RefArgSeq)[RefArgSeq])) *
-             T(Array)[Array]) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << _(RefHead)
-                       << (RefArgSeq << *_[RefArgSeq]
-                                     << (RefArgBrack << *_[Array]));
-          },
-
-        In(RuleRef) *
-            ((T(Ref) << (T(RefHead)[RefHead] * T(RefArgSeq)[RefArgSeq])) *
-             (T(Array) << ((T(Group)[Arg] << StringToken) * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << _(RefHead)
-                       << (RefArgSeq << *_[RefArgSeq]
-                                     << (RefArgBrack << _(Arg)));
-          },
-
-        // errors
-        In(RefArgBrack) * (T(Group) * T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return err(
-              _(Group), "Multi-dimensional array references are not supported");
-          },
-
-        In(RefArgSeq) * (T(RefArgBrack)[RefArgBrack] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(RefArgBrack), "Must provide an index argument");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/comparison.cc b/src/passes/comparison.cc
deleted file mode 100644
index 68dbf5c4..00000000
--- a/src/passes/comparison.cc
+++ /dev/null
@@ -1,114 +0,0 @@
-#include "internal.hh"
-
-namespace rego
-{
-  const inline auto NotArg = ArithInfixArg / T(BoolInfix);
-  const inline auto BoolInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
-    T(ArithInfix) / T(Term) / T(ExprCall) / T(BinInfix) / T(Set) / T(SetCompr);
-  const inline auto RefArgBrackArg =
-    T(Scalar) / T(Var) / T(Object) / T(Array) / T(Set);
-
-  // Transforms boolean comparison operations into BoolInfix nodes.
-  // Also generates LiteralNot nodes for not expressions.
-  PassDef comparison()
-  {
-    return {
-      "comparison",
-      wf_pass_comparison,
-      dir::topdown,
-      {
-        In(UnifyBody) * (T(Literal) << (T(Expr) << (T(Not) * Any++[Expr]))) >>
-          [](Match& _) {
-            ACTION();
-            return LiteralNot << (UnifyBody << (Literal << (Expr << _[Expr])));
-          },
-
-        In(Expr) * (T(Expr) << (BoolInfixArg[Arg] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Arg);
-          },
-
-        In(Expr) * (BoolInfixArg[Lhs] * BoolToken[Op] * BoolInfixArg[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            std::set<Token> set_types = {Set, SetCompr};
-            Node lhs = _(Lhs);
-            if (contains(set_types, lhs->type()))
-            {
-              lhs = Term << lhs;
-            }
-
-            Node rhs = _(Rhs);
-            if (contains(set_types, rhs->type()))
-            {
-              rhs = Term << rhs;
-            }
-
-            return BoolInfix << (BoolArg << lhs) << _(Op) << (BoolArg << rhs);
-          },
-
-        In(Expr) * (T(Set) / T(SetCompr))[Set] >>
-          [](Match& _) {
-            ACTION();
-            return Term << _(Set);
-          },
-
-        In(RefArgBrack) * (T(Term) << RefArgBrackArg[Arg]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Arg);
-          },
-
-        In(ArithArg, BoolArg) * (T(Expr) << ArithInfixArg[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Val);
-          },
-
-        // errors
-
-        T(Expr)[Expr] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Empty expression");
-          },
-
-        In(Expr) * BoolToken[Op] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Op), "Invalid comparison");
-          },
-
-        In(Expr) * T(Not)[Not] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Not), "Invalid not");
-          },
-
-        In(BoolArg) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Invalid boolean argument");
-          },
-
-        In(BoolArg) * (T(Set) / T(SetCompr))[Set] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Set), "Invalid boolean argument");
-          },
-
-        In(ArithArg) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Invalid argument");
-          },
-
-        In(BinArg) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Invalid set argument");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/expand_imports.cc b/src/passes/expand_imports.cc
deleted file mode 100644
index 7d111bbe..00000000
--- a/src/passes/expand_imports.cc
+++ /dev/null
@@ -1,48 +0,0 @@
-#include "internal.hh"
-
-namespace rego
-{
-  // Expands imports to their fully-qualified values.
-  PassDef expand_imports()
-  {
-    return {
-      "expand_imports",
-      wf_pass_expand_imports,
-      dir::topdown,
-      {
-        In(RefTerm, RuleRef) * T(Var)[Var] >> [](Match& _) -> Node {
-          // <import>
-          ACTION();
-          Nodes defs = _(Var)->lookup();
-          if (defs.empty())
-            return NoChange;
-          Node import = defs[0];
-          if (import->type() != Import)
-            return NoChange;
-          Node ref = import / Ref;
-          return ref->clone();
-        },
-
-        In(RefTerm, RuleRef) *
-            (T(Ref)
-             << ((T(RefHead) << T(Var)[Var]) * T(RefArgSeq)[RefArgSeq])) >>
-          [](Match& _) -> Node {
-          // <import>.dot <import>[brack]
-          ACTION();
-          Nodes defs = _(Var)->lookup();
-          if (defs.empty())
-            return NoChange;
-          Node import = defs[0];
-          if (import->type() != Import)
-            return NoChange;
-
-          Node ref = import / Ref;
-          Node refhead = (ref / RefHead)->clone();
-          Node refargseq = (ref / RefArgSeq)->clone();
-          refargseq->insert(
-            refargseq->end(), _(RefArgSeq)->begin(), _(RefArgSeq)->end());
-          return Ref << refhead << refargseq;
-        },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/if_else.cc b/src/passes/if_else.cc
deleted file mode 100644
index 5941e725..00000000
--- a/src/passes/if_else.cc
+++ /dev/null
@@ -1,83 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  const inline auto ExprTailToken = ExprToken / T(IsIn) / T(Assign) / T(Unify);
-}
-
-namespace rego
-{
-  // Handles the if keyword.
-  PassDef ifs()
-  {
-    return {
-      "ifs",
-      wf_pass_ifs,
-      dir::topdown,
-      {
-        In(Group) *
-            (T(If) * (ExprToken / T(SomeDecl))[Head] * ExprTailToken++[Tail]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << If << (UnifyBody << (Group << _(Head) << _[Tail]));
-          },
-
-        // errors
-      }};
-  }
-
-  // Creates Else nodes
-  PassDef elses()
-  {
-    return {
-      "elses",
-      wf_pass_elses,
-      dir::topdown,
-      {
-        In(Group) *
-            (T(Else) * (T(Assign) / T(Unify)) * ExprToken[Head] *
-             ExprTailToken++[Tail] * ~T(If) * T(UnifyBody)[UnifyBody]) >>
-          [](Match& _) {
-            ACTION();
-            return Else << (Group << _(Head) << _[Tail]) << _(UnifyBody);
-          },
-
-        In(Group) * (T(Else) * ~T(If) * T(UnifyBody)[UnifyBody]) >>
-          [](Match& _) {
-            ACTION();
-            return Else << (Group << (True ^ "true")) << _(UnifyBody);
-          },
-
-        In(Group) *
-            (T(Else) * (T(Assign) / T(Unify)) * T(Group)[Group] * ~T(If) *
-             T(UnifyBody)[UnifyBody]) >>
-          [](Match& _) {
-            ACTION();
-            return Else << _(Group) << _(UnifyBody);
-          },
-
-        In(Policy) * ((T(Group)[Lhs] << T(Var)) * (T(Group)[Rhs] << T(Else))) >>
-          [](Match& _) {
-            ACTION();
-            return Group << *_[Lhs] << *_[Rhs];
-          },
-
-        In(Group) *
-            (T(Else) * (T(Assign) / T(Unify)) * ExprToken[Head] *
-             ExprTailToken++[Tail]) >>
-          [](Match& _) {
-            ACTION();
-            return Else << (Group << _(Head) << _[Tail]) << Empty;
-          },
-
-        // errors
-        In(Group) * (T(Else)[Else] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Else), "Invalid else statement");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/imports.cc b/src/passes/imports.cc
deleted file mode 100644
index 2ce7b746..00000000
--- a/src/passes/imports.cc
+++ /dev/null
@@ -1,120 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  bool loc_equals(const Node& node, const std::string& value)
-  {
-    return node->location().view() == value;
-  }
-
-  const inline auto ImportToken = T(Var) / T(Dot) / T(Square);
-}
-
-namespace rego
-{
-  // Handles import statements, grouping them into an ImportSeq node. Also
-  // transforms With statements to give them additional structure.
-  PassDef imports()
-  {
-    return {
-      "imports",
-      wf_pass_imports,
-      dir::topdown,
-      {
-        In(With) *
-            (T(Group)[RuleRef] * (T(Group) << (T(As) * Any++[WithExpr]))) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << (RuleRef << _(RuleRef))
-                       << (WithExpr << (Group << _[WithExpr]));
-          },
-
-        In(ImportSeq) *
-            (T(Import)
-             << (T(Group)
-                 << (T(Var)(
-                       [](auto& n) { return loc_equals(*n.first, "future"); }) *
-                     T(Dot) * T(Var)([](auto& n) {
-                       return loc_equals(*n.first, "keywords");
-                     }) *
-                     T(Dot) * T(Var)[Keyword] * End))) >>
-          [](Match& _) {
-            ACTION();
-            if (_(Keyword)->location().view() == "every")
-            {
-              return Seq << (Keyword << (Var ^ "every"))
-                         << (Keyword << (Var ^ "in"));
-            }
-            return Keyword << _(Keyword);
-          },
-
-        In(ImportSeq) *
-            (T(Import)
-             << (T(Group)
-                 << (T(Var)(
-                       [](auto& n) { return loc_equals(*n.first, "future"); }) *
-                     T(Dot) * T(Var)([](auto& n) {
-                       return loc_equals(*n.first, "keywords");
-                     }) *
-                     End))) >>
-          [](Match&) {
-            ACTION();
-            Node seq = NodeDef::create(Seq);
-            for (auto& keyword : Keywords)
-            {
-              seq->push_back(Keyword << (Var ^ keyword));
-            }
-            return seq;
-          },
-
-        In(ImportSeq) *
-            (T(Import)
-             << ((T(Group)
-                  << (ImportToken[Head] * ImportToken++[Tail] * T(As) *
-                      T(Var)[Var] * End)) *
-                 End)) >>
-          [](Match& _) {
-            ACTION();
-            return Import << (ImportRef << (Group << _(Head) << _[Tail])) << As
-                          << _(Var);
-          },
-
-        In(ImportSeq) *
-            (T(Import)
-             << ((T(Group) << (ImportToken[Head] * ImportToken++[Tail] * End)) *
-                 End)) >>
-          [](Match& _) {
-            ACTION();
-            return Import << (ImportRef << (Group << _(Head) << _[Tail])) << As
-                          << Undefined;
-          },
-
-        // errors
-        In(Import) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid import");
-          },
-
-        In(With) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid with reference");
-          },
-
-        In(Group) * T(As)[As] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(As), "Invalid as statement");
-          },
-
-        In(WithExpr) * (T(Group)[Group] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid with expression");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/input_data.cc b/src/passes/input_data.cc
deleted file mode 100644
index 921bc4dd..00000000
--- a/src/passes/input_data.cc
+++ /dev/null
@@ -1,58 +0,0 @@
-#include "internal.hh"
-
-namespace rego
-{
-  // Separates out Input and Data nodes from the rest of the AST.
-  PassDef input_data()
-  {
-    return {
-      "input_data",
-      wf_pass_input_data,
-      dir::topdown,
-      {
-        In(Input) * (T(File) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Group);
-          },
-
-        In(Rego) * (T(Input) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return Input << (Key ^ "input") << _(Group);
-          },
-
-        In(DataSeq) * (T(File) << (T(Group) << T(Brace)[Brace])) >>
-          [](Match& _) {
-            ACTION();
-            return Data << _(Brace);
-          },
-
-        In(Rego) * (T(Input) << T(Undefined)) >>
-          [](Match&) {
-            ACTION();
-            return Input << (Key ^ "input") << Undefined;
-          },
-
-        // errors
-        In(Input) * T(File)[File] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(File), "Invalid JSON file for input");
-          },
-
-        In(DataSeq) * T(File)[File] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(File), "Invalid JSON file for data");
-          },
-
-        In(Rego) * (T(Input)[Input] << T(Error)) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Input), "Invalid input");
-          },
-      }};
-  }
-
-}
\ No newline at end of file
diff --git a/src/passes/keywords.cc b/src/passes/keywords.cc
deleted file mode 100644
index 80f71ced..00000000
--- a/src/passes/keywords.cc
+++ /dev/null
@@ -1,65 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  bool maybe_keyword(const Node& node)
-  {
-    if (is_in(node, {Package}))
-    {
-      return false;
-    }
-    std::string name = std::string(node->location().view());
-    return contains(Keywords, name);
-  }
-}
-
-namespace rego
-{
-  // Replaces keywords with their specific tokens, conditional on whether those
-  // keywords have been imported.
-  PassDef keywords()
-  {
-    return {
-      "keywords",
-      wf_pass_keywords,
-      dir::topdown,
-      {
-        In(Group) * T(Var)[Var]([](auto& n) {
-          Node var = *n.first;
-          if (maybe_keyword(var))
-          {
-            Nodes defs = var->lookup();
-            return defs.size() >= 1 && defs[0]->type() == Keyword;
-          }
-          return false;
-        }) >>
-          [](Match& _) {
-            ACTION();
-            auto name = _(Var)->location().view();
-            if (name == "if")
-            {
-              return If ^ _(Var)->location();
-            }
-
-            if (name == "in")
-            {
-              return IsIn ^ _(Var)->location();
-            }
-
-            if (name == "contains")
-            {
-              return Contains ^ _(Var)->location();
-            }
-
-            if (name == "every")
-            {
-              return Every ^ _(Var)->location();
-            }
-
-            return err(_(Var), "unsupported keyword");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/lists.cc b/src/passes/lists.cc
deleted file mode 100644
index 81b518ea..00000000
--- a/src/passes/lists.cc
+++ /dev/null
@@ -1,500 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  bool contains_or(const Node& group)
-  {
-    for (Node child : *group)
-    {
-      if (child->type() == Or)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool contains(const Node& node, const std::set<Token>& tokens)
-  {
-    if (node->type() == Error)
-    {
-      return false;
-    }
-
-    if (rego::contains(tokens, node->type()))
-    {
-      return true;
-    }
-
-    for (Node child : *node)
-    {
-      if (contains(child, tokens))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool is_top_level(const Node& node)
-  {
-    return node->parent()->parent()->type() == Policy;
-  }
-}
-
-namespace rego
-{
-  // Processes lists into more semantically meaningful structures.
-  PassDef lists()
-  {
-    return {
-      "lists",
-      wf_pass_lists,
-      dir::topdown,
-      {
-        In(Data) * (T(Brace) << T(List)[List]) >>
-          [](Match& _) {
-            ACTION();
-            return ObjectItemSeq << *_[List];
-          },
-
-        In(Data) * (T(Brace) << End) >>
-          [](Match&) -> Node { return ObjectItemSeq; },
-
-        In(Group) * (T(Var)[Var] * T(If) * T(Brace)[Brace]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _(Var) << If << (UnifyBody << *_[Brace]);
-          },
-
-        In(Group) *
-            (Start *
-             T(Var)[Var]([](auto& n) { return is_top_level(*n.first); }) *
-             T(Brace)[Brace]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _(Var) << (UnifyBody << *_[Brace]);
-          },
-
-        In(Group) *
-            (Start *
-             T(Var)[Var]([](auto& n) { return is_top_level(*n.first); }) *
-             (T(Assign) / T(Unify))[Assign] * ExprToken[Head] *
-             ExprToken++[Tail] * T(Brace)[Brace]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _(Var) << _(Assign) << _(Head) << _[Tail]
-                       << (UnifyBody << *_[Brace]);
-          },
-
-        In(Group) *
-            ((T(Square) / T(Brace))[Compr] << (T(Group)[Group](
-               [](auto& n) { return contains_or(*n.first); }))) >>
-          [](Match& _) {
-            ACTION();
-            // array/set comprehension
-            Node group = NodeDef::create(Group);
-            Node item = NodeDef::create(Undefined);
-            for (auto& node : *_(Group))
-            {
-              if (node->type() == Or)
-              {
-                item = group;
-                group = NodeDef::create(Group);
-              }
-              else
-              {
-                group->push_back(node);
-              }
-            }
-
-            Node unifybody = NodeDef::create(UnifyBody);
-            if (group->size() > 0)
-            {
-              unifybody->push_back(group);
-            }
-            for (auto it = _(Compr)->begin() + 1; it != _(Compr)->end(); ++it)
-            {
-              unifybody->push_back(*it);
-            }
-
-            if (_(Compr)->type() == Brace)
-            {
-              return SetCompr << item << unifybody;
-            }
-
-            return ArrayCompr << item << unifybody;
-          },
-
-        In(Group) *
-            (T(Brace)[Compr]
-             << ((T(List) << ((T(ObjectItem)
-                               << (T(Group)[Key] * T(Group)[Val]([](auto& n) {
-                                     return contains_or(*n.first);
-                                   })))) *
-                    End) *
-                 T(Group)++[UnifyBody] * End)) >>
-          [](Match& _) {
-            ACTION();
-            // object comprehension
-            Node group = NodeDef::create(Group);
-            Node val;
-            for (auto& node : *_(Val))
-            {
-              if (node->type() == Or)
-              {
-                val = group;
-                group = NodeDef::create(Group);
-              }
-              else
-              {
-                group->push_back(node);
-              }
-            }
-
-            Node unifybody = NodeDef::create(UnifyBody);
-            if (group->size() > 0)
-            {
-              unifybody->push_back(group);
-            }
-
-            unifybody << _[UnifyBody];
-
-            return ObjectCompr << _(Key) << val << unifybody;
-          },
-
-        In(Group) * (T(Brace) << End) >> [](Match&) -> Node { return Object; },
-
-        In(Group) *
-            (T(Brace)
-             << (T(List)
-                 << (T(ObjectItem)[Head] * T(ObjectItem)++[Tail] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Object << _(Head) << _[Tail];
-          },
-
-        In(Group) * (T(Brace) << (T(List)[List] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Set << *_[List];
-          },
-
-        In(Group) *
-            T(Brace)[Brace]([](auto& n) { return is_top_level(*n.first); }) >>
-          [](Match& _) {
-            ACTION();
-            return UnifyBody << *_[Brace];
-          },
-
-        In(Group) * (T(Square) << (T(List)[List] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Array << *_[List];
-          },
-
-        In(Group, ImportRef) * (T(Square) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return Array << _(Group);
-          },
-
-        T(List)
-            << ((T(Group) << (T(Every) * T(Var)[Key])) *
-                (T(Group) << (T(Var)[Val] * Any[Head] * Any++[Tail]))) >>
-          [](Match& _) {
-            ACTION();
-            return Group
-              << (ExprEvery
-                  << (VarSeq << (Group << _(Key)) << (Group << _(Val)))
-                  << (EverySeq << (Group << _(Head) << _[Tail])));
-          },
-
-        In(Group) * (T(Every) * T(Var)[Var] * Any++[Tail]) >>
-          [](Match& _) {
-            ACTION();
-            return ExprEvery << (VarSeq << (Group << _(Var)))
-                             << (EverySeq << (Group << _[Tail]));
-          },
-
-        In(Group) * T(UnifyBody)[UnifyBody] * In(ExprEvery)++ >>
-          [](Match& _) {
-            ACTION();
-            return Lift << ExprEvery << _(UnifyBody);
-          },
-
-        In(Group) * (T(Brace) << (T(Group)[Head] * T(Group)++[Tail] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return UnifyBody << _(Head) << _[Tail];
-          },
-
-        T(List)
-            << ((T(Group)
-                 << (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
-                     MembershipToken++[Idx] * End)) *
-                (T(Group) << (MembershipToken++[Item] * T(IsIn) *
-                              MembershipToken++[ItemSeq] * T(IsIn) *
-                              MembershipToken++[ItemSeq1]) *
-                   End)) >>
-          [](Match& _) {
-            ACTION();
-            return Group << _[Lhs] << _(Assign)
-                         << (Paren
-                             << (Group << _[Idx] << Comma << _[Item] << IsIn
-                                       << _[ItemSeq]))
-                         << IsIn << _[ItemSeq1];
-          },
-
-        T(List)
-            << ((T(Group) << (MembershipToken++[Idx] * End)) *
-                (T(Group) << (MembershipToken++[Item] * T(IsIn) *
-                              MembershipToken++[ItemSeq] * T(IsIn) *
-                              MembershipToken++[ItemSeq1]) *
-                   End)) >>
-          [](Match& _) {
-            ACTION();
-            return Group << (Paren
-                             << (Group << _[Idx] << Comma << _[Item] << IsIn
-                                       << _[ItemSeq]))
-                         << IsIn << _[ItemSeq1];
-          },
-
-        T(List)
-            << ((T(Group)
-                 << (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
-                     MembershipToken++[Idx] * End)) *
-                (T(Group)
-                 << (MembershipToken++[Item] * T(IsIn)[IsIn] *
-                     MembershipToken++[ItemSeq] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Group << _[Lhs] << _(Assign) << _[Idx] << Comma << _[Item]
-                         << IsIn << _[ItemSeq];
-          },
-
-        T(List)
-            << ((T(Group) << (MembershipToken++[Idx] * End)) *
-                (T(Group)
-                 << (MembershipToken++[Item] * T(IsIn)[IsIn] *
-                     MembershipToken++[ItemSeq] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Group << _[Idx] << Comma << _[Item] << IsIn << _[ItemSeq];
-          },
-
-        In(UnifyBody) * T(List)[List] >>
-          [](Match& _) {
-            ACTION();
-            return Seq << *_[List];
-          },
-
-        In(Group) * (T(Some) << (T(List)[List] * End)) >>
-          [](Match& _) {
-            ACTION();
-            Node list = _(List);
-            if (list->size() == 0)
-            {
-              return err(list, "some must contain at least one variable");
-            }
-
-            Node maybe_contains_in;
-            if (list->size() == 1)
-            {
-              maybe_contains_in = list->front();
-            }
-            else
-            {
-              maybe_contains_in = list->at(1);
-            }
-
-            auto in_pos = maybe_contains_in->begin();
-            for (; in_pos != maybe_contains_in->end(); ++in_pos)
-            {
-              Node n = *in_pos;
-              if (n->type() == IsIn)
-              {
-                break;
-              }
-            }
-
-            if (in_pos != maybe_contains_in->end())
-            {
-              Node item = NodeDef::create(Group);
-              item->insert(item->end(), maybe_contains_in->begin(), in_pos);
-              Node itemseq = NodeDef::create(Group);
-              itemseq->insert(itemseq->end(), in_pos, maybe_contains_in->end());
-              if (list->size() > 2)
-              {
-                // some i, x in c with ...
-                Node seq = Seq
-                  << (SomeDecl << (VarSeq << list->front() << item) << itemseq);
-                for (auto it = list->begin() + 2; it != list->end(); ++it)
-                {
-                  Node group = *it;
-                  seq->insert(seq->end(), group->begin(), group->end());
-                }
-                return seq;
-              }
-              else if (list->size() == 2)
-              {
-                // some i, x in c
-                return SomeDecl << (VarSeq << list->front() << item) << itemseq;
-              }
-              else
-              {
-                // some x in c
-                return SomeDecl << (VarSeq << item) << itemseq;
-              }
-            }
-
-            // some x, y, z, ...
-            return SomeDecl << (VarSeq << *_[List]) << (Group << Undefined);
-          },
-
-        In(Group) * (T(Some) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            Node group = _(Group);
-
-            auto in_pos = group->begin();
-            for (; in_pos != group->end(); ++in_pos)
-            {
-              Node n = *in_pos;
-              if (n->type() == IsIn)
-              {
-                break;
-              }
-            }
-
-            if (in_pos != group->end())
-            {
-              Node item = NodeDef::create(Group);
-              item->insert(item->end(), group->begin(), in_pos);
-              Node itemseq = NodeDef::create(Group);
-              itemseq->insert(itemseq->end(), in_pos, group->end());
-              // some x in c
-              return SomeDecl << (VarSeq << item) << itemseq;
-            }
-
-            // some x
-            return SomeDecl << (VarSeq << group) << (Group << Undefined);
-          },
-
-        In(Group) * T(EmptySet)[EmptySet] >>
-          ([](Match& _) -> Node { return Set ^ _(EmptySet); }),
-
-        In(Group) * (T(Square) << End) >> [](Match&) -> Node { return Array; },
-
-        // errors
-
-        In(Data) * T(Brace)[Brace] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Brace), "Invalid data body");
-          },
-
-        In(Group) * T(Brace)[Brace] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Brace), "Invalid object");
-          },
-
-        In(Group) * T(Square)[Square] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Square), "Invalid array");
-          },
-
-        In(Group) * T(Some)[Some] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Some), "Invalid some declaration");
-          },
-
-        In(SomeDecl) * T(ObjectItem)[ObjectItem] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ObjectItem), "Invalid object item in some-decl");
-          },
-
-        In(VarSeq) * T(ObjectItem)[ObjectItem] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ObjectItem), "Invalid object item in var-seq");
-          },
-
-        In(Some) * (T(List) * Any[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Rhs), "Invalid second node in some declaration");
-          },
-
-        In(ObjectItemSeq, Object) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid object key/value");
-          },
-
-        In(Array, Set, List) * T(ObjectItem)[ObjectItem] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ObjectItem), "Invalid item");
-          },
-
-        In(Group) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Syntax error");
-          },
-
-        In(Group) * T(Every)[Every] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Every), "Invalid every");
-          },
-
-        In(ImportRef) * T(Square)[Square] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Square), "Invalid reference");
-          },
-
-        In(UnifyBody) * T(List)[List] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(List), "Invalid block");
-          },
-
-        In(EverySeq) * (T(Group)[Group] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid every sequence");
-          },
-
-        In(ExprEvery) * (T(VarSeq) * T(EverySeq)[EverySeq]([](auto& n) {
-                           return !::contains(*n.first, {UnifyBody, Brace});
-                         })) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(EverySeq), "Missing body of every");
-          },
-
-        In(ArrayCompr, SetCompr, ObjectCompr) * (T(Group)[Group] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid comprehension");
-          },
-
-        In(UnifyBody) * T(ObjectItem)[ObjectItem] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ObjectItem), "Invalid key/value pair");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/membership.cc b/src/passes/membership.cc
deleted file mode 100644
index d07ea3c4..00000000
--- a/src/passes/membership.cc
+++ /dev/null
@@ -1,104 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  const auto inline GroundToken =
-    ScalarToken / T(JSONString) / T(RawString) / T(ExprCall);
-}
-
-namespace rego
-{
-  // Assembles tokens into Membership nodes.
-  PassDef membership()
-  {
-    return {
-      "membership",
-      wf_pass_membership,
-      dir::topdown,
-      {
-        In(Group) * (T(IsIn) * (T(UnifyBody) << (T(Group)[Group] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << IsIn << (Set << _(Group));
-          },
-
-        In(Group) *
-            (MembershipToken++[Item] * T(IsIn) * MembershipToken++[ItemSeq] *
-             T(IsIn) * MembershipToken++[ItemSeq1]) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << (Paren << (Group << _[Item] << IsIn << _[ItemSeq]))
-                       << IsIn << _[ItemSeq1];
-          },
-
-        In(Group) *
-            (MembershipToken++[Idx] * T(Comma) * MembershipToken++[Item] *
-             T(IsIn) * MembershipToken++[ItemSeq] * End) >>
-          [](Match& _) {
-            ACTION();
-            return Membership << (Group << _[Idx]) << (Group << _[Item])
-                              << (Group << _[ItemSeq]);
-          },
-
-        In(Group) *
-            (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
-             MembershipToken++[Idx] * T(Comma) * MembershipToken++[Item] *
-             T(IsIn) * MembershipToken++[ItemSeq] * End) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _[Lhs] << _(Assign)
-                       << (Membership << (Group << _[Idx]) << (Group << _[Item])
-                                      << (Group << _[ItemSeq]));
-          },
-
-        In(Group) *
-            (MembershipToken[Head] * MembershipToken++[Tail] * T(IsIn)[IsIn] *
-             MembershipToken[Head1] * MembershipToken++[Tail1] *
-             End)[Membership] >>
-          [](Match& _) {
-            ACTION();
-            return Membership << Undefined << (Group << _(Head) << _[Tail])
-                              << (Group << _(Head1) << _[Tail1]);
-          },
-
-        In(Group) *
-            (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
-             MembershipToken[Head] * MembershipToken++[Tail] * T(IsIn)[IsIn] *
-             MembershipToken[Head1] * MembershipToken++[Tail1] *
-             End)[Membership] >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _[Lhs] << _(Assign)
-                       << (Membership << Undefined
-                                      << (Group << _(Head) << _[Tail])
-                                      << (Group << _(Head1) << _[Tail1]));
-          },
-
-        In(Group) *
-            (T(SomeDecl)
-             << ((T(VarSeq)
-                  << ((T(Group) << (GroundToken[Item] * End)) * End)) *
-                 (T(Group) << (T(IsIn) * MembershipToken++[ItemSeq] * End)))) >>
-          [](Match& _) {
-            ACTION();
-            return Membership << Undefined << (Group << _(Item))
-                              << (Group << _[ItemSeq]);
-          },
-
-        // errors
-        In(Group) * T(Comma)[Comma] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Comma), "invalid membership statement");
-          },
-
-        T(Group)[Group] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Syntax error: empty group.");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/modules.cc b/src/passes/modules.cc
deleted file mode 100644
index ee1d8e5e..00000000
--- a/src/passes/modules.cc
+++ /dev/null
@@ -1,114 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  const inline auto KeyToken = T(Var) / T(Square) / T(Dot) / ScalarToken /
-    T(RawString) / T(JSONString) / T(Brace);
-  const inline auto RefToken =
-    T(Var) / T(Dot) / T(Square) / T(RawString) / T(JSONString);
-  const inline auto ImportToken = T(Var) / T(Dot) / T(Square) / T(As);
-}
-
-namespace rego
-{
-  // Separates out Modules from the rest of the AST.
-  PassDef modules()
-  {
-    return {
-      "modules",
-      wf_pass_modules,
-      dir::topdown,
-      {
-        In(ModuleSeq) *
-            (T(File)
-             << ((T(Group) << (T(Package) * RefToken++[Package])) *
-                 T(Group)++[Policy])) >>
-          [](Match& _) {
-            ACTION();
-            return Module << (Package << (Group << _[Package])) << ImportSeq
-                          << (Policy << _[Policy]);
-          },
-
-        In(List, Compr) *
-            (T(Group) << (KeyToken++[Key] * T(Colon) * Any++[Val])) >>
-          [](Match& _) {
-            ACTION();
-            return ObjectItem << (Group << _[Key]) << (Group << _[Val]);
-          },
-
-        In(Brace) * (T(Group) << (KeyToken++[Key] * T(Colon) * Any++[Val])) >>
-          [](Match& _) {
-            ACTION();
-            return List
-              << (ObjectItem << (Group << _[Key]) << (Group << _[Val]));
-          },
-
-        In(Policy) *
-            ((T(Group) << (T(Import) * ImportToken++[Import] * End)) *
-             (T(Group) << (T(As) * T(Var)[Var] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return Lift << Module
-                        << (Import << (Group << _[Import] << As << _(Var)));
-          },
-
-        In(Policy) * (T(Group) << (T(Import) * ImportToken++[Import] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Lift << Module << (Import << (Group << _[Import]));
-          },
-
-        In(Module) * (T(ImportSeq)[ImportSeq] * T(Import)[Import]) >>
-          [](Match& _) {
-            ACTION();
-            return ImportSeq << *_[ImportSeq] << _(Import);
-          },
-
-        T(Placeholder) >>
-          [](Match& _) {
-            ACTION();
-            Location temp = _.fresh({"_"});
-            return (Var ^ temp);
-          },
-
-        // errors
-        In(ModuleSeq) * T(File)[File] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(File), "Invalid module file");
-          },
-
-        In(Group) * T(Package)[Package] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Package), "Invalid package declaration.");
-          },
-
-        In(Group) * T(Colon)[Colon] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Colon), "Invalid character");
-          },
-
-        In(ObjectItem) * (T(Group)[Group] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid key/value pair");
-          },
-
-        In(Group) * (T(Import)[Import] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Import), "Invalid import");
-          },
-
-        In(Import, Package) * (T(Group)[Group] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid import");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/rules.cc b/src/passes/rules.cc
deleted file mode 100644
index 26a611bf..00000000
--- a/src/passes/rules.cc
+++ /dev/null
@@ -1,366 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  Node to_elseseq(const NodeRange& elses, const Node& init_value)
-  {
-    Node value = init_value;
-    Node elseseq = NodeDef::create(ElseSeq);
-    for (auto it = elses.first; it < elses.second; ++it)
-    {
-      Node n = *it;
-      if (n->type() == UnifyBody)
-      {
-        n = Else << init_value->clone() << n;
-      }
-      else if (n->type() == Else)
-      {
-        Node else_value = n / Val;
-        if (else_value->type() == Undefined)
-        {
-          n->replace(else_value, value->clone());
-        }
-        else
-        {
-          value = else_value;
-        }
-      }
-      elseseq->push_back(n);
-    }
-    return elseseq;
-  }
-}
-
-namespace rego
-{
-  // Assembles tokens into a early form of what will become rule
-  // definitions.
-  PassDef rules()
-  {
-    return {
-      "rules",
-      wf_pass_rules,
-      dir::topdown,
-      {
-        In(Policy) *
-            (T(Group)
-             << (T(Var)[Id] * (T(Array) << (T(Group)[Item] * End)) *
-                 T(UnifyBody)[UnifyBody])) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead << (RuleRef << _(Id))
-                                     << (RuleHeadSet << _(Item)))
-                        << _(UnifyBody) << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (T(Var)[Id] * (T(Array) << (T(Group)[Item] * End)) * T(If) *
-                 T(UnifyBody)[UnifyBody])) >>
-          [](Match& _) {
-            ACTION();
-            // this has the form of a set rule but the reference implementation
-            // interprets it as an object rule instead.
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(Id))
-                            << (RuleHeadObj << _(Item)
-                                            << (AssignOperator << Assign)
-                                            << (Group << (True ^ "true"))))
-                        << _(UnifyBody) << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (T(Var)[Id] * (T(Array) << (T(Group)[Item] * End)) * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead << (RuleRef << _(Id))
-                                     << (RuleHeadSet << _(Item)))
-                        << Empty << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (T(Var)[Id] * (T(Array) << (T(Group)[Key] * End)) *
-                 (T(Assign) / T(Unify)) * ExprToken[Head] * ExprToken++[Tail] *
-                 ~T(If) * T(UnifyBody)[UnifyBody])) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead << (RuleRef << _(Id))
-                                     << (RuleHeadObj
-                                         << _(Key) << (AssignOperator << Assign)
-                                         << (Group << _(Head) << _[Tail])))
-                        << _(UnifyBody) << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (T(Var)[Id] * (T(Array) << (T(Group)[Key] * End)) *
-                 (T(Assign) / T(Unify)) * ExprToken[Head] *
-                 ExprToken++[Tail])) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead << (RuleRef << _(Id))
-                                     << (RuleHeadObj
-                                         << _(Key) << (AssignOperator << Assign)
-                                         << (Group << _(Head) << _[Tail])))
-                        << Empty << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] * ~T(If) *
-                 T(UnifyBody)[UnifyBody] * (T(Else) / T(UnifyBody))++[Else])) >>
-          [](Match& _) {
-            ACTION();
-            Node value = Group << True;
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadComp << (AssignOperator << Assign)
-                                             << value))
-                        << _(UnifyBody) << to_elseseq(_[Else], value);
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] *
-                 (T(Assign) / T(Unify)) * ExprToken[Head] * ExprToken++[Tail] *
-                 ~T(If) * T(UnifyBody)[UnifyBody] *
-                 (T(Else) / T(UnifyBody))++[Else])) >>
-          [](Match& _) {
-            ACTION();
-            Node value = (Group << _(Head) << _[Tail]);
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadComp << (AssignOperator << Assign)
-                                             << value))
-                        << _(UnifyBody) << to_elseseq(_[Else], value);
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (~T(Default)[Default] * RuleRefToken[RefHead] *
-                 RuleRefToken++[RefArgSeq] * (T(Assign) / T(Unify)) * ~T(If) *
-                 ExprToken[Head] * ExprToken++[Tail])) >>
-          [](Match& _) {
-            ACTION();
-            Node is_default = _(Default) != nullptr ? True : False;
-            return Rule << is_default
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadComp << (AssignOperator << Assign)
-                                             << (Group << _(Head) << _[Tail])))
-                        << Empty << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (~T(Default)[Default] * RuleRefToken[RefHead] *
-                 RuleRefToken++[RefArgSeq] * (T(Assign) / T(Unify)) *
-                 T(UnifyBody)[UnifyBody])) >>
-          [](Match& _) {
-            ACTION();
-            // a misclassified single-element set
-            Node is_default = _(Default) != nullptr ? True : False;
-            return Rule << is_default
-                        << (RuleHead << (RuleRef << _(RefHead) << _[RefArgSeq])
-                                     << (RuleHeadComp
-                                         << (AssignOperator << Assign)
-                                         << (Group << (Set << *_[UnifyBody]))))
-                        << Empty << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] *
-                 T(Paren)[Paren] * ~T(If) * T(UnifyBody)[UnifyBody] *
-                 (T(Else) / T(UnifyBody))++[Else])) >>
-          [](Match& _) {
-            ACTION();
-            Node args = NodeDef::create(RuleArgs);
-            Node paren = _(Paren);
-            if (paren->front()->type() == List)
-            {
-              for (const auto& arg : *paren->front())
-              {
-                args->push_back(arg);
-              }
-            }
-            else
-            {
-              Node arg = paren->front();
-              if (arg->size() > 0)
-              {
-                args->push_back(arg);
-              }
-            }
-
-            Node value = Group << True;
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadFunc
-                                << args << (AssignOperator << Assign) << value))
-                        << _(UnifyBody) << to_elseseq(_[Else], value);
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] *
-                 T(Paren)[Paren] * (T(Assign) / T(Unify)) * ExprToken[Head] *
-                 ExprToken++[Tail] * ~T(If) * T(UnifyBody)[UnifyBody] *
-                 (T(Else) / T(UnifyBody))++[Else])) >>
-          [](Match& _) {
-            ACTION();
-            Node args = NodeDef::create(RuleArgs);
-            Node paren = _(Paren);
-            if (paren->front()->type() == List)
-            {
-              for (const auto& arg : *paren->front())
-              {
-                args->push_back(arg);
-              }
-            }
-            else
-            {
-              Node arg = paren->front();
-              if (arg->size() > 0)
-              {
-                args->push_back(arg);
-              }
-            }
-
-            Node value = Group << _(Head) << _[Tail];
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadFunc
-                                << args << (AssignOperator << Assign) << value))
-                        << _(UnifyBody) << to_elseseq(_[Else], value);
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (~T(Default)[Default] * RuleRefToken[RefHead] *
-                 RuleRefToken++[RefArgSeq] * T(Paren)[Paren] *
-                 (T(Assign) / T(Unify)) * ExprToken[Head] * ExprToken++[Tail] *
-                 (T(Else) / T(UnifyBody))++[Else])) >>
-          [](Match& _) {
-            ACTION();
-            Node args = NodeDef::create(RuleArgs);
-            Node paren = _(Paren);
-            if (paren->front()->type() == List)
-            {
-              for (const auto& arg : *paren->front())
-              {
-                args->push_back(arg);
-              }
-            }
-            else
-            {
-              Node arg = paren->front();
-              if (arg->size() > 0)
-              {
-                args->push_back(arg);
-              }
-            }
-
-            Node is_default = _(Default) != nullptr ? True : False;
-            Node value = Group << _(Head) << _[Tail];
-            return Rule << is_default
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadFunc
-                                << args << (AssignOperator << Assign) << value))
-                        << Empty << to_elseseq(_[Else], value);
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] *
-                 T(Contains) * ExprToken[Head] * ExprToken++[Tail] * ~T(If) *
-                 T(UnifyBody)[UnifyBody])) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadSet << (Group << _(Head) << _[Tail])))
-                        << _(UnifyBody) << ElseSeq;
-          },
-
-        In(Policy) *
-            (T(Group)
-             << (RuleRefToken[RefHead] * RuleRefToken++[RefArgSeq] *
-                 T(Contains) * ExprToken[Head] * ExprToken++[Tail])) >>
-          [](Match& _) {
-            ACTION();
-            return Rule << False
-                        << (RuleHead
-                            << (RuleRef << _(RefHead) << _[RefArgSeq])
-                            << (RuleHeadSet << (Group << _(Head) << _[Tail])))
-                        << Empty << ElseSeq;
-          },
-
-        In(RuleRef) * (T(Group) << (RuleRefToken++[RuleRef] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Seq << _[RuleRef];
-          },
-
-        In(Policy) * (T(Group) << T(Var)[Var]) *
-            (T(Group) << T(UnifyBody)[UnifyBody]) >>
-          [](Match& _) {
-            ACTION();
-            return Group << _(Var) << _(UnifyBody);
-          },
-
-        // errors
-
-        In(Policy) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid rule");
-          },
-
-        In(Group) * T(Else)[Else] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Else), "Invalid else");
-          },
-
-        In(Group) * T(Default)[Default] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Default), "Invalid default rule");
-          },
-
-        In(Group) * T(If)[If] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(If), "Invalid if");
-          },
-
-        In(RuleRef) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid rule reference");
-          },
-
-        In(With) * (T(RuleRef)[RuleRef] << (T(Group) << End)) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(RuleRef), "Empty rule ref");
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/strings.cc b/src/passes/strings.cc
deleted file mode 100644
index 338772b7..00000000
--- a/src/passes/strings.cc
+++ /dev/null
@@ -1,81 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  std::string raw_to_json(const std::string& raw_string)
-  {
-    std::ostringstream buf;
-    buf << '"';
-    for (auto& c : raw_string)
-    {
-      switch (c)
-      {
-        case '`':
-          break;
-
-        case '"':
-          buf << "\\\"";
-          break;
-        case '\\':
-          buf << "\\\\";
-          break;
-
-        case '\b':
-          buf << "\\b";
-          break;
-
-        case '\f':
-          buf << "\\f";
-          break;
-
-        case '\n':
-          buf << "\\n";
-          break;
-
-        case '\r':
-          buf << "\\r";
-          break;
-
-        case '\t':
-          buf << "\\t";
-          break;
-
-        default:
-          buf << c;
-          break;
-      }
-    }
-
-    buf << '"';
-
-    return buf.str();
-  }
-
-}
-
-namespace rego
-{
-  // Convert all RawString nodes to JSONString nodes.
-  PassDef strings()
-  {
-    return {
-      "strings",
-      wf_pass_strings,
-      dir::topdown,
-      {
-        In(String) * T(RawString)[RawString] >>
-          [](Match& _) {
-            ACTION();
-            std::string raw_string =
-              std::string(_(RawString)->location().view());
-            return JSONString ^ raw_to_json(raw_string);
-          },
-
-        In(Scalar) * (T(String) << T(JSONString)[JSONString]) >>
-          [](Match& _) {
-            ACTION();
-            return _(JSONString);
-          },
-      }};
-  }
-}
\ No newline at end of file
diff --git a/src/passes/structure.cc b/src/passes/structure.cc
deleted file mode 100644
index e64d2473..00000000
--- a/src/passes/structure.cc
+++ /dev/null
@@ -1,626 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-
-  const inline std::set<Token> Ops = {
-    Add,
-    Subtract,
-    Multiply,
-    Divide,
-    Modulo,
-    Equals,
-    NotEquals,
-    GreaterThan,
-    LessThan,
-    GreaterThanOrEquals,
-    LessThanOrEquals,
-    And,
-    Or,
-    Assign,
-    Unify};
-}
-
-namespace rego
-{
-  // Modify the AST to resemble the target Rego syntax as much as possible.
-  // At this point "parsing" is done, i.e. the AST resembles the syntactical
-  // form of Rego code. From here, passes modify the AST to prepare it for
-  // unification.
-  PassDef structure()
-  {
-    PassDef structure{
-      "structure",
-      wf_pass_structure,
-      dir::topdown,
-      {
-        In(UnifyBody) *
-            (T(Group)[Lhs]([](auto& n) {
-               // test if there was a newline inside an expression
-               Node node = *n.first;
-               return contains(Ops, node->back()->type());
-             }) *
-             T(Group)[Rhs]) >>
-          [](Match& _) {
-            ACTION();
-            return Group << *_[Lhs] << *_[Rhs];
-          },
-
-        In(Query) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Literal << (Expr << *_[Group]);
-          },
-
-        In(Input) * (T(Group) << ScalarToken[Scalar]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << _(Scalar));
-          },
-
-        In(Input) * (T(Group) << StringToken[String]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << (String << _(String)));
-          },
-
-        In(Input) * (T(Group) << TermToken[Term]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << _(Term);
-          },
-
-        In(With) * (T(WithExpr) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(Membership) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(UnifyBody) *
-            (T(Group)
-             << (T(ExprEvery)
-                 << (T(VarSeq)[VarSeq] * T(UnifyBody)[UnifyBody] *
-                     (T(EverySeq) << T(Group)[EverySeq])))) >>
-          [](Match& _) {
-            ACTION();
-            Node maybe_with = _(EverySeq)->back();
-            if (maybe_with->type() == With)
-            {
-              maybe_with->parent()->pop_back();
-              return LiteralWith
-                << (UnifyBody
-                    << (Literal
-                        << (Expr
-                            << (ExprEvery << _(VarSeq) << _(UnifyBody)
-                                          << _(EverySeq)))))
-                << (WithSeq << maybe_with);
-            }
-
-            return Literal
-              << (Expr
-                  << (ExprEvery << _(VarSeq) << _(UnifyBody) << _(EverySeq)));
-          },
-
-        In(Rule) *
-            (T(RuleHead)
-             << ((T(RuleRef) << (T(Ref)[Ref] * (T(Array) << T(Group)[Key]))) *
-                 (T(RuleHeadComp)
-                  << (T(AssignOperator)[AssignOperator] * T(Group)[Val])))) >>
-          [](Match& _) {
-            ACTION();
-            return RuleHead
-              << (RuleRef << _(Ref))
-              << (RuleHeadObj << _(Key) << _(AssignOperator) << _(Val));
-          },
-
-        In(RuleHeadComp, RuleHeadFunc, RuleHeadSet, RuleHeadObj) *
-            T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(ObjectItemSeq, Object) *
-            (T(ObjectItem) << T(Group)[Key] * T(Group)[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return ObjectItem << (Expr << *_[Key]) << (Expr << *_[Val]);
-          },
-
-        In(ArrayCompr, ObjectCompr, SetCompr) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(Package) * (T(Group) << (T(Var)[Var] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Ref << (RefHead << _(Var)) << RefArgSeq;
-          },
-
-        In(Package) * (T(Group) << (T(Ref)[Ref] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Ref);
-          },
-
-        In(Import) *
-            ((T(ImportRef) << (T(Group) << (T(Ref)[Ref] * End))) * T(As) *
-             T(Undefined)) >>
-          [](Match& _) {
-            ACTION();
-            Node refhead = _(Ref) / RefHead;
-            Node refargseq = _(Ref) / RefArgSeq;
-            Node var;
-            if (refargseq->size() == 0)
-            {
-              if (refhead->front()->type() == Var)
-              {
-                var = refhead->front()->clone();
-              }
-              else
-              {
-                return err(refhead, "Invalid import statement");
-              }
-            }
-            else
-            {
-              Node refarg = refargseq->back();
-              if (refarg->type() == RefArgDot)
-              {
-                var = refarg->front()->clone();
-              }
-              else if (refarg->type() == RefArgBrack)
-              {
-                var = Var ^ strip_quotes(to_json(refarg->front()));
-              }
-              else
-              {
-                return err(refarg, "Invalid refarg in import");
-              }
-            }
-            return Seq << _(Ref) << As << var;
-          },
-
-        In(Import) * (T(ImportRef) << (T(Group) << (T(Ref)[Ref] * End))) >>
-          [](Match& _) {
-            ACTION();
-            return _(Ref);
-          },
-
-        In(Else) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(ArgSeq) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(Expr) * (T(Paren) << T(Group)[Group]) >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(Expr) * T(ObjectItemSeq)[ObjectItemSeq] >>
-          [](Match& _) {
-            ACTION();
-            return Object << *_[ObjectItemSeq];
-          },
-
-        In(Array) * (T(Group) << T(Expr)[Expr]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Expr);
-          },
-
-        In(RefArgBrack) * (T(Group) << (T(Var)[Var] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Var);
-          },
-
-        In(RefArgBrack) * (T(Group) << (ScalarToken[Val] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Scalar << _(Val);
-          },
-
-        In(RefArgBrack) * (T(Group) << (StringToken[Val] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Scalar << (String << _(Val));
-          },
-
-        In(RefArgBrack) * (T(Group) << (T(Object)[Object] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Object);
-          },
-
-        In(RefArgBrack) * (T(Group) << (T(Array)[Array] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Array);
-          },
-
-        In(RefArgBrack) * (T(Group) << (T(Set)[Set] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Set);
-          },
-
-        In(RefArgBrack) * (T(Group) << (T(UnifyBody)[UnifyBody] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Set << *_[UnifyBody];
-          },
-
-        In(RefArgBrack) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(Array, Set) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        In(UnifyBody) *
-            (T(Group) << (T(SomeDecl)[SomeDecl] * T(With)++[WithSeq])) >>
-          [](Match& _) {
-            ACTION();
-            if (_[WithSeq].first == _[WithSeq].second)
-            {
-              return Literal << _(SomeDecl);
-            }
-
-            return LiteralWith << (UnifyBody << (Literal << _(SomeDecl)))
-                               << (WithSeq << _[WithSeq]);
-          },
-
-        In(UnifyBody) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            Node withseq = NodeDef::create(WithSeq);
-            Node maybe_with = _(Group)->back();
-            while (maybe_with->type() == With)
-            {
-              _(Group)->pop_back();
-              withseq->push_front(maybe_with);
-              if (_(Group)->size() == 0)
-              {
-                break;
-              }
-
-              maybe_with = _(Group)->back();
-            }
-
-            if (_(Group)->size() == 0)
-            {
-              return withseq;
-            }
-
-            Node expr = NodeDef::create(Expr);
-            expr->insert(expr->end(), _(Group)->begin(), _(Group)->end());
-
-            if (withseq->size() > 0)
-            {
-              return LiteralWith << (UnifyBody << (Literal << expr)) << withseq;
-            }
-
-            return Literal << expr;
-          },
-
-        In(UnifyBody) * (T(Literal)[Literal] * T(WithSeq)[WithSeq]) >>
-          [](Match& _) {
-            ACTION();
-            return LiteralWith << (UnifyBody << _(Literal)) << _(WithSeq);
-          },
-
-        In(UnifyBody) *
-            ((T(LiteralWith) << (T(UnifyBody)[UnifyBody] * T(WithSeq)[Head])) *
-             T(WithSeq)[Tail]) >>
-          [](Match& _) {
-            ACTION();
-            return LiteralWith << _(UnifyBody)
-                               << (WithSeq << *_[Head] << *_[Tail]);
-          },
-
-        In(UnifyBody) *
-            ((T(LiteralWith)
-              << (T(UnifyBody)[UnifyBody] * T(WithSeq)[WithSeq])) *
-             T(With)[With]) >>
-          [](Match& _) {
-            ACTION();
-            return LiteralWith << _(UnifyBody)
-                               << (WithSeq << *_[WithSeq] << _(With));
-          },
-
-        T(Paren) << (T(Expr)[Expr] * End) >>
-          [](Match& _) {
-            ACTION();
-            return _(Expr);
-          },
-
-        In(Expr) * (T(Every) * Any++[Every] * End) >>
-          [](Match& _) {
-            ACTION();
-            return ExprEvery << _[Every];
-          },
-
-        In(Expr) * StringToken[String] >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << (String << _(String)));
-          },
-
-        In(Expr) * ScalarToken[Val] >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << _(Val));
-          },
-
-        In(Expr) * TermToken[Val] >>
-          [](Match& _) {
-            ACTION();
-            return Term << _(Val);
-          },
-
-        In(RuleArgs) * (T(Group) << StringToken[String]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << (String << _(String)));
-          },
-
-        In(RuleArgs) * (T(Group) << ScalarToken[Scalar]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << _(Scalar));
-          },
-
-        In(RuleArgs) * (T(Group) << TermToken[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return Term << _(Val);
-          },
-
-        In(RuleArgs) * (T(Group) << (T(Subtract) * (T(Int) / T(Float))[Val])) >>
-          [](Match& _) {
-            ACTION();
-            return Term << (Scalar << Resolver::negate(_(Val)));
-          },
-
-        In(ExprEvery) * (T(Group) << (T(IsIn) * ExprToken++[Expr] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return IsIn << (Expr << _[Expr]);
-          },
-
-        In(Expr) * (T(UnifyBody) << (T(Group)[Group] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return Set << _(Group);
-          },
-
-        In(VarSeq) * (T(Group) << T(Var)[Var]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Var);
-          },
-
-        In(SomeDecl) * (T(VarSeq)[VarSeq] * (T(Group) << T(Undefined))) >>
-          [](Match& _) {
-            ACTION();
-            return _(VarSeq);
-          },
-
-        In(Literal) *
-            (T(SomeDecl)
-             << (T(VarSeq)[VarSeq] *
-                 (T(Group) << (T(IsIn) * ExprToken++[Expr] * End)))) >>
-          [](Match& _) {
-            ACTION();
-            if (_(VarSeq)->size() == 1)
-            {
-              return SomeExpr << Undefined << _(VarSeq)->front()
-                              << (IsIn << (Expr << _[Expr]));
-            }
-            else if (_(VarSeq)->size() == 2)
-            {
-              return SomeExpr << *_[VarSeq] << (IsIn << (Expr << _[Expr]));
-            }
-            else
-            {
-              return err(_(VarSeq), "Invalid some expression");
-            }
-          },
-
-        In(SomeExpr) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return Expr << *_[Group];
-          },
-
-        // errors
-
-        In(Input) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid input term");
-          },
-
-        In(VarSeq) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Expected a variable");
-          },
-
-        In(Import) * T(ImportRef)[ImportRef] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ImportRef), "Invalid import reference");
-          },
-
-        In(Package) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid package name");
-          },
-
-        In(ExprCall, ExprEvery, SomeDecl) * (T(VarSeq)[VarSeq] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(VarSeq), "Missing variables");
-          },
-
-        In(DefaultRule) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid default rule");
-          },
-
-        In(ObjectCompr, ArrayCompr, SetCompr) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid comprehension");
-          },
-
-        In(Expr) * T(Contains)[Contains] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Contains), "Invalid set rule");
-          },
-
-        In(Expr) * T(Paren)[Paren] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Paren), "Invalid sub-expressions");
-          },
-
-        In(Expr) * T(With)[With] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(With), "Invalid with");
-          },
-
-        In(Expr) * T(Undefined)[Undefined] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Undefined), "Syntax error");
-          },
-
-        In(ExprEvery) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid every sequence");
-          },
-
-        In(ExprEvery) * T(EverySeq)[EverySeq] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(EverySeq), "Invalid every sequence");
-          },
-
-        T(UnifyBody)[UnifyBody] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(UnifyBody), "Empty body");
-          },
-
-        T(Expr)[Expr] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Empty expression");
-          },
-
-        In(UnifyBody, Expr) * T(SomeDecl)[SomeDecl] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(SomeDecl), "Invalid some");
-          },
-
-        In(Expr) * T(UnifyBody)[UnifyBody] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(UnifyBody), "Invalid body location");
-          },
-
-        In(RuleArgs) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid argument");
-          },
-
-        In(UnifyBody) * T(With)[With] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(With), "Invalid with statement");
-          },
-
-        In(Rego) * (T(Query)[Query] << End) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Query), "Must provide a query");
-          },
-
-        In(Expr) * T(IsIn)[IsIn] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(IsIn), "Invalid expression");
-          },
-
-        In(SomeDecl) * T(Group)[Group] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Group), "Invalid some declaration");
-          },
-
-        In(RuleRef) * (Any * Any[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Val), "Invalid rule reference");
-          },
-
-        In(RuleRef) * (T(Dot) / T(Array))[Val] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Val), "Invalid rule reference");
-          },
-
-        In(UnifyBody) * T(WithSeq)[WithSeq] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(WithSeq), "Invalid with statement");
-          },
-
-        In(UnifyBody) * T(SomeDecl)[SomeDecl] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(SomeDecl), "Invalid some declaration");
-          },
-
-        In(Set) * T(SomeDecl)[SomeDecl] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(SomeDecl), "Invalid some declaration");
-          },
-      }};
-
-    return structure;
-  }
-}
\ No newline at end of file
diff --git a/src/passes/symbols.cc b/src/passes/symbols.cc
deleted file mode 100644
index b0676e8a..00000000
--- a/src/passes/symbols.cc
+++ /dev/null
@@ -1,572 +0,0 @@
-#include "internal.hh"
-
-namespace
-{
-  using namespace rego;
-  using namespace wf::ops;
-
-  void find_assigned_vars(const Node& node, Nodes& vars)
-  {
-    if (node->type() == LiteralWith || node->type() == SomeDecl)
-    {
-      return;
-    }
-
-    if (node->type() == Var)
-    {
-      Nodes defs = node->lookup();
-      if (defs.size() == 0)
-      {
-        vars.push_back(node->clone());
-      }
-    }
-    else
-    {
-      for (auto& child : *node)
-      {
-        find_assigned_vars(child, vars);
-      }
-    }
-  }
-
-  const inline auto LocalToken = T(RefTerm) / T(Term);
-}
-
-namespace rego
-{
-  // Process most nodes that will be referenced by symbols so that they can be
-  // appropriately indexed. Also processes some nodes into forms that will
-  // be easier to process for later passes.
-  PassDef symbols()
-  {
-    return {
-      "symbols",
-      wf_pass_symbols,
-      dir::topdown,
-      {
-        In(Query) * (T(Literal)[Head] * T(Literal)++[Tail]) >>
-          [](Match& _) {
-            ACTION();
-            return UnifyBody << _(Head) << _[Tail];
-          },
-
-        In(ModuleSeq) *
-            (T(Module)
-             << (T(Package)[Package] * T(ImportSeq)[ImportSeq] *
-                 T(Policy)[Policy])) >>
-          [](Match& _) {
-            ACTION();
-            Node policy = NodeDef::create(Policy);
-            for (auto& import : *_(ImportSeq))
-            {
-              if (import->type() == Import)
-              {
-                policy->push_back(import);
-              }
-            }
-            policy->insert(policy->end(), _(Policy)->begin(), _(Policy)->end());
-            return Module << _(Package) << policy;
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(False) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadComp)
-                       << (T(AssignOperator) * T(Expr)[Expr])))) *
-                 (T(Empty) / T(UnifyBody))[Body] * T(ElseSeq)[ElseSeq])) >>
-          [](Match& _) {
-            ACTION();
-            Node elseseq = _(ElseSeq);
-            if (elseseq->size() == 0)
-            {
-              return RuleComp << _(Id) << _(Body) << _(Expr) << (Int ^ "0");
-            }
-
-            // we need to create sub-rules for each possibility
-            Location rulename = _(Id)->location();
-            Location subrule = _.fresh(rulename);
-            Node seq = NodeDef::create(Seq);
-            seq
-              << (RuleComp << (Var ^ subrule) << _(Body) << _(Expr)
-                           << (Int ^ "0"));
-            for (std::size_t i = 0; i < elseseq->size(); ++i)
-            {
-              Node expr = elseseq->at(i) / Val;
-              Node body = elseseq->at(i) / Body;
-              seq
-                << (RuleComp << (Var ^ subrule) << body << expr
-                             << (Int ^ std::to_string(i + 1)));
-            }
-
-            Location value = _.fresh({"value"});
-            return seq
-              << (RuleComp << _(Id) << Empty
-                           << (UnifyBody
-                               << (Literal
-                                   << (Expr << (RefTerm << (Var ^ value))
-                                            << Unify
-                                            << (RefTerm << (Var ^ subrule)))))
-                           << (Int ^ "0"));
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(True) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadComp)
-                       << (T(AssignOperator) * T(Expr)[Expr])))) *
-                 T(Empty) * (T(ElseSeq) << End))) >>
-          [](Match& _) {
-            ACTION();
-            std::size_t rank = std::numeric_limits<std::uint16_t>::max();
-            return RuleComp << _(Id) << Empty << _(Expr)
-                            << (Int ^ std::to_string(rank));
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(False) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadFunc)
-                       << (T(RuleArgs)[RuleArgs] * T(AssignOperator) *
-                           T(Expr)[Expr])))) *
-                 (T(Empty) / T(UnifyBody))[Body] * T(ElseSeq)[ElseSeq])) >>
-          [](Match& _) {
-            ACTION();
-            Node elseseq = _(ElseSeq);
-            if (elseseq->size() == 0)
-            {
-              return RuleFunc << _(Id) << _(RuleArgs) << _(Body) << _(Expr)
-                              << (Int ^ "0");
-            }
-
-            // we need to create sub-rules for each possibility
-            Location rulename = _(Id)->location();
-            Location subrule = _.fresh(rulename);
-            Node seq = NodeDef::create(Seq);
-            seq
-              << (RuleFunc << (Var ^ subrule) << _(RuleArgs) << _(Body)
-                           << _(Expr) << (Int ^ "0"));
-            for (std::size_t i = 0; i < elseseq->size(); ++i)
-            {
-              Node expr = elseseq->at(i) / Val;
-              Node body = elseseq->at(i) / Body;
-              seq
-                << (RuleFunc << (Var ^ subrule) << _(RuleArgs)->clone() << body
-                             << expr << (Int ^ std::to_string(i + 1)));
-            }
-
-            Location value = _.fresh({"value"});
-            Node argseq = NodeDef::create(ArgSeq);
-            for (auto& arg : *_(RuleArgs))
-            {
-              if (arg->type() == ArgVar)
-              {
-                argseq->push_back(Expr << (RefTerm << (arg / Var)->clone()));
-              }
-              else if (arg->type() == ArgVal)
-              {
-                argseq->push_back(Expr << (Term << arg->front()->clone()));
-              }
-            }
-
-            return seq
-              << (RuleFunc
-                  << _(Id) << _(RuleArgs)->clone() << Empty
-                  << (UnifyBody
-                      << (Literal
-                          << (Expr << (RefTerm << (Var ^ value)) << Unify
-                                   << (ExprCall << (RuleRef << (Var ^ subrule))
-                                                << argseq))))
-                  << (Int ^ "0"));
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(True) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadFunc)
-                       << (T(RuleArgs)[RuleArgs] * T(AssignOperator) *
-                           T(Expr)[Expr])))) *
-                 T(Empty) * (T(ElseSeq) << End))) >>
-          [](Match& _) {
-            ACTION();
-            std::size_t rank = std::numeric_limits<std::uint16_t>::max();
-            return RuleFunc << _(Id) << _(RuleArgs) << Empty << _(Expr)
-                            << (Int ^ std::to_string(rank));
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(False) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadSet) << T(Expr)[Expr]))) *
-                 (T(Empty) / T(UnifyBody))[Body] * T(ElseSeq))) >>
-          [](Match& _) {
-            ACTION();
-            return RuleSet << _(Id) << _(Body) << _(Expr);
-          },
-
-        In(Policy) *
-            (T(Rule)
-             << (T(False) *
-                 (T(RuleHead)
-                  << ((T(RuleRef) << T(Var)[Id]) *
-                      (T(RuleHeadObj)
-                       << (T(Expr)[Key] * T(AssignOperator) * T(Expr)[Val])))) *
-                 (T(Empty) / T(UnifyBody))[Body] * T(ElseSeq))) >>
-          [](Match& _) {
-            ACTION();
-            return RuleObj << _(Id) << _(Body) << _(Key) << _(Val);
-          },
-
-        In(Expr) * (T(Expr) << (T(Term)[Term] * End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Term);
-          },
-
-        In(Expr) * (T(Term) << (T(Ref) / T(Var))[Val]) >>
-          [](Match& _) {
-            ACTION();
-            return RefTerm << _(Val);
-          },
-
-        In(Expr) * (T(Term) << (T(Scalar) << (T(Int) / T(Float))[Val])) >>
-          [](Match& _) {
-            ACTION();
-            return NumTerm << _(Val);
-          },
-
-        In(Expr) * (T(Term) << (T(Set) / T(SetCompr))[Set]) >>
-          [](Match& _) {
-            ACTION();
-            return _(Set);
-          },
-
-        In(RefArgBrack) * T(Var)[Var] >>
-          [](Match& _) {
-            ACTION();
-            return RefTerm << _(Var);
-          },
-
-        In(UnifyBody) * (T(Literal) << (T(SomeDecl) << T(VarSeq)[VarSeq])) >>
-          [](Match& _) {
-            ACTION();
-            Node seq = NodeDef::create(Seq);
-            for (auto& var : *_(VarSeq))
-            {
-              seq->push_back(Local << var << Undefined);
-            }
-            return seq;
-          },
-
-        In(UnifyBody) *
-            (T(Literal)
-             << (T(SomeExpr)
-                 << (T(Undefined) * T(Expr)[Item] *
-                     (T(IsIn) << T(Expr)[ItemSeq])))) >>
-          [](Match& _) {
-            ACTION();
-            Location item = _.fresh({"item"});
-            return Seq << (Local << (Var ^ item) << Undefined)
-                       << (LiteralEnum << (Var ^ item) << _(ItemSeq))
-                       << (Literal
-                           << (Expr
-                               << _(Item) << Unify
-                               << (RefTerm
-                                   << (Ref
-                                       << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "1"))))))));
-          },
-
-        In(UnifyBody) *
-            (T(Literal)
-             << (T(SomeExpr)
-                 << (T(Expr)[Idx] * T(Expr)[Item] *
-                     (T(IsIn) << T(Expr)[ItemSeq])))) >>
-          [](Match& _) {
-            ACTION();
-            Location item = _.fresh({"item"});
-            return Seq << (Local << (Var ^ item) << Undefined)
-                       << (LiteralEnum << (Var ^ item) << _(ItemSeq))
-                       << (Literal
-                           << (Expr
-                               << _(Idx) << Unify
-                               << (RefTerm
-                                   << (Ref
-                                       << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "0"))))))))
-                       << (Literal
-                           << (Expr
-                               << _(Item) << Unify
-                               << (RefTerm
-                                   << (Ref
-                                       << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "1"))))))));
-          },
-
-        In(UnifyBody, LiteralWith) *
-            (T(Literal)
-             << (T(Expr) << (LocalToken++[Head] * T(Assign) * Any++[Tail]))) >>
-          [](Match& _) {
-            ACTION();
-            Node seq = NodeDef::create(Seq);
-            Nodes vars;
-            for (auto& node : *_(Head))
-            {
-              find_assigned_vars(node, vars);
-            }
-
-            for (auto& var : vars)
-            {
-              seq->push_back(Lift << UnifyBody << (Local << var << Undefined));
-            }
-
-            seq->push_back(Literal << (Expr << _[Head] << Unify << _[Tail]));
-            return seq;
-          },
-
-        In(Policy) * (T(Import) << (T(Ref)[Ref] * T(As) * T(Var)[Var])) >>
-          [](Match& _) {
-            ACTION();
-            return Import << _(Var) << _(Ref);
-          },
-
-        In(RuleComp, RuleFunc, RuleObj, RuleSet) *
-            (T(Expr)
-             << (T(Term)[Term]([](auto& n) { return is_constant(*n.first); }) *
-                 End)) >>
-          [](Match& _) {
-            ACTION();
-            return _(Term);
-          },
-
-        In(RuleComp, RuleFunc, RuleObj, RuleSet) *
-            (T(Expr) << (T(NumTerm)[NumTerm] * End)) >>
-          [](Match& _) {
-            ACTION();
-            Node number = _(NumTerm)->front();
-            return Term << (Scalar << number);
-          },
-
-        In(Expr) * (Start * T(Subtract) * T(NumTerm)[NumTerm] * End) >>
-          [](Match& _) {
-            ACTION();
-            Node number = Resolver::negate(_(NumTerm)->front());
-            return NumTerm << number;
-          },
-
-        In(RuleComp, RuleFunc) * (T(Empty) * T(Expr)[Expr]) >>
-          [](Match& _) {
-            ACTION();
-            Location out = _.fresh({"out"});
-            Location value = _.fresh({"value"});
-            return Seq << (UnifyBody
-                           << (Literal
-                               << (Expr << (RefTerm << (Var ^ out)) << Unify
-                                        << *_[Expr])))
-                       << (UnifyBody
-                           << (Literal
-                               << (Expr << (RefTerm << (Var ^ value)) << Unify
-                                        << (RefTerm << (Var ^ out)))));
-          },
-
-        In(RuleComp, RuleFunc) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            Location value = _.fresh({"value"});
-            return UnifyBody
-              << (Literal
-                  << (Expr << (RefTerm << (Var ^ value)) << Unify << *_[Expr]));
-          },
-
-        In(ObjectCompr, SetCompr, ArrayCompr) * T(UnifyBody)[UnifyBody] >>
-          [](Match& _) {
-            ACTION();
-            std::string prefix;
-            auto& parent_type = _(UnifyBody)->parent()->type();
-            if (parent_type == ArrayCompr)
-            {
-              prefix = "array";
-            }
-            else if (parent_type == SetCompr)
-            {
-              prefix = "set";
-            }
-            else
-            {
-              prefix = "object";
-            }
-            Location compr = _.fresh({prefix + "compr"});
-            return NestedBody << (Key ^ compr) << _(UnifyBody);
-          },
-
-        In(Expr) *
-            ((T(ExprEvery) * In(UnifyBody)++)
-             << ((T(VarSeq) << (T(Var)[Val] * End)) * T(UnifyBody)[UnifyBody] *
-                 (T(IsIn) << T(Expr)[Expr]))) >>
-          [](Match& _) {
-            ACTION();
-            Location item = _.fresh({"item"});
-            Location itemseq = _.fresh({"itemseq"});
-            Node rulebody = UnifyBody
-              << (Local << (Var ^ item) << Undefined)
-              << (LiteralEnum << (Var ^ item)
-                              << (Expr << (RefTerm << (Var ^ itemseq))))
-              << (Literal
-                  << (Expr << (RefTerm << _(Val)->clone()) << Unify
-                           << (RefTerm
-                               << (Ref << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "1"))))))))
-              << *_[UnifyBody];
-
-            return Seq
-              << (Lift << UnifyBody << (Local << (Var ^ itemseq) << Undefined))
-              << (Lift << UnifyBody
-                       << (Literal
-                           << (Expr << (RefTerm << (Var ^ itemseq)) << Unify
-                                    << *_[Expr])))
-              << (ExprCall << (RuleRef << (Var ^ "count"))
-                           << (ArgSeq
-                               << (Expr
-                                   << (Term
-                                       << (SetCompr
-                                           << (Expr
-                                               << (RefTerm << _(Val)->clone()))
-                                           << rulebody)))))
-              << Equals
-              << (ExprCall
-                  << (RuleRef << (Var ^ "count"))
-                  << (ArgSeq
-                      << (Expr
-                          << (ExprCall
-                              << (RuleRef << (Var ^ "cast_set"))
-                              << (ArgSeq
-                                  << (Expr
-                                      << (RefTerm << (Var ^ itemseq))))))));
-          },
-
-        In(Expr) *
-            ((T(ExprEvery) * In(UnifyBody)++)
-             << ((T(VarSeq) << (T(Var)[Idx] * T(Var)[Val] * End)) *
-                 T(UnifyBody)[UnifyBody] * (T(IsIn) << T(Expr)[Expr]))) >>
-          [](Match& _) {
-            ACTION();
-            Location item = _.fresh({"item"});
-            Location itemseq = _.fresh({"itemseq"});
-            Node rulebody = UnifyBody
-              << (Local << (Var ^ item) << Undefined)
-              << (LiteralEnum << (Var ^ item)
-                              << (Expr << (RefTerm << (Var ^ itemseq))))
-              << (Literal
-                  << (Expr << (RefTerm << _(Idx)->clone()) << Unify
-                           << (RefTerm
-                               << (Ref << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "0"))))))))
-              << (Literal
-                  << (Expr << (RefTerm << _(Val)->clone()) << Unify
-                           << (RefTerm
-                               << (Ref << (RefHead << (Var ^ item))
-                                       << (RefArgSeq
-                                           << (RefArgBrack
-                                               << (Scalar << (Int ^ "1"))))))))
-              << *_[UnifyBody];
-
-            return Seq << (Lift << UnifyBody
-                                << (Local << (Var ^ itemseq) << Undefined))
-                       << (Lift << UnifyBody
-                                << (Literal
-                                    << (Expr << (RefTerm << (Var ^ itemseq))
-                                             << Unify << *_[Expr])))
-                       << (ExprCall
-                           << (RuleRef << (Var ^ "count"))
-                           << (ArgSeq
-                               << (Expr
-                                   << (Term
-                                       << (ArrayCompr
-                                           << (Expr
-                                               << (RefTerm << _(Val)->clone()))
-                                           << rulebody)))))
-                       << Equals
-                       << (ExprCall
-                           << (RuleRef << (Var ^ "count"))
-                           << (ArgSeq
-                               << (Expr << (RefTerm << (Var ^ itemseq)))));
-          },
-
-        // errors
-
-        In(Policy) * T(Rule)[Rule] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Rule), "Invalid rule");
-          },
-
-        In(UnifyBody) * (T(RuleHead)[RuleHead] << (T(Var) * T(RuleHeadFunc))) >>
-          [](Match& _) {
-            ACTION();
-            return err(_(RuleHead), "No rule functions allowed in rule bodies");
-          },
-
-        In(Term) * T(Ref)[Ref] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Ref), "Invalid ref term");
-          },
-
-        In(Term) * T(Var)[Var] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Var), "Invalid var term");
-          },
-
-        In(Literal) * T(SomeDecl)[SomeDecl] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(SomeDecl), "Invalid some declaration");
-          },
-
-        In(Expr) * T(Assign)[Assign] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Assign), "Invalid assignment");
-          },
-
-        T(UnifyBody)[UnifyBody] << End >>
-          [](Match& _) {
-            ACTION();
-            return err(_(UnifyBody), "Invalid unification body");
-          },
-
-        In(Expr) * T(Dot)[Dot] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Dot), "Invalid dot expression");
-          },
-
-        In(Expr) * T(ExprEvery)[ExprEvery] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(ExprEvery), "Invalid every expression");
-          },
-      }};
-  }
-
-}
diff --git a/src/reader.cc b/src/reader.cc
new file mode 100644
index 00000000..72f619c6
--- /dev/null
+++ b/src/reader.cc
@@ -0,0 +1,1559 @@
+#include "internal.hh"
+
+namespace
+{
+  using namespace rego;
+
+  bool in_parent(NodeRange range, Token token)
+  {
+    return range.front()->parent()->parent() == token;
+  }
+
+  bool newline_between(Node lhs, Node rhs)
+  {
+    Location start = lhs->location();
+    Location end = rhs->location();
+    Location between(
+      start.source, start.pos + start.len, end.pos - start.pos - start.len);
+    return between.view().find('\n') != std::string::npos;
+  }
+
+  const std::set<Token> op_tokens = {
+    Add,
+    Subtract,
+    Multiply,
+    Divide,
+    Modulo,
+    Equals,
+    NotEquals,
+    LessThan,
+    GreaterThan,
+    LessThanOrEquals,
+    GreaterThanOrEquals,
+    Or,
+    And,
+    Assign,
+    Unify,
+    IsIn,
+    Colon,
+    As};
+
+  Nodes comma_separation(Node group)
+  {
+    Nodes items;
+    Node current = NodeDef::create(Group);
+    for (Node node : *group)
+    {
+      if (node == Comma)
+      {
+        items.push_back(current);
+        current = NodeDef::create(Group);
+      }
+      else
+      {
+        current->push_back(node);
+      }
+    }
+
+    if (!current->empty())
+    {
+      items.push_back(current);
+    }
+
+    return items;
+  }
+
+  Node set_or_query(Node brace, bool has_commas)
+  {
+    auto parent = brace->parent();
+    Node group = brace->front();
+    auto pos = parent->find(brace);
+    Node before = pos != parent->begin() ? pos[-1] : nullptr;
+    Node after = pos + 1 != parent->end() ? pos[1] : nullptr;
+    if (before != nullptr && contains(op_tokens, before->type()))
+    {
+      if (has_commas)
+      {
+        return Set << comma_separation(group);
+      }
+      else
+      {
+        return Set << group;
+      }
+    }
+    else if (after != nullptr && contains(op_tokens, after->type()))
+    {
+      if (has_commas)
+      {
+        return Set << comma_separation(group);
+      }
+      else
+      {
+        return Set << group;
+      }
+    }
+    else if (before == nullptr && after == nullptr)
+    {
+      if (has_commas)
+      {
+        return Set << comma_separation(group);
+      }
+      else
+      {
+        return Set << group;
+      }
+    }
+    else
+    {
+      return Query << group;
+    }
+  }
+
+  const auto RefGroup = TokenDef("rego-refgroup");
+  const auto Keyword = TokenDef("rego-keyword", flag::lookdown);
+
+  const auto wf_prep_tokens = (wf_parse_tokens | Scalar | Placeholder) -
+    (Int | Float | JSONString | RawString | True | False | Null);
+
+  // clang-format off
+  const auto wf_prep =
+    (Top <<= (Module | Query))
+    | (Square <<= Group++)
+    | (Brace <<= Group++)
+    | (Paren <<= Group++)
+    | (Module <<= (Group | Package | Import)++)
+    | (Query <<= Group)
+    | (Package <<= RefGroup)
+    | (Import <<= RefGroup * Var)
+    | (Scalar <<= Int | Float | String | True | False | Null)
+    | (String <<= JSONString | RawString)
+    | (RefGroup <<= (Var | Dot | Square)++)
+    | (Group <<= wf_prep_tokens++)
+    ;
+  // clang-format on
+
+  PassDef prep()
+  {
+    return {
+      "prep",
+      wf_prep,
+      dir::topdown | dir::once,
+      {
+        In(Top) * (T(File) << (T(Group) << (T(Module)[Module]))) >>
+          [](Match& _) { return _(Module); },
+
+        In(Top) * (T(File) << (T(Group) << (T(Query)[Query]))) >>
+          [](Match& _) { return _(Query); },
+
+        In(Module) * T(Group)
+            << (T(Package) * T(Var, Dot, Square)++[Group] * End) >>
+          [](Match& _) { return Package << (RefGroup << _[Group]); },
+
+        In(Module) * T(Group)
+            << (T(Import) * T(Var, Dot, Square)++[Group] * End) >>
+          [](Match& _) {
+            Node alias = _[Group].back();
+            if (alias == Square)
+            {
+              Node group = alias->front();
+              if (group->empty())
+              {
+                return err(alias, "Invalid import", "well-formed error");
+              }
+              alias = group->front();
+              if (alias == JSONString)
+              {
+                alias = Var ^ strip_quotes(alias->location().view());
+              }
+            }
+
+            if (alias != Var)
+            {
+              return err(alias, "Invalid import", "well-formed error");
+            }
+
+            return Import << (RefGroup << _[Group]) << alias->clone();
+          },
+
+        T(Group)
+            << (T(Import) * T(Var, Dot, Square)++[Group] * T(As) * T(Var)[Var] *
+                End) >>
+          [](Match& _) { return Import << (RefGroup << _[Group]) << _(Var); },
+
+        T(Var, "_")[Var] >> [](Match& _) { return Placeholder ^ _(Var); },
+
+        In(Group) * T(Int, Float, True, False, Null)[Scalar] >>
+          [](Match& _) { return Scalar << _(Scalar); },
+
+        In(Group) * T(JSONString, RawString)[String] >>
+          [](Match& _) { return Scalar << (String << _(String)); },
+
+        // errors
+        In(Top) * T(File)[File] >>
+          [](Match& _) {
+            return err(_(File), "Invalid file", WellFormedError);
+          },
+
+        T(Package)[Package] << End >>
+          [](Match& _) {
+            return err(_(Package), "Invalid package declaration");
+          },
+
+        T(Import)[Import] << End >>
+          [](Match& _) { return err(_(Import), "Invalid import declaration"); },
+      }};
+  }
+
+  const auto wf_keywords_tokens = wf_prep_tokens | If | IsIn | Contains | Every;
+
+  // clang-format off
+  const auto wf_keywords =
+    wf_prep
+    | (Module <<= (Group | Package | Import | Keyword)++)
+    | (Keyword <<= Var)[Var]
+    | (Group <<= wf_keywords_tokens++)
+    ;
+  // clang-format on
+
+  PassDef keywords()
+  {
+    std::shared_ptr<std::map<std::string, Token>> keywords =
+      std::make_shared<std::map<std::string, Token>>();
+    PassDef pass = {
+      "keywords",
+      wf_keywords,
+      dir::topdown | dir::once,
+      {
+        In(Group) *
+            T(Var, "(?:if|in|contains|every)")[Var]([keywords](auto& n) {
+              std::string keyword(n.front()->location().view());
+              return keywords->count(keyword) != 0;
+            }) >>
+          [keywords](Match& _) {
+            std::string keyword(_(Var)->location().view());
+            return keywords->at(keyword) ^ _(Var);
+          },
+
+        In(Module) *
+            (T(Import)
+             << (T(RefGroup)
+                 << (T(Var, "future") * T(Dot) * T(Var, "keywords") *
+                     ~(T(Dot) * T(Var, "(?:if|in|contains|every)")[Var])))) >>
+          [keywords](Match& _) {
+            auto nodes = NodeDef::create(Seq);
+            if (_(Var) == nullptr)
+            {
+              keywords->insert({"if", If});
+              nodes << (Lift << Module << (Keyword << (Var ^ "if")));
+              keywords->insert({"in", IsIn});
+              nodes << (Lift << Module << (Keyword << (Var ^ "in")));
+              keywords->insert({"contains", Contains});
+              nodes << (Lift << Module << (Keyword << (Var ^ "contains")));
+              keywords->insert({"every", Every});
+              nodes << (Lift << Module << (Keyword << (Var ^ "every")));
+            }
+            else
+            {
+              std::string keyword(_(Var)->location().view());
+              if (keyword == "if")
+              {
+                keywords->insert({"if", If});
+              }
+              else if (keyword == "in")
+              {
+                keywords->insert({"in", IsIn});
+              }
+              else if (keyword == "contains")
+              {
+                keywords->insert({"contains", Contains});
+              }
+              else if (keyword == "every")
+              {
+                keywords->insert({"in", IsIn});
+                keywords->insert({"every", Every});
+              }
+              nodes << (Lift << Module << (Keyword << _(Var)));
+            }
+
+            return nodes;
+          },
+      }};
+
+    pass.post([keywords](Node) {
+      keywords->clear();
+      return 0;
+    });
+
+    return pass;
+  }
+
+  // clang-format off
+  const auto wf_some_every =
+    wf_keywords
+    | (Every <<= Var++)
+    | (Some <<= Var++)
+    ;
+  // clang-format on
+
+  PassDef some_every()
+  {
+    return {
+      "some_every",
+      wf_some_every,
+      dir::bottomup | dir::once,
+      {
+        In(Group) *
+            (T(Some) * T(Var)[Head] * (T(Comma) * T(Var))++[Tail] *
+             --T(IsIn, Dot, Comma)) >>
+          [](Match& _) -> Node {
+          Node some = Some << _(Head);
+          for (auto it = _[Tail].begin(); it != _[Tail].end(); it += 2)
+          {
+            // skip the comma
+            some << it[1];
+          }
+
+          return some;
+        },
+
+        In(Group) * (T(Every) * T(Var)[Head] * (T(Comma) * T(Var))++[Tail]) >>
+          [](Match& _) {
+            Node every = Every << _(Head);
+            for (auto it = _[Tail].begin(); it != _[Tail].end(); it += 2)
+            {
+              // skip the comma
+              every << it[1];
+            }
+
+            return every;
+          },
+      }};
+  }
+
+  const auto wf_ref_args_tokens =
+    (wf_keywords_tokens | RefArgDot | RefArgBrack);
+
+  // clang-format off
+  const auto wf_ref_args =
+    wf_some_every
+    | (RefArgDot <<= Var)
+    | (RefArgBrack <<= Group)
+    | (RefGroup <<= (Var | RefArgDot | RefArgBrack)++)
+    | (Group <<= wf_ref_args_tokens++)
+    ;
+  // clang-format on
+
+  PassDef ref_args()
+  {
+    return {
+      "ref_args",
+      wf_ref_args,
+      dir::bottomup,
+      {
+        In(Group, RefGroup) * (T(Dot) * T(Var)[Var])[RefArgDot] >>
+          [](Match& _) { return RefArgDot << _(Var); },
+
+        In(Group) * (T(Dot) * T(If, IsIn, Contains, Every)[Var]) >>
+          [](Match& _) { return RefArgDot << (Var ^ _(Var)); },
+
+        In(Group) * (T(Contains)[Contains] * T(Paren)[Paren]) >>
+          [](Match& _) { return Seq << (Var ^ _(Contains)) << _(Paren); },
+
+        In(Group, RefGroup) *
+            (T(Var, RefArgDot, RefArgBrack, Square, Brace, Paren)[Val] *
+             (T(Square) << T(Group)[Group]))(
+              [](auto n) { return !newline_between(n.front(), n.back()); }) >>
+          [](Match& _) { return Seq << _(Val) << (RefArgBrack << _(Group)); },
+
+        // errors
+
+        In(RefGroup) * T(Dot, Square)[Val] >>
+          [](Match& _) {
+            return err(_(Val), "Invalid reference argument", WellFormedError);
+          },
+      }};
+  }
+
+  const auto wf_refs_tokens = (wf_ref_args_tokens | Expr | Ref);
+
+  // clang-format off
+  const auto wf_refs =
+    wf_ref_args
+    | (Expr <<= ExprCall)
+    | (ExprCall <<= Ref * ExprSeq)
+    | (ExprSeq <<= Group++)
+    | (Ref <<= RefHead * RefArgSeq)
+    | (RefHead <<= Var | ExprCall | Square | Brace)
+    | (RefArgSeq <<= (RefArgDot | RefArgBrack)++)
+    | (RefGroup <<= Var | Ref)
+    | (Group <<= wf_refs_tokens++)
+    ;
+  // clang-format on
+
+  PassDef refs()
+  {
+    PassDef pass = {
+      "refs",
+      wf_refs,
+      dir::bottomup,
+      {
+        In(Group) * (T(Ref, Var)[Ref] * (T(Paren) << T(Group)[Group])) >>
+          [](Match& _) {
+            Node ref = _(Ref);
+            if (ref == Var)
+            {
+              ref = Ref << (RefHead << ref) << RefArgSeq;
+            }
+
+            return ExprCall << ref << (ExprSeq << comma_separation(_(Group)));
+          },
+
+        In(Group) * (T(Ref, Var)[Ref] * (T(Paren) << End)) >>
+          [](Match& _) {
+            Node ref = _(Ref);
+            if (ref == Var)
+            {
+              ref = Ref << (RefHead << ref) << RefArgSeq;
+            }
+
+            return ExprCall << ref << ExprSeq;
+          },
+
+        In(Group, RefGroup) *
+            (T(Var, ExprCall, Square, Brace)[RefHead] *
+             T(RefArgDot, RefArgBrack)[Head] *
+             T(RefArgDot, RefArgBrack)++[Tail]) >>
+          [](Match& _) {
+            return Ref << (RefHead << _(RefHead))
+                       << (RefArgSeq << _(Head) << _[Tail]);
+          },
+
+        In(Group, RefGroup) * T(Var)[Var] >>
+          [](Match& _) { return Ref << (RefHead << _(Var)) << RefArgSeq; },
+
+        In(Group) * T(ExprCall)[ExprCall] >>
+          [](Match& _) { return Expr << _(ExprCall); },
+
+        // errors
+
+        In(RefGroup) * T(Var, RefArgDot, RefArgBrack)[Ref] >>
+          [](Match& _) {
+            return err(_(Ref), "Invalid reference", WellFormedError);
+          },
+
+        T(RefGroup)[RefGroup] << End >>
+          [](Match& _) {
+            return err(_(RefGroup), "Missing reference", WellFormedError);
+          },
+
+        In(RefGroup) * (T(Ref) * T(Ref)[Ref]) >>
+          [](Match& _) {
+            return err(_(Ref), "Invalid reference", WellFormedError);
+          },
+      }};
+
+    return pass;
+  }
+
+  const auto wf_groups_tokens = (wf_refs_tokens | Array | Object | Set |
+                                 ArrayCompr | SetCompr | ObjectCompr) -
+    (Square | Brace | Paren | EmptySet);
+
+  // clang-format off
+  const auto wf_groups =
+    wf_refs
+    | (Expr <<= ExprParens | ExprCall)
+    | (ExprParens <<= Group)
+    | (RefHead <<= Var | ExprCall | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr)
+    | (Array <<= Group++)
+    | (Object <<= Group++)
+    | (Set <<= Group++)
+    | (Query <<= Group)
+    | (ArrayCompr <<= Group * Query)
+    | (SetCompr <<= Group * Query)
+    | (ObjectCompr <<= Group * Query)
+    | (Package <<= Ref)
+    | (Import <<= Ref * Var)
+    | (RefArgBrack <<= Group | Placeholder)
+    | (Group <<= wf_groups_tokens++)
+    ;
+  // clang-format on
+
+  PassDef groups()
+  {
+    auto comma_groups = std::make_shared<std::set<NodeDef*>>();
+    auto colon_groups = std::make_shared<std::set<NodeDef*>>();
+    auto or_groups = std::make_shared<std::set<NodeDef*>>();
+
+    PassDef pass = {
+      "groups",
+      wf_groups,
+      dir::bottomup | dir::once,
+      {
+        In(Package) * (T(RefGroup) << (T(Ref)[Ref] * End)) >>
+          [](Match& _) { return _(Ref); },
+
+        In(Import) * (T(RefGroup) << (T(Ref)[Ref] * End)) >>
+          [](Match& _) { return _(Ref); },
+
+        T(Brace)[Brace] << T(Group)[Group] >>
+          [comma_groups, colon_groups, or_groups](Match& _) {
+            NodeDef* group = _(Group).get();
+            if (contains(comma_groups, group))
+            {
+              comma_groups->erase(group);
+              if (colon_groups->count(group) != 0)
+              {
+                colon_groups->erase(group);
+                if (contains(or_groups, group))
+                {
+                  or_groups->erase(group);
+                  auto pos = _(Group)->find_first(Or, _(Group)->begin());
+                  Nodes item(_(Group)->begin(), pos);
+                  Nodes query(pos + 1, _(Group)->end());
+                  return ObjectCompr << (Group << item)
+                                     << (Query << (Group << query));
+                }
+                else
+                {
+                  return Object << comma_separation(_(Group));
+                }
+              }
+              else
+              {
+                return set_or_query(_(Brace), true);
+              }
+            }
+            else if (contains(colon_groups, group))
+            {
+              colon_groups->erase(group);
+              if (contains(or_groups, group))
+              {
+                or_groups->erase(group);
+                auto pos = _(Group)->find_first(Or, _(Group)->begin());
+                Nodes item(_(Group)->begin(), pos);
+                Nodes query(pos + 1, _(Group)->end());
+                return ObjectCompr << (Group << item)
+                                   << (Query << (Group << query));
+              }
+              else
+              {
+                return Object << _(Group);
+              }
+            }
+            else if (contains(or_groups, group))
+            {
+              or_groups->erase(group);
+              auto pos = _(Group)->find_first(Or, _(Group)->begin());
+              Nodes item(_(Group)->begin(), pos);
+              Nodes query(pos + 1, _(Group)->end());
+              return SetCompr << (Group << item) << (Query << (Group << query));
+            }
+            else
+            {
+              return set_or_query(_(Brace), false);
+            }
+          },
+
+        T(Square) << T(Group)[Group] >>
+          [comma_groups, or_groups](Match& _) {
+            NodeDef* group = _(Group).get();
+            if (contains(or_groups, group))
+            {
+              or_groups->erase(group);
+              if (contains(comma_groups, group))
+              {
+                comma_groups->erase(group);
+                return Array << comma_separation(_(Group));
+              }
+              else
+              {
+                auto pos = _(Group)->find_first(Or, _(Group)->begin());
+                Nodes item(_(Group)->begin(), pos);
+                Nodes query(pos + 1, _(Group)->end());
+                return ArrayCompr << (Group << item)
+                                  << (Query << (Group << query));
+              }
+            }
+            else if (contains(comma_groups, group))
+            {
+              return Array << comma_separation(_(Group));
+            }
+            else
+            {
+              return Array << _(Group);
+            }
+          },
+
+        T(Paren) << T(Group)[Group] >>
+          [](Match& _) { return Expr << (ExprParens << _(Group)); },
+
+        T(EmptySet)[EmptySet] >> [](Match& _) { return Set ^ _(EmptySet); },
+
+        In(RefArgBrack) * (T(Group) << T(Placeholder)[Placeholder]) >>
+          [](Match& _) { return _(Placeholder); },
+
+        T(Square)[Square] << End >> [](Match& _) { return Array ^ _(Square); },
+
+        T(Brace)[Brace] << End >> [](Match& _) { return Object ^ _(Brace); },
+
+        // errors
+
+        In(Group) * T(Brace, Square, Paren)[Val] >>
+          [](Match& _) {
+            return err(_(Val), "Invalid collection", WellFormedError);
+          },
+
+        T(RefGroup)[RefGroup] >>
+          [](Match& _) {
+            return err(_(RefGroup), "Invalid reference", WellFormedError);
+          },
+      }};
+
+    pass.pre(Comma, [comma_groups](Node node) {
+      comma_groups->insert(node->parent());
+      return 0;
+    });
+
+    pass.pre(Colon, [colon_groups](Node node) {
+      colon_groups->insert(node->parent());
+      return 0;
+    });
+
+    pass.pre(Or, [or_groups](Node node) {
+      auto group = node->parent();
+      auto pos = group->find(node);
+      for (auto it = group->begin(); it != pos; ++it)
+      {
+        Node n = *it;
+        if (n == Comma || (n != Colon && contains(op_tokens, n->type())))
+        {
+          return 0;
+        }
+      }
+
+      or_groups->insert(group);
+      return 0;
+    });
+
+    pass.post([comma_groups, colon_groups, or_groups](Node) {
+      comma_groups->clear();
+      colon_groups->clear();
+      or_groups->clear();
+      return 0;
+    });
+
+    return pass;
+  }
+
+  const auto wf_terms_tokens = wf_groups_tokens -
+    (Ref | Var | Scalar | Array | Object | Set | ArrayCompr | ObjectCompr |
+     SetCompr | Placeholder);
+
+  // clang-format off
+  const auto wf_terms =
+    wf_groups
+    | (Expr <<= Term | ExprParens | ExprCall)
+    | (Term <<= Ref | Var | Scalar | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr)
+    | (Some <<= Group++)
+    | (Group <<= wf_terms_tokens++)
+    ;
+  // clang-format on
+
+  const auto TermToken =
+    T(Ref, Var, Scalar, Array, Object, Set, ArrayCompr, ObjectCompr, SetCompr);
+
+  PassDef terms()
+  {
+    return {
+      "terms",
+      wf_terms,
+      dir::bottomup | dir::once,
+      {
+        In(Group) * TermToken[Term] >>
+          [](Match& _) { return Expr << (Term << _(Term)); },
+
+        In(Group) * T(Placeholder)[Placeholder] >>
+          [](Match& _) {
+            return Expr
+              << (Term << (Var ^ _.fresh(_(Placeholder)->location())));
+          },
+
+        In(Some) * T(Var)[Var] >>
+          [](Match& _) {
+            // because ExprSeq <<= Group++ at this point
+            return (Group << (Expr << (Term << _(Var))));
+          },
+      }};
+  }
+
+  const auto InfixOperatorToken =
+    T(Multiply,
+      Divide,
+      Modulo,
+      Add,
+      Subtract,
+      Equals,
+      NotEquals,
+      LessThan,
+      GreaterThan,
+      LessThanOrEquals,
+      GreaterThanOrEquals,
+      Assign,
+      Unify);
+
+  // clang-format off
+  const auto wf_unary =
+    wf_terms
+    | (Expr <<= Term | UnaryExpr | ExprParens | ExprCall)
+    | (UnaryExpr <<= Expr)
+    ;
+  // clang-format on
+
+  PassDef unary()
+  {
+    return {
+      "unary",
+      wf_unary,
+      dir::bottomup,
+      {
+        In(Group) * (Start * T(Subtract) * T(Expr)[Expr]) >>
+          [](Match& _) { return Expr << (UnaryExpr << _(Expr)); },
+
+        In(Group) * (InfixOperatorToken[Op] * T(Subtract) * T(Expr)[Expr]) >>
+          [](Match& _) {
+            return Seq << _(Op) << (Expr << (UnaryExpr << _(Expr)));
+          },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_arithbin_first =
+    wf_unary
+    | (Expr <<= Term | ExprInfix | ExprParens | ExprCall | UnaryExpr)
+    | (ExprInfix <<= Expr * InfixOperator * Expr)
+    | (InfixOperator <<= ArithOperator | BinOperator)
+    | (ArithOperator <<= Multiply | Divide | Modulo)
+    | (BinOperator <<= And)
+    ;
+  // clang-format on
+
+  PassDef arithbin_first()
+  {
+    return {
+      "arithbin_first",
+      wf_arithbin_first,
+      dir::bottomup,
+      {
+        In(Group) *
+            (T(Expr)[Lhs] * T(Multiply, Divide, Modulo)[Op] * T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (ArithOperator << _(Op)))
+                            << _(Rhs));
+          },
+
+        In(Group) * (T(Expr)[Lhs] * T(And)[Op] * T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (BinOperator << _(Op)))
+                            << _(Rhs));
+          },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_arithbin_second =
+    wf_arithbin_first
+    | (ArithOperator <<= Multiply | Divide | Modulo | Add | Subtract)
+    | (BinOperator <<= And | Or)
+    ;
+  // clang-format on
+
+  PassDef arithbin_second()
+  {
+    return {
+      "arithbin_second",
+      wf_arithbin_second,
+      dir::bottomup,
+      {
+        In(Group) * (T(Expr)[Lhs] * T(Add, Subtract)[Op] * T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (ArithOperator << _(Op)))
+                            << _(Rhs));
+          },
+
+        In(Group) *
+            (T(Expr)[Lhs] *
+             (T(Expr) << (T(Term) << (T(Scalar) << T(Int, "-.*")[Rhs])))) >>
+          [](Match& _) {
+            Location subtract_loc = _(Rhs)->location();
+            Location value_loc = subtract_loc;
+            value_loc.pos += 1;
+            value_loc.len -= 1;
+            subtract_loc.len = 1;
+            return Expr
+              << (ExprInfix
+                  << _(Lhs)
+                  << (InfixOperator
+                      << (ArithOperator << (Subtract ^ subtract_loc)))
+                  << (Expr << (Term << (Scalar << (Int ^ value_loc)))));
+          },
+
+        In(Group) *
+            (T(Expr)[Lhs] *
+             (T(Expr) << (T(Term) << (T(Scalar) << T(Float, "-.*")[Rhs])))) >>
+          [](Match& _) {
+            Location subtract_loc = _(Rhs)->location();
+            Location value_loc = subtract_loc;
+            value_loc.pos += 1;
+            value_loc.len -= 1;
+            subtract_loc.len = 1;
+            return Expr
+              << (ExprInfix
+                  << _(Lhs)
+                  << (InfixOperator
+                      << (ArithOperator << (Subtract ^ subtract_loc)))
+                  << (Expr << (Term << (Scalar << (Float ^ value_loc)))));
+          },
+
+        In(Group) * (T(Expr)[Lhs] * T(Or)[Op] * T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (BinOperator << _(Op)))
+                            << _(Rhs));
+          },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_comparison =
+    wf_arithbin_second
+    | (InfixOperator <<= ArithOperator | BinOperator | BoolOperator)
+    | (BoolOperator <<= Equals | NotEquals | LessThan | GreaterThan | LessThanOrEquals | GreaterThanOrEquals)
+    ;
+  // clang-format on
+
+  PassDef comparison()
+  {
+    return {
+      "comparison",
+      wf_comparison,
+      dir::bottomup,
+      {
+        In(Group) *
+            (T(Expr)[Lhs] *
+             T(Equals,
+               NotEquals,
+               LessThan,
+               GreaterThan,
+               LessThanOrEquals,
+               GreaterThanOrEquals)[Op] *
+             T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (BoolOperator << _(Op)))
+                            << _(Rhs));
+          },
+      }};
+  }
+
+  const auto wf_membership_tokens = (wf_terms_tokens | SomeDecl);
+
+  // clang-format off
+  const auto wf_membership =
+    wf_comparison
+    | (Term <<= Var | Ref | Scalar | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr | Membership)
+    | (Membership <<= ExprSeq * Expr)
+    | (SomeDecl <<= ExprSeq * (IsIn >>= Expr | Undefined))
+    | (Group <<= wf_membership_tokens++)
+    ;
+  // clang-format on
+
+  PassDef membership()
+  {
+    return {
+      "membership",
+      wf_membership,
+      dir::bottomup,
+      {
+        In(Group) *
+            ((T(Some) << End) * T(Expr)[Idx] * T(Comma) * T(Expr)[Val] *
+             T(IsIn) * T(Expr)[Expr]) >>
+          [](Match& _) {
+            return SomeDecl
+              << (ExprSeq << (Group << _(Idx)) << (Group << _(Val))) << _(Expr);
+          },
+
+        In(Group) *
+            ((T(Some) << End) * T(Expr)[Val] * T(IsIn) * T(Expr)[Expr]) >>
+          [](Match& _) {
+            return SomeDecl << (ExprSeq << (Group << _(Val))) << _(Expr);
+          },
+
+        In(Group) * (T(Some) << (T(Group)[Head] * T(Group)++[Tail] * End)) >>
+          [](Match& _) {
+            return SomeDecl << (ExprSeq << _(Head) << _[Tail]) << Undefined;
+          },
+
+        In(Group) *
+            (T(Expr)[Head] * (T(Comma) * T(Expr))++[Tail] * T(IsIn) *
+             T(Expr)[Expr]) >>
+          [](Match& _) {
+            Node exprseq = ExprSeq << (Group << _(Head));
+            for (auto it = _[Tail].begin(); it != _[Tail].end(); it += 2)
+            {
+              // skip the comma
+              exprseq << (Group << it[1]);
+            }
+
+            return Expr << (Term << (Membership << exprseq << _(Expr)));
+          },
+      }};
+  }
+
+  const auto wf_assign_tokens = wf_membership_tokens | ObjectItem;
+
+  // clang-format off
+  const auto wf_assign =
+    wf_membership
+    | (InfixOperator <<= ArithOperator | BinOperator | BoolOperator | AssignOperator)
+    | (AssignOperator <<= Assign | Unify)
+    | (ObjectItem <<= Expr * Expr)
+    | (Group <<= wf_assign_tokens++)
+    ;
+  // clang-format on
+
+  PassDef assign()
+  {
+    return {
+      "assign",
+      wf_assign,
+      dir::bottomup,
+      {
+        In(Group) * (T(Expr)[Lhs] * T(Assign, Unify)[Op] * T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprInfix << _(Lhs)
+                            << (InfixOperator << (AssignOperator << _(Op)))
+                            << _(Rhs));
+            ;
+          },
+
+        In(Group) * (T(Expr)[Key] * T(Colon) * T(Expr)[Val]) >>
+          [](Match& _) { return ObjectItem << _(Key) << _(Val); },
+      }};
+  }
+
+  const auto wf_else_not_tokens = wf_assign_tokens | NotExpr;
+
+  // clang-format off
+  const auto wf_else_not =
+    wf_assign
+    | (Else <<= Expr * Query)
+    | (NotExpr <<= Expr)
+    | (Group <<= wf_else_not_tokens++)
+    ;
+  // clang-format on
+
+  PassDef else_not()
+  {
+    return {
+      "else_not",
+      wf_else_not,
+      dir::bottomup | dir::once,
+      {
+        In(Group) *
+            (T(Else) * T(Assign, Unify) * T(Expr)[Expr] * ~T(If) *
+             T(Query)[Query]) >>
+          [](Match& _) { return Else << _(Expr) << _(Query); },
+
+        In(Group) *
+            (T(Else) * T(Assign, Unify) * T(Expr)[Expr] * T(If) * ~T(Not)[Not] *
+             T(Expr, SomeDecl)[Query]) >>
+          [](Match& _) {
+            Node query = _(Query);
+            if (_(Not) != nullptr)
+            {
+              query = NotExpr << query;
+            }
+
+            return Else << _(Expr) << (Query << (Group << query));
+          },
+
+        In(Group) * (T(Else) * T(Query)[Query]) >>
+          [](Match& _) {
+            return Else << (Expr << (Term << (Scalar << True))) << _(Query);
+          },
+
+        In(Group) * (T(Else) * T(Assign, Unify) * T(Expr)[Expr]) >>
+          [](Match& _) {
+            return Else << _(Expr)
+                        << (Query
+                            << (Group << (Expr << (Term << (Scalar << True)))));
+          },
+
+        In(Group) * (T(If)[If] * ~T(Not)[Not] * T(Expr, SomeDecl)[Query]) >>
+          [](Match& _) {
+            Node query = _(Query);
+            if (_(Not) != nullptr)
+            {
+              query = NotExpr << query;
+            }
+            return Seq << _(If) << (Query << (Group << query));
+          },
+
+        In(Group) * (T(Not) * T(Expr)[Expr]) >>
+          [](Match& _) { return NotExpr << _(Expr); },
+
+        // errors
+
+        T(Else)[Else] << End >>
+          [](Match& _) {
+            return err(_(Else), "Invalid else statement", WellFormedError);
+          },
+      }};
+  }
+
+  const auto wf_collections_tokens = wf_else_not_tokens -
+    (ObjectItem | Comma | Add | Subtract | Multiply | Divide | Modulo | And |
+     Or | Equals | NotEquals | LessThan | GreaterThan | LessThanOrEquals |
+     GreaterThanOrEquals | Assign | Unify);
+
+  // clang-format off
+  const auto wf_collections =
+    wf_else_not
+    | (Object <<= ObjectItem++)
+    | (Array <<= Expr++)
+    | (Set <<= Expr++)
+    | (ArrayCompr <<= Expr * Query)
+    | (SetCompr <<= Expr * Query)
+    | (ObjectCompr <<= Expr * Expr * Query)
+    | (ExprParens <<= Expr)
+    | (RefArgBrack <<= Expr | Placeholder)
+    | (Membership <<= ExprSeq * Expr)
+    | (ExprSeq <<= Expr++)
+    | (Group <<= wf_collections_tokens++)
+    ;
+
+  PassDef collections()
+  {
+    return {
+      "collections",
+      wf_collections,
+      dir::topdown | dir::once,
+      {
+        In(Object) * (T(Group) << (T(ObjectItem)[ObjectItem] * End)) >>
+          [](Match& _) { return _(ObjectItem); },
+
+        In(Array, Set, ExprSeq) * (T(Group) << (T(Expr)[Expr] * End)) >>
+          [](Match& _) { return _(Expr); },
+
+        In(Array, Set, ObjectItem) *
+            (T(Group)
+             << (T(Query)
+                 << (T(Group)
+                     << (T(Expr)[Head] * (T(Comma) * T(Expr))++[Tail] *
+                         End)))) >>
+          [](Match& _) {
+            Node set = Set << _(Head);
+            for (auto it = _[Tail].begin(); it != _[Tail].end(); it += 2)
+            {
+              // skip the comma
+              set << it[1];
+            }
+            return Expr << (Term << set);
+          },
+
+        In(RefArgBrack) * (T(Group) << (T(Expr)[Expr]) * End) >>
+          [](Match& _) { return _(Expr); },
+
+        In(ExprParens) * (T(Group) << (T(Expr)[Expr]) * End) >>
+          [](Match& _) { return _(Expr); },
+
+        In(ArrayCompr, SetCompr) * (T(Group) << (T(Expr)[Expr]) * End) >>
+          [](Match& _) { return _(Expr); },
+
+        In(ObjectCompr) *
+            (T(Group)
+             << ((T(ObjectItem) << (T(Expr)[Key] * T(Expr)[Val])) * End)) >>
+          [](Match& _) { return Seq << _(Key) << _(Val); },
+
+        T(Term)
+            << (T(Expr)
+                << (T(ExprParens) << (T(Expr) << (T(Term)[Term] * End)))) >>
+          [](Match& _) { return _(Term); },
+        
+        // errors
+
+        In(Group) * T(ObjectItem)[ObjectItem] >>
+          [](Match& _){
+            return err(_(ObjectItem), "Invalid object item", WellFormedError);
+          },
+        
+        In(Group) * T(Comma)[Comma] >>
+          [](Match& _){
+            return err(_(Comma), "Invalid comma", WellFormedError);
+          },
+
+        In(Group) * T(Add, Subtract, Multiply, Divide, Modulo)[Op] >>
+          [](Match& _){
+            return err(_(Op), "Invalid arithmetic operator", WellFormedError);
+          },
+
+        In(Group) * T(And, Or)[Op] >>
+          [](Match& _){
+            return err(_(Op), "Invalid set operator", WellFormedError);
+          },
+
+        In(Group) * T(Equals, NotEquals, LessThan, GreaterThan, LessThanOrEquals, GreaterThanOrEquals)[Op] >>
+          [](Match& _){
+            return err(_(Op), "Invalid boolean operator", WellFormedError);
+          },
+
+        In(Group) * T(Assign, Unify)[Op] >>
+          [](Match& _){
+            return err(_(Op), "Invalid assignment operator", WellFormedError);
+          },
+        
+        In(Array, Object, Set) * T(Group)[Group] >>
+          [](Match& _){
+            return err(_(Group), "Invalid collection", WellFormedError);
+          },
+
+        In(ExprParens) * T(Group)[Group] >>
+          [](Match& _){
+            return err(_(Group), "Invalid expression", WellFormedError);
+          },
+
+        In(ExprSeq) * T(Group)[Group] >>
+          [](Match& _){
+            return err(_(Group), "Invalid sequence", WellFormedError);
+          },
+
+        In(ArrayCompr, ObjectCompr, SetCompr) * T(Group)[Group] >>
+          [](Match& _){
+            return err(_(Group), "Invalid comprehension", WellFormedError);
+          },
+
+        In(RefArgBrack) * T(Group)[Group] >>
+          [](Match& _){
+            return err(_(Group), "Invalid reference argument", WellFormedError);
+          },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_lines =
+    wf_collections
+    | (Expr <<= Term | ExprInfix | ExprEvery | ExprParens | ExprCall | UnaryExpr)
+    | (ExprEvery <<= VarSeq * (IsIn >>= Term | ExprCall | ExprInfix) * Query)
+    | (VarSeq <<= Var++[1])
+    | (With <<= Term * Expr)
+    ;
+  // clang-format on
+
+  PassDef lines()
+  {
+    return {
+      "lines",
+      wf_lines,
+      dir::bottomup,
+      {
+        In(Group) *
+            (T(SomeDecl, Expr, Query, Else, With, NotExpr)[Lhs] *
+             T(SomeDecl, NotExpr, Default, Expr)[Rhs])(
+              [](auto& n) { return newline_between(n.front(), n.back()); }) >>
+          [](Match& _) { return Seq << _(Lhs) << NewLine << _(Rhs); },
+
+        In(Group) *
+            ((T(With) << End) * (T(Expr) << T(Term)[Term]) * T(As) *
+             T(Expr)[Expr]) >>
+          [](Match& _) { return With << _(Term) << _(Expr); },
+
+        In(Group) *
+            (T(Expr, NotExpr, Query, With, SomeDecl, Else)[Expr] * End) >>
+          [](Match& _) { return Seq << _(Expr) << NewLine; },
+
+        In(Group) *
+            (T(Every)[Every] * T(IsIn) *
+             (T(Expr) << T(Term, ExprCall, ExprInfix)[IsIn]) *
+             T(Query)[Query]) >>
+          [](Match& _) {
+            return Expr
+              << (ExprEvery << (VarSeq << *_[Every]) << _(IsIn) << _(Query));
+          },
+
+        // errors
+        T(With)[With] << End >>
+          [](Match& _) {
+            return err(_(With), "Invalid with statement", WellFormedError);
+          },
+      }};
+  }
+
+  const auto wf_rules_tokens = (wf_collections_tokens | Rule) - (If | Contains);
+
+  // clang-format off
+  const auto wf_rules =
+    wf_lines
+    | (Rule <<= (Default >>= True | False) * RuleHead * RuleBodySeq)
+    | (RuleHead <<= RuleRef * (RuleHeadType >>= RuleHeadComp | RuleHeadFunc | RuleHeadObj | RuleHeadSet))
+    | (RuleBodySeq <<= (Query | Else)++)
+    | (RuleHeadComp <<= Expr)
+    | (RuleHeadFunc <<= RuleArgs * Expr)
+    | (RuleHeadObj <<= Expr * Expr)
+    | (RuleHeadSet <<= Expr)
+    | (RuleRef <<= Ref | Var)
+    | (RuleArgs <<= Expr++)
+    | (Group <<= wf_rules_tokens++)
+    ;
+  // clang-format on
+
+  PassDef rules()
+  {
+    return {
+      "rules",
+      wf_rules,
+      dir::bottomup | dir::once,
+      {
+        In(Group)([](auto& n) { return in_parent(n, Module); }) *
+            ((T(Expr)
+              << (T(ExprInfix)
+                  << ((T(Expr)
+                       << (T(Term)
+                           << (T(Ref)
+                               << ((T(RefHead) << T(Var)[RuleRef]) *
+                                   (T(RefArgSeq)[RefArgSeq]([](auto& n) {
+                                     Node refargseq = n.front();
+                                     return !refargseq->empty() &&
+                                       refargseq->back()->type() == RefArgBrack;
+                                   })))))) *
+                      (T(InfixOperator) << T(AssignOperator)) *
+                      T(Expr)[Val]))) *
+             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [](Match& _) {
+            Node refargseq = _(RefArgSeq);
+            Node key = refargseq->pop_back()->front();
+            Node ruleref = _(RuleRef);
+            if (!refargseq->empty())
+            {
+              ruleref = Ref << (RefHead << ruleref) << refargseq;
+            }
+            return Rule << False
+                        << (RuleHead << (RuleRef << ruleref)
+                                     << (RuleHeadObj << key << _(Val)))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        In(Group)([](auto& n) { return in_parent(n, Module); }) *
+            ((T(Expr)
+              << (T(Term)
+                  << (T(Ref)
+                      << ((T(RefHead) << T(Var)[RuleRef]) *
+                          (T(RefArgSeq)[RefArgSeq]([](auto& n) {
+                            Node refargseq = n.front();
+                            return !refargseq->empty() &&
+                              refargseq->back()->type() == RefArgBrack;
+                          })))))) *
+             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [](Match& _) {
+            Node refargseq = _(RefArgSeq);
+            Node value = refargseq->pop_back()->front();
+            Node ruleref = _(RuleRef);
+            if (!refargseq->empty())
+            {
+              ruleref = Ref << (RefHead << ruleref) << refargseq;
+            }
+
+            auto defs = _(RuleRef)->scope()->lookdown({"contains"});
+            Node rulehead;
+            if (defs.empty())
+            {
+              rulehead = RuleHead << (RuleRef << ruleref)
+                                  << (RuleHeadSet << value);
+            }
+            else
+            {
+              rulehead = RuleHead
+                << (RuleRef << ruleref)
+                << (RuleHeadObj << value
+                                << (Expr << (Term << (Scalar << True))));
+            }
+
+            return Rule << False << rulehead << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        In(Group)([](auto& n) { return in_parent(n, Module); }) *
+            (~T(Default)[Default] *
+             (T(Expr)
+              << (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& _) {
+            return Rule << (_(Default) != nullptr ? True : False)
+                        << (RuleHead << (RuleRef << _(RuleRef))
+                                     << (RuleHeadComp << _(Expr)))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        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& _) {
+            return Rule << (_(Default) != nullptr ? True : False)
+                        << (RuleHead
+                            << (RuleRef << _(RuleRef))
+                            << (RuleHeadComp
+                                << (Expr
+                                    << (Term << (Scalar << (True ^ "true"))))))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        In(Group)([](auto& n) { return in_parent(n, Module); }) *
+            (~T(Default)[Default] *
+             (T(Expr)
+              << (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& _) {
+            return Rule << (_(Default) != nullptr ? True : False)
+                        << (RuleHead
+                            << (RuleRef << _(RuleRef))
+                            << (RuleHeadFunc << (RuleArgs << *_[RuleArgs])
+                                             << _(Expr)))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        In(Group)([](auto& n) { return in_parent(n, Module); }) *
+            (~T(Default)[Default] *
+             (T(Expr)
+              << (T(ExprCall) << (T(Ref)[RuleRef] * T(ExprSeq)[RuleArgs]))) *
+             ~T(If) * T(Query, Else)++[RuleBodySeq] * T(NewLine)) >>
+          [](Match& _) {
+            return Rule << (_(Default) != nullptr ? True : False)
+                        << (RuleHead
+                            << (RuleRef << _(RuleRef))
+                            << (RuleHeadFunc
+                                << (RuleArgs << *_[RuleArgs])
+                                << (Expr
+                                    << (Term << (Scalar << (True ^ "true"))))))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        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(NewLine)) >>
+          [](Match& _) {
+            return Rule << False
+                        << (RuleHead << (RuleRef << _(RuleRef))
+                                     << (RuleHeadSet << _(Expr)))
+                        << (RuleBodySeq << _[RuleBodySeq]);
+          },
+
+        In(Group) * T(If, Contains)[Op] >>
+          [](Match& _) {
+            return err(_(Op), "Invalid rule head", WellFormedError);
+          },
+      }};
+  }
+
+  const auto wf_literals_tokens = (wf_rules_tokens | Literal) -
+    (Expr | NotExpr | SomeDecl | With | NewLine | Keyword);
+
+  // clang-format off
+  const auto wf_literals =
+    wf_rules
+    | (Literal <<= (Expr >>= Expr | NotExpr | SomeDecl) * WithSeq)
+    | (WithSeq <<= With++)
+    | (Group <<= wf_literals_tokens++)
+    ;
+  // clang-format on
+
+  PassDef literals()
+  {
+    return {
+      "literals",
+      wf_literals,
+      dir::bottomup | dir::once,
+      {
+        In(Group) * (T(SomeDecl)[SomeDecl] * T(With)++[WithSeq] * T(NewLine)) >>
+          [](Match& _) {
+            return Literal << _(SomeDecl) << (WithSeq << _[WithSeq]);
+          },
+
+        In(Group) *
+            (T(Expr, NotExpr)[Expr] * T(With)++[WithSeq] * T(NewLine)) >>
+          [](Match& _) {
+            return Literal << _(Expr) << (WithSeq << _[WithSeq]);
+          },
+
+        In(Term) *
+            (T(Ref) << ((T(RefHead) << T(Var)[Var]) * (T(RefArgSeq) << End))) >>
+          [](Match& _) { return _(Var); },
+
+        T(Keyword, NewLine) >> [](Match&) -> Node { return nullptr; },
+
+        In(Group) * T(Expr, NotExpr, SomeDecl, With)[Val] >>
+          [](Match& _) {
+            return err(_(Val), "Invalid literal", WellFormedError);
+          },
+      }};
+  }
+
+  PassDef structure()
+  {
+    return {
+      "structure",
+      rego::wf,
+      dir::bottomup | dir::once,
+      {
+        In(Module) *
+            (T(Package)[Package] * T(Import)++[ImportSeq] *
+             (T(Group) << (T(Rule)++[Policy] * End))) >>
+          [](Match& _) {
+            return Seq << _(Package) << (ImportSeq << _[ImportSeq])
+                       << (Policy << _[Policy]);
+          },
+
+        In(Module) * (T(Package)[Package] * T(Import)++[ImportSeq] * End) >>
+          [](Match& _) {
+            return Seq << _(Package) << (ImportSeq << _[ImportSeq]) << Policy;
+          },
+
+        In(Query) * (T(Group) << (T(Literal)++[Query] * End)) >>
+          [](Match& _) { return Seq << _[Query]; },
+
+        In(RuleRef) * T(Var)[Var] >>
+          [](Match& _) { return Ref << (RefHead << _(Var)) << RefArgSeq; },
+
+        In(RuleArgs) * (T(Expr) << T(Term)[Term]) >>
+          [](Match& _) { return _(Term); },
+
+        In(Expr) * (T(Expr) << (T(Term)[Term] * End)) >>
+          [](Match& _) { return _(Term); },
+
+        In(TermSeq) * (T(Expr) << T(Term)[Term]) >>
+          [](Match& _) { return _(Term); },
+
+        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
+              << (Expr << (Term << (Scalar << (True ^ "true"))));
+          },
+
+        // errors
+        T(Group)[Group] >>
+          [](Match& _) {
+            return err(_(Group), "Syntax error", WellFormedError);
+          },
+
+        T(Module)[Module] << (T(Import, Group)++ * End) >>
+          [](Match& _) {
+            return err(
+              _(Module), "Missing package declaration", WellFormedError);
+          },
+
+        T(Module)[Module] << T(Package) * T(Import)++ * T(Group) *
+              T(Import)[Import] >>
+          [](Match& _) {
+            return err(
+              _(Import),
+              "Import statements must come before rules",
+              WellFormedError);
+          },
+      }};
+  }
+
+  // clang-format off
+  const auto wf_input =
+    (Top <<= Input)
+    | (Input <<= DataTerm)
+    | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
+    | (DataArray <<= DataTerm++)
+    | (DataSet <<= DataTerm++)
+    | (DataObject <<= DataObjectItem++)
+    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
+    | (Scalar <<= Int | Float | True | False | Null | String)
+    | (String <<= JSONString | RawString)
+    ;
+  // clang-format on
+
+  PassDef to_input_()
+  {
+    return {
+      "to_input",
+      wf_input,
+      dir::bottomup | dir::once,
+      {
+        In(Top) * (T(Query) << (T(Literal)[Literal] * End)) >>
+          [](Match& _) { return Input << _(Literal)->front(); },
+
+        T(Expr) << (T(Term) << T(Scalar)[Scalar]) >>
+          [](Match& _) { return DataTerm << _(Scalar); },
+
+        T(Expr) << (T(Term) << T(Array)[Array]) >>
+          [](Match& _) { return DataTerm << (DataArray << *_[Array]); },
+
+        T(Expr) << (T(Term) << T(Object)[Object]) >>
+          [](Match& _) { return DataTerm << (DataObject << *_[Object]); },
+
+        T(Expr) << (T(Term) << T(Set)[Set]) >>
+          [](Match& _) { return DataTerm << (DataSet << *_[Set]); },
+
+        T(ObjectItem)[ObjectItem] >>
+          [](Match& _) { return DataObjectItem << *_[ObjectItem]; },
+      }};
+  }
+}
+
+namespace rego
+{
+  Reader reader()
+  {
+    return {
+      "rego",
+      {
+        prep(),           keywords(),        some_every(),  ref_args(),
+        refs(),           groups(),          terms(),       unary(),
+        arithbin_first(), arithbin_second(), comparison(),  membership(),
+        assign(),         else_not(),        collections(), lines(),
+        rules(),          literals(),        structure(),
+      },
+      parser(),
+    };
+  }
+
+  Rewriter to_input()
+  {
+    return {
+      "to_input",
+      {
+        to_input_(),
+      },
+      rego::wf};
+  }
+}
\ No newline at end of file
diff --git a/src/rego.cc b/src/rego.cc
index 934e5879..6a6c57d6 100644
--- a/src/rego.cc
+++ b/src/rego.cc
@@ -1,58 +1,12 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace logging = trieste::logging;
 
 namespace rego
 {
-  std::vector<Pass> passes(const BuiltIns& builtins)
+  std::string set_log_level_from_string(const std::string& level)
   {
-    return {
-      input_data(),
-      modules(),
-      imports(),
-      keywords(),
-      lists(),
-      ifs(),
-      elses(),
-      rules(),
-      build_calls(),
-      membership(),
-      build_refs(),
-      structure(),
-      strings(),
-      merge_data(),
-      lift_refheads(),
-      symbols(),
-      replace_argvals(),
-      lift_query(),
-      expand_imports(),
-      constants(),
-      explicit_enums(),
-      body_locals(builtins),
-      value_locals(builtins),
-      compr_locals(builtins),
-      rules_to_compr(),
-      compr(),
-      absolute_refs(),
-      merge_modules(),
-      datarule(),
-      skips(),
-      unary(),
-      multiply_divide(),
-      add_subtract(),
-      comparison(),
-      assign(builtins),
-      skip_refs(builtins),
-      simple_refs(),
-      init(),
-      implicit_enums(),
-      enum_locals(),
-      rulebody(),
-      lift_to_rule(),
-      functions(),
-      unify(builtins),
-      query(),
-    };
+    return logging::set_log_level_from_string(level);
   }
 
   void set_log_level(LogLevel level)
@@ -81,6 +35,20 @@ namespace rego
       case LogLevel::Trace:
         logging::set_level<logging::Trace>();
         break;
+      default:
+        throw std::runtime_error("Unknown log level");
     }
   }
+
+  std::vector<Pass> passes(BuiltIns builtins)
+  {
+    auto reader_passes = reader().passes();
+    auto unify_passes = unify(builtins).passes();
+    std::vector<Pass> all_passes;
+    all_passes.insert(
+      all_passes.end(), reader_passes.begin(), reader_passes.end());
+    all_passes.insert(
+      all_passes.end(), unify_passes.begin(), unify_passes.end());
+    return all_passes;
+  }
 }
\ No newline at end of file
diff --git a/src/rego_c.cc b/src/rego_c.cc
index f0f3acc0..9de94283 100644
--- a/src/rego_c.cc
+++ b/src/rego_c.cc
@@ -23,7 +23,7 @@ extern "C"
     return reinterpret_cast<rego::Interpreter*>(rego)->m_c_error.c_str();
   }
 
-  void regoSetLogLevel(regoEnum level)
+  regoEnum regoSetLogLevel(regoEnum level)
   {
     switch (level)
     {
@@ -54,7 +54,23 @@ extern "C"
       case REGO_LOG_LEVEL_TRACE:
         rego::set_log_level(rego::LogLevel::Trace);
         break;
+
+      default:
+        return REGO_ERROR_INVALID_LOG_LEVEL;
     }
+
+    return REGO_OK;
+  }
+
+  regoEnum regoSetLogLevelFromString(const char* level)
+  {
+    std::string error = rego::set_log_level_from_string(level);
+    if (error.empty())
+    {
+      return REGO_OK;
+    }
+
+    return REGO_ERROR_INVALID_LOG_LEVEL;
   }
 
   regoInterpreter* regoNew()
@@ -148,10 +164,18 @@ extern "C"
 
   regoEnum regoSetInputJSON(regoInterpreter* rego, const char* contents)
   {
+    logging::Warn()
+      << "regoSetInputJSON is deprecated. Please use regoSetInputTerm instead.";
     logging::Debug() << "regoSetInputJSON: " << contents;
+    return regoSetInputTerm(rego, contents);
+  }
+
+  regoEnum regoSetInputTerm(regoInterpreter* rego, const char* contents)
+  {
+    logging::Debug() << "regoSetInputTerm: " << contents;
     try
     {
-      reinterpret_cast<rego::Interpreter*>(rego)->set_input_json(contents);
+      reinterpret_cast<rego::Interpreter*>(rego)->set_input_term(contents);
       return REGO_OK;
     }
     catch (const std::exception& e)
@@ -192,15 +216,13 @@ extern "C"
     regoInterpreter* rego, regoBoolean enabled)
   {
     logging::Debug() << "regoSetWellFormedChecksEnabled: " << enabled;
-    reinterpret_cast<rego::Interpreter*>(rego)->well_formed_checks_enabled(
-      enabled);
+    reinterpret_cast<rego::Interpreter*>(rego)->wf_check_enabled(enabled);
   }
 
   regoBoolean regoGetWellFormedChecksEnabled(regoInterpreter* rego)
   {
     logging::Debug() << "regoGetWellFormedChecksEnabled";
-    return reinterpret_cast<rego::Interpreter*>(rego)
-      ->well_formed_checks_enabled();
+    return reinterpret_cast<rego::Interpreter*>(rego)->wf_check_enabled();
   }
 
   regoOutput* regoQuery(regoInterpreter* rego, const char* query_expr)
@@ -226,7 +248,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);
   }
 
@@ -235,7 +257,7 @@ extern "C"
     logging::Debug() << "regoGetStrictBuiltInErrors";
     return reinterpret_cast<rego::Interpreter*>(rego)
       ->builtins()
-      .strict_errors();
+      ->strict_errors();
   }
 
   // Output functions
@@ -263,17 +285,16 @@ extern "C"
       return nullptr;
     }
 
-    for (auto binding : *node_ptr)
+    assert(node_ptr->type() == rego::Result);
+    trieste::WFContext context(rego::wf_result);
+    auto defs = node_ptr->lookdown({name});
+    if (defs.empty())
     {
-      rego::Node var = binding / rego::Var;
-      if (var->location().view() == name)
-      {
-        rego::Node val = binding / rego::Term;
-        return reinterpret_cast<regoNode*>(val.get());
-      }
+      return nullptr;
     }
 
-    return nullptr;
+    rego::Node val = defs[0] / rego::Term;
+    return reinterpret_cast<regoNode*>(val.get());
   }
 
   const char* regoOutputString(regoOutput* output)
@@ -449,15 +470,18 @@ extern "C"
   {
     logging::Debug() << "regoNodeJSONSize";
     auto node_ptr = reinterpret_cast<trieste::NodeDef*>(node);
-    std::string json = rego::to_json(node_ptr->shared_from_this());
+    trieste::WFContext context(rego::wf_result);
+    std::string json = rego::to_key(node_ptr->shared_from_this(), true);
     return static_cast<regoSize>(json.size() + 1);
   }
 
   regoEnum regoNodeJSON(regoNode* node, char* buffer, regoSize size)
   {
     logging::Debug() << "regoNodeJSON: " << buffer << "[" << size << "]";
+
     auto node_ptr = reinterpret_cast<trieste::NodeDef*>(node);
-    std::string json = rego::to_json(node_ptr->shared_from_this());
+    trieste::WFContext context(rego::wf_result);
+    std::string json = rego::to_key(node_ptr->shared_from_this(), true);
     if (size < json.size() + 1)
     {
       return REGO_ERROR_BUFFER_TOO_SMALL;
diff --git a/src/resolver.cc b/src/resolver.cc
index 21260c6f..a395e599 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -349,11 +349,11 @@ namespace rego
     {
       Node lhs_number = maybe_lhs_number.node;
       Node rhs_number = maybe_rhs_number.node;
-      if (
-        lhs_number->type() == Int && rhs_number->type() == Int &&
-        op->type() != Divide)
+      auto lhs_int = try_get_int(lhs_number);
+      auto rhs_int = try_get_int(rhs_number);
+      if (lhs_int.has_value() && rhs_int.has_value() && op->type() != Divide)
       {
-        return do_arith(op, get_int(lhs_number), get_int(rhs_number));
+        return do_arith(op, *lhs_int, *rhs_int);
       }
       else
       {
@@ -374,9 +374,12 @@ namespace rego
         return err(rhs, "operand 2 must be number but got set", EvalTypeError);
       }
 
-      return err(
-        op->parent()->shared_from_this(),
-        "Cannot perform arithmetic operations on non-numeric values");
+      if (maybe_lhs_set.success && maybe_rhs_number.success)
+      {
+        return err(rhs, "operand 2 must be set but got number", EvalTypeError);
+      }
+
+      return Undefined;
     }
   }
 
@@ -404,9 +407,7 @@ namespace rego
     }
     else
     {
-      return err(
-        op->parent()->shared_from_this(),
-        "Cannot perform set operations on non-set values");
+      return Undefined;
     }
   }
 
@@ -445,7 +446,7 @@ namespace rego
     }
     else
     {
-      return do_bool(op, to_json(lhs), to_json(rhs));
+      return do_bool(op, to_key(lhs), to_key(rhs));
     }
   }
 
@@ -470,9 +471,10 @@ namespace rego
         index = index->front();
       }
 
-      if (index->type() == Int)
+      auto maybe_i = try_get_int(index);
+      if (maybe_i.has_value())
       {
-        auto i = get_int(index).to_size();
+        auto i = maybe_i->to_size();
         if (i < container->size())
         {
           Node value = container->at(i);
@@ -496,23 +498,21 @@ namespace rego
       return object_lookdown(container, query);
     }
 
-    if (
-      container->type() == Data || container->type() == DataItem ||
-      container->type() == Submodule)
+    if (container->type() == Data || container->type() == Submodule)
     {
       Node key = arg->front();
       std::string key_str = std::string((container / Key)->location().view());
-      key_str += "." + strip_quotes(to_json(key));
+      key_str += "." + strip_quotes(to_key(key));
       return module_lookdown(container, key_str);
     }
 
     if (container->type() == Set)
     {
       std::set<std::string> reprs;
-      std::string query_repr = to_json(arg);
+      std::string query_repr = to_key(arg);
       for (Node member : *container)
       {
-        std::string repr = to_json(member);
+        std::string repr = to_key(member);
         if (repr == query_repr)
         {
           return Nodes({to_term(arg)});
@@ -561,11 +561,11 @@ namespace rego
         return object_items->at(i);
       }
 
-      std::string key = to_json(object_items->at(i));
+      std::string key = to_key(object_items->at(i));
       if (contains(items, key))
       {
-        std::string current = to_json(items[key] / Val);
-        std::string next = to_json(object_items->at(i + 1));
+        std::string current = to_key(items[key] / Val);
+        std::string next = to_key(object_items->at(i + 1));
         if (current != next)
         {
           if (is_rule)
@@ -630,7 +630,7 @@ namespace rego
         throw std::runtime_error("Not implemented");
       }
 
-      std::string repr = to_json(member);
+      std::string repr = to_key(member);
       if (!contains(members, repr))
       {
         members[repr] = to_term(member);
@@ -661,12 +661,12 @@ namespace rego
     std::set<std::string> values;
     for (auto term : *lhs)
     {
-      values.insert(to_json(term));
+      values.insert(to_key(term));
     }
 
     for (auto term : *rhs)
     {
-      if (contains(values, to_json(term)))
+      if (contains(values, to_key(term)))
       {
         set->push_back(term->clone());
       }
@@ -690,12 +690,12 @@ namespace rego
     std::map<std::string, Node> members;
     for (auto term : *lhs)
     {
-      members[to_json(term)] = term;
+      members[to_key(term)] = term;
     }
 
     for (auto term : *rhs)
     {
-      std::string key = to_json(term);
+      std::string key = to_key(term);
       if (!contains(members, key))
       {
         members[key] = term;
@@ -727,12 +727,12 @@ namespace rego
     std::set<std::string> values;
     for (auto term : *rhs)
     {
-      values.insert(to_json(term));
+      values.insert(to_key(term));
     }
 
     for (auto term : *lhs)
     {
-      if (!contains(values, to_json(term)))
+      if (!contains(values, to_key(term)))
       {
         set->push_back(term->clone());
       }
@@ -778,7 +778,7 @@ namespace rego
 
   void Resolver::term_str(logging::Log& log, const Node& node)
   {
-    log << node->type().str() << "(" << to_json(node) << ")";
+    log << node->type().str() << "(" << to_key(node) << ")";
   }
 
   void Resolver::with_str(logging::Log& os, const Node& unifyexprwith)
@@ -905,7 +905,7 @@ namespace rego
     }
     else
     {
-      os << to_json(arg);
+      os << to_key(arg);
     }
   }
 
@@ -960,8 +960,8 @@ namespace rego
       Node arg = args[i]->clone();
       if (rulearg->type() == ArgVal)
       {
-        std::string rulearg_str = to_json(rulearg->front());
-        std::string arg_str = to_json(arg);
+        std::string rulearg_str = to_key(rulearg->front());
+        std::string arg_str = to_key(arg);
         if (rulearg_str != arg_str)
         {
           return Undefined;
@@ -980,9 +980,7 @@ namespace rego
     const Node& container, const std::string& query)
   {
     Node module = container;
-    if (
-      module->type() == Submodule || module->type() == Data ||
-      module->type() == DataItem)
+    if (module->type() == Submodule || module->type() == Data)
     {
       module = module / Val;
     }
@@ -1027,7 +1025,7 @@ namespace rego
   Nodes Resolver::object_lookdown(const Node& object, const Node& query)
   {
     Nodes terms;
-    std::string query_str = to_json(query);
+    std::string query_str = to_key(query);
     for (auto& object_item : *object)
     {
       Node key = object_item / Key;
@@ -1036,7 +1034,7 @@ namespace rego
         throw std::runtime_error("Not implemented.");
       }
 
-      std::string key_str = to_json(key);
+      std::string key_str = to_key(key);
       if (key_str == query_str)
       {
         terms.push_back((object_item / Val)->clone());
@@ -1063,7 +1061,7 @@ namespace rego
           Nodes defs = result->lookdown(var->location());
           for (auto& def : defs)
           {
-            if (def->type() == DataItem || def->type() == Submodule)
+            if (def->type() == Submodule)
             {
               new_results.push_back(def / Val);
             }
@@ -1086,7 +1084,7 @@ namespace rego
     std::set<std::string> values;
     for (Node term : *termset)
     {
-      std::string repr = to_json(term);
+      std::string repr = to_key(term);
       if (!contains(values, repr))
       {
         values.insert(repr);
@@ -1102,7 +1100,7 @@ namespace rego
     return reduce;
   }
 
-  Node Resolver::resolve_query(const Node& query, const BuiltIns& builtins)
+  Node Resolver::resolve_query(const Node& query, BuiltIns builtins)
   {
     Nodes defs = query->front()->lookup();
     if (defs.size() != 1)
@@ -1211,18 +1209,18 @@ namespace rego
     std::vector<std::string> indices;
     if (items->type() == Array || items->type() == Set)
     {
-      indices = array_find(items, to_json(item));
+      indices = array_find(items, to_key(item));
     }
     else if (items->type() == Object)
     {
-      indices = object_find(items, to_json(item));
+      indices = object_find(items, to_key(item));
     }
     else
     {
       return False ^ "false";
     }
 
-    std::string index_str = to_json(index);
+    std::string index_str = to_key(index);
     for (auto& i : indices)
     {
       if (i == index_str)
@@ -1245,11 +1243,11 @@ namespace rego
     std::vector<std::string> indices;
     if (items->type() == Array || items->type() == Set)
     {
-      indices = array_find(items, to_json(item));
+      indices = array_find(items, to_key(item));
     }
     else if (items->type() == Object)
     {
-      indices = object_find(items, to_json(item));
+      indices = object_find(items, to_key(item));
     }
     else
     {
@@ -1271,7 +1269,7 @@ namespace rego
     for (std::size_t i = 0; i < array->size(); ++i)
     {
       Node item = array->at(i);
-      if (to_json(item) == search)
+      if (to_key(item) == search)
       {
         indices.push_back(std::to_string(i));
       }
@@ -1286,9 +1284,9 @@ namespace rego
     std::vector<std::string> indices;
     for (auto& objectitem : *object)
     {
-      if (to_json(objectitem / Val) == search)
+      if (to_key(objectitem / Val) == search)
       {
-        indices.push_back(to_json(objectitem / Key));
+        indices.push_back(to_key(objectitem / Key));
       }
     }
 
@@ -1358,7 +1356,7 @@ namespace rego
         Node value = current / Val;
         if (value->type() == Term)
         {
-          os << to_json(value) << std::endl;
+          os << to_key(value) << std::endl;
         }
         else
         {
@@ -1460,11 +1458,11 @@ namespace rego
       Node query = JSONString ^ prefix;
 
       bool found = false;
-      std::string query_str = to_json(query);
+      std::string query_str = to_key(query);
       for (auto& object_item : *current)
       {
         Node key = object_item / Key;
-        std::string key_str = to_json(key);
+        std::string key_str = to_key(key);
         if (key_str == query_str)
         {
           current = object_item / Val;
@@ -1501,12 +1499,12 @@ namespace rego
     }
 
     Node key = (Scalar << (JSONString ^ path.substr(start)));
-    std::string key_str = to_json(key);
+    std::string key_str = to_key(key);
     Node existing = nullptr;
     for (auto& object_item : *current)
     {
       Node item_key = object_item / Key;
-      std::string item_key_str = to_json(item_key);
+      std::string item_key_str = to_key(item_key);
       if (key_str == item_key_str)
       {
         existing = object_item;
diff --git a/src/unifier.cc b/src/unifier.cc
index dbaa3f84..7bdabe77 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
@@ -9,7 +9,7 @@ namespace rego
     const Node& rulebody,
     CallStack call_stack,
     WithStack with_stack,
-    const BuiltIns& builtins,
+    BuiltIns builtins,
     UnifierCache cache) :
     m_rule(rule),
     m_call_stack(call_stack),
@@ -676,8 +676,8 @@ namespace rego
               }
               else if (new_result->rank() == old_result->rank())
               {
-                std::string old_str = to_json(result->to_term());
-                std::string new_str = to_json(maybe_result.value()->to_term());
+                std::string old_str = to_key(result->to_term());
+                std::string new_str = to_key(maybe_result.value()->to_term());
                 if (old_str == "undefined" && new_str != "undefined")
                 {
                   result = maybe_result.value();
@@ -924,18 +924,6 @@ namespace rego
         {
           values.push_back(ValueDef::create(def));
         }
-        else if (def->type() == DataItem)
-        {
-          Node value = def / Val;
-          if (value->type() == DataModule)
-          {
-            values.push_back(ValueDef::create(def));
-          }
-          else
-          {
-            values.push_back(ValueDef::create(value));
-          }
-        }
         else
         {
           values.push_back(
@@ -1144,9 +1132,9 @@ namespace rego
       std::back_inserter(function_args),
       [](auto& arg) { return arg->node(); });
 
-    if (m_builtins.is_builtin(function->location()))
+    if (m_builtins->is_builtin(function->location()))
     {
-      Node node = m_builtins.call(function->location(), function_args);
+      Node node = m_builtins->call(function->location(), function_args);
       return ValueDef::create(var, node, sources);
     }
 
@@ -1690,8 +1678,8 @@ namespace rego
         }
         else if (index == rank)
         {
-          std::string result_str = to_json(result);
-          std::string value_str = to_json(value);
+          std::string result_str = to_key(result);
+          std::string value_str = to_key(value);
           if (result_str != value_str)
           {
             return err(
@@ -2057,7 +2045,7 @@ namespace rego
     const Node& rulebody,
     const CallStack& call_stack,
     const WithStack& with_stack,
-    const BuiltIns& builtins,
+    BuiltIns builtins,
     const UnifierCache& cache)
   {
     if (contains(cache, key))
diff --git a/src/unify.cc b/src/unify.cc
new file mode 100644
index 00000000..e6fd9748
--- /dev/null
+++ b/src/unify.cc
@@ -0,0 +1,204 @@
+#include "unify.hh"
+
+namespace rego
+{
+  bool is_undefined(const Node& node)
+  {
+    if (node->type() == DataModule)
+    {
+      return false;
+    }
+
+    if (node->type() == Undefined)
+    {
+      return true;
+    }
+
+    for (auto& child : *node)
+    {
+      if (is_undefined(child))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool is_truthy(const Node& node)
+  {
+    assert(node->type() == Term || node->type() == TermSet);
+    if (node->type() == TermSet)
+    {
+      return true;
+    }
+
+    Node value = node->front();
+    if (value->type() == Scalar)
+    {
+      value = value->front();
+      return value->type() != False;
+    }
+
+    if (
+      value->type() == Object || value->type() == Array || value->type() == Set)
+    {
+      return true;
+    }
+
+    return false;
+  }
+
+  bool is_module(const Node& var)
+  {
+    return var->type().in({Submodule, Data});
+  }
+
+  bool in_query(const Node& node)
+  {
+    if (node->type() == Rego)
+    {
+      return false;
+    }
+
+    if (node->type() == RuleComp)
+    {
+      std::string name = std::string((node / Var)->location().view());
+      return name.find("query$") != std::string::npos;
+    }
+
+    return in_query(node->parent()->shared_from_this());
+  }
+
+  bool contains_local(const Node& node)
+  {
+    if (node->type() == NestedBody)
+    {
+      return false;
+    }
+
+    if (node->type() == Var)
+    {
+      Nodes defs = node->lookup();
+      if (defs.size() == 0)
+      {
+        // check if it is a temporary variable
+        // (which may not yet be in the symbol table)
+        return node->location().view().find('$') != std::string::npos;
+      }
+      return defs.size() == 1 && defs[0]->type() == Local;
+    }
+
+    for (auto& child : *node)
+    {
+      if (contains_local(child))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool contains_ref(const Node& node)
+  {
+    if (node->type() == NestedBody)
+    {
+      return false;
+    }
+
+    if (node->type() == Ref || node->type() == Var)
+    {
+      return true;
+    }
+
+    for (auto& child : *node)
+    {
+      if (contains_ref(child))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  Node expr_infix(Token op_token, Node lhs, Node rhs)
+  {
+    if (lhs != Expr)
+    {
+      lhs = Expr << lhs;
+    }
+
+    if (rhs != Expr)
+    {
+      rhs = Expr << rhs;
+    }
+
+    Node op;
+    if (op_token.in({Add, Subtract, Multiply, Divide, Modulo}))
+    {
+      op = InfixOperator << (ArithOperator << op_token);
+    }
+    else if (op_token.in(
+               {Equals,
+                NotEquals,
+                LessThan,
+                LessThanOrEquals,
+                GreaterThan,
+                GreaterThanOrEquals}))
+    {
+      op = InfixOperator << (BoolOperator << op_token);
+    }
+    else if (op_token.in({Assign, Unify}))
+    {
+      op = InfixOperator << (AssignOperator << op_token);
+    }
+    else
+    {
+      op = err(op_token, "Unsupported infix operator");
+    }
+
+    return ExprInfix << lhs << op << rhs;
+  }
+
+  Rewriter unify(BuiltIns builtins)
+  {
+    return {
+      "unify",
+      {
+        strings(),
+        merge_data(),
+        lift_refheads(),
+        symbols(),
+        replace_argvals(),
+        lift_query(),
+        expand_imports(builtins),
+        constants(),
+        explicit_enums(),
+        body_locals(builtins),
+        value_locals(builtins),
+        compr_locals(builtins),
+        rules_to_compr(),
+        compr(),
+        absolute_refs(),
+        merge_modules(),
+        datarule(),
+        skips(),
+        infix(),
+        assign(builtins),
+        skip_refs(builtins),
+        simple_refs(),
+        init(),
+        implicit_enums(),
+        enum_locals(),
+        rulebody(),
+        lift_to_rule(),
+        functions(),
+        unifier(builtins),
+        result(),
+      },
+      wf_unify_input,
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/unify.hh b/src/unify.hh
new file mode 100644
index 00000000..ca718e0e
--- /dev/null
+++ b/src/unify.hh
@@ -0,0 +1,729 @@
+#pragma once
+
+#include "internal.hh"
+#include "rego.hh"
+
+namespace rego
+{
+  inline const auto Rego = TokenDef("rego", flag::symtab);
+  inline const auto Data = TokenDef("rego-data", flag::lookup);
+  inline const auto DataSeq = TokenDef("rego-dataseq");
+  inline const auto ModuleSeq = TokenDef("rego-moduleseq");
+  inline const auto Submodule =
+    TokenDef("rego-submodule", flag::lookdown | flag::lookup);
+
+  inline const auto RuleComp = TokenDef(
+    "rego-rulecomp",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleFunc = TokenDef(
+    "rego-rulefunc",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleSet = TokenDef(
+    "rego-ruleset",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto RuleObj = TokenDef(
+    "rego-ruleobj",
+    flag::symtab | flag::lookup | flag::lookdown | flag::defbeforeuse);
+  inline const auto DefaultRule =
+    TokenDef("rego-defaultrule", flag::lookup | flag::lookdown);
+  inline const auto Local =
+    TokenDef("rego-local", flag::lookup | flag::shadowing);
+  inline const auto ArithInfix = TokenDef("rego-arithinfix");
+  inline const auto BinInfix = TokenDef("rego-bininfix");
+  inline const auto BoolInfix = TokenDef("rego-boolinfix");
+  inline const auto AssignInfix = TokenDef("rego-assigninfix");
+  inline const auto ArgVar = TokenDef("rego-argvar", flag::lookup);
+  inline const auto ArgVal = TokenDef("rego-argval");
+  inline const auto LiteralWith = TokenDef("rego-literalwith");
+  inline const auto LiteralEnum = TokenDef("rego-literalenum");
+  inline const auto LiteralInit = TokenDef("rego-literalinit");
+  inline const auto LiteralNot = TokenDef("rego-literalnot");
+  inline const auto Enumerate = TokenDef("rego-enumerate");
+  inline const auto Merge = TokenDef("rego-merge");
+  inline const auto UnifyBody = TokenDef("rego-unifybody");
+  inline const auto Arg = TokenDef("rego-arg");
+  inline const auto Body = TokenDef("rego-body");
+  inline const auto Empty = TokenDef("rego-empty");
+  inline const auto NestedBody = TokenDef("rego-nestedbody", flag::symtab);
+  inline const auto Item = TokenDef("rego-item");
+  inline const auto ItemSeq = TokenDef("rego-itemseq");
+  inline const auto SimpleRef = TokenDef("rego-simpleref");
+  inline const auto Function = TokenDef("rego-function");
+  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 UnifyExprCompr = TokenDef("rego-unifyexprcompr");
+  inline const auto Compr = TokenDef("rego-compr");
+  inline const auto SkipSeq = TokenDef("rego-skipseq");
+  inline const auto Skip = TokenDef("rego-skip", flag::lookup);
+  inline const auto BuiltInHook = TokenDef("rego-builtinhook", flag::lookup);
+  inline const auto ElseSeq = TokenDef("rego-elseseq");
+  inline const auto TermSet = TokenDef("rego-termset");
+
+  const std::set<Token> RuleTypes(
+    {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
+
+  const inline auto ArithInfixArg =
+    T(Expr, NumTerm, Ref, UnaryExpr, ArithInfix, RefTerm, ExprCall);
+  const inline auto BinInfixArg =
+    T(Expr, Ref, RefTerm, ExprCall, Set, SetCompr, BinInfix);
+
+  // clang-format off
+  inline const auto wf_unify_input =
+    rego::wf
+    | (Top <<= Rego)
+    | (Rego <<= Query * Input * DataSeq * ModuleSeq)
+    | (ModuleSeq <<= Module++)
+    | (Input <<= DataTerm | Undefined)
+    | (DataSeq <<= Data++)
+    | (Data <<= DataTerm)
+    | (DataObject <<= DataObjectItem++)
+    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
+    | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
+    | (DataArray <<= DataTerm++)
+    | (DataSet <<= DataTerm++)
+    | (Import <<= Ref * Var)[Var]
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_strings =
+    wf_unify_input
+    | (Scalar <<= JSONString | Int | Float | True | False | Null)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_merge_data =
+    wf_pass_strings
+    | (Rego <<= Query * Input * Data * ModuleSeq)
+    | (Input <<= Key * (Val >>= DataTerm | Undefined))[Key]
+    | (Data <<= Key * (Val >>= DataModule))[Key]
+    | (DataModule <<= (DataRule | Submodule)++)
+    | (DataRule <<= Var * (Val >>= DataTerm))
+    | (Submodule <<= Key * (Val >>= DataModule))[Key]
+    | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
+    | (DataArray <<= DataTerm++)
+    | (DataSet <<= DataTerm++)
+    | (DataObject <<= DataObjectItem++)
+    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
+    | (RuleArgs <<= (ArgVar | ArgVal)++)
+    | (ArgVar <<= Var * (Val >>= Undefined))[Var]
+    | (ArgVal <<= Scalar | Array | Object | Set)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_lift_refheads = wf_pass_merge_data;
+  // clang-format on
+
+  inline const auto wf_symbols_exprs =
+    (wf_exprs - ExprEvery) | RefTerm | NumTerm;
+
+  // clang-format off
+  inline const auto wf_pass_symbols =
+    wf_pass_lift_refheads
+    | (Module <<= Package * 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]
+    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum)++[1])
+    | (Local <<= Var * (Val >>= Undefined))[Var]
+    | (Literal <<= Expr | NotExpr | SomeDecl)
+    | (LiteralEnum <<= (Item >>= Var) * (ItemSeq >>= Expr))
+    | (LiteralWith <<= UnifyBody * WithSeq)
+    | (With <<= RuleRef * Expr)
+    | (ExprCall <<= RuleRef * ExprSeq)
+    | (Query <<= (Body >>= UnifyBody))
+    | (Term <<= Scalar | Array | Object | 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)
+    | (Expr <<= wf_symbols_exprs)
+    | (NestedBody <<= Key * (Val >>= UnifyBody))
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_replace_argvals =
+    wf_pass_symbols
+    | (RuleArgs <<= ArgVar++)
+    | (Literal <<= Expr | NotExpr)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_lift_query =
+    wf_pass_replace_argvals
+    | (Query <<= Var)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_expand_imports = wf_pass_lift_query;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_constants =
+    wf_pass_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]
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_explicit_enums = 
+    wf_pass_constants
+    | (LiteralEnum <<= (Item >>= Var) * (ItemSeq >>= Var) * UnifyBody);
+  // clang-format on
+
+  inline const auto wf_pass_locals = wf_pass_explicit_enums;
+
+  // 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]
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_compr =
+    wf_pass_rules_to_compr
+    | (ObjectCompr <<= Var * NestedBody)
+    | (ArrayCompr <<= Var * NestedBody)
+    | (SetCompr <<= Var * NestedBody)
+    ;
+  // clang-format on
+
+  inline const auto wf_pass_absolute_refs = wf_pass_compr;
+
+  // clang-format off
+  inline const auto wf_pass_merge_modules =
+    wf_pass_absolute_refs
+    | (Rego <<= Query * Input * Data)
+    | (DataModule <<= (DataRule | RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
+    | (Submodule <<= Key * (Val >>= DataModule))[Key]
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_datarule = 
+    wf_pass_merge_modules
+    | (DataModule <<= (RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
+    | (Rego <<= Query * Input * Data)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_skips = 
+    wf_pass_datarule
+    | (Rego <<= Query * Input * Data * SkipSeq)
+    | (SkipSeq <<= Skip++)
+    | (Skip <<= Key * (Val >>= VarSeq | BuiltInHook | Undefined))[Key]
+    ;
+  // clang-format on
+
+  inline const auto wf_infix_exprs =
+    (wf_symbols_exprs | ArithInfix | BinInfix | BoolInfix);
+
+  // clang-format off
+  inline const auto wf_pass_infix =
+    wf_pass_skips
+    | (ArithInfix <<= Expr * (Op >>= Add | Subtract | Multiply | Divide | Modulo) * Expr)
+    | (BinInfix <<= Expr * (Op >>= And | Or) * Expr)
+    | (BoolInfix <<= Expr * (Op >>= Equals | NotEquals | LessThan | LessThanOrEquals | GreaterThan | GreaterThanOrEquals) * Expr)
+    | (Expr <<= wf_infix_exprs)
+    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot)++[1])
+    | (LiteralNot <<= UnifyBody)
+    | (Literal <<= Expr)
+    ;
+  // clang-format on
+
+  inline const auto wf_assign_exprs =
+    (wf_infix_exprs | AssignInfix) - ExprInfix;
+
+  // clang-format off
+  inline const auto wf_pass_assign =
+    wf_pass_infix
+    | (AssignInfix <<= AssignArg * AssignArg)
+    | (AssignArg <<= Expr)
+    | (Expr <<= wf_assign_exprs)
+    ;
+  // clang-format on
+
+  inline const auto wf_pass_skip_refs = wf_pass_assign;
+
+  // clang-format off
+  inline const auto wf_pass_simple_refs =
+    wf_pass_skip_refs
+    | (AssignArg <<= RefTerm | NumTerm | Term | ExprCall | UnaryExpr | ArithInfix | BinInfix | BoolInfix)
+    | (RefTerm <<= Var | SimpleRef)
+    | (SimpleRef <<= Var * (Op >>= RefArgDot | RefArgBrack))
+    | (Expr <<= wf_assign_exprs)
+    | (ExprCall <<= Var * ExprSeq)
+    | (RefHead <<= Var)
+    | (RuleRef <<= Var)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_init =
+    wf_pass_simple_refs
+    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot | LiteralInit)++[1])
+    | (LiteralInit <<= (Lhs >>= VarSeq) * (Rhs >>= VarSeq) * AssignInfix)
+    | (VarSeq <<= Var++)
+    ;
+  // clang-format on
+
+  inline const auto wf_pass_implicit_enums = wf_pass_init;
+
+  inline const auto wf_pass_enum_locals = wf_pass_implicit_enums;
+
+  inline const auto wf_rulebody_exprs = (wf_assign_exprs - AssignInfix);
+
+  // clang-format off
+  inline const auto wf_pass_rulebody =
+    wf_pass_enum_locals
+    | (Module <<= (Import | RuleComp | RuleFunc | RuleSet | RuleObj | Submodule)++)
+    | (UnifyExpr <<= Var * (Val >>= Expr))
+    | (Expr <<= wf_rulebody_exprs)
+    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprCompr | UnifyExprEnum | UnifyExprNot)++[1])
+    | (UnifyExprWith <<= UnifyBody * WithSeq)
+    | (UnifyExprCompr <<= Var * (Val >>= ArrayCompr | SetCompr | ObjectCompr) * NestedBody)
+    | (UnifyExprEnum <<= Var * (Item >>= Var) * (ItemSeq >>= Var) * UnifyBody)
+    | (UnifyExprNot <<= UnifyBody)
+    | (ArrayCompr <<= Var)
+    | (SetCompr <<= Var)
+    | (ObjectCompr <<= Var)
+    | (With <<= RuleRef * Var)
+    ;
+  // clang-format on
+
+  inline const auto wf_lift_to_rule_exprs =
+    wf_rulebody_exprs | Enumerate | ArrayCompr | SetCompr | ObjectCompr | Merge;
+
+  // clang-format off
+  inline const auto wf_pass_lift_to_rule =
+    wf_pass_rulebody
+    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprNot)++[1])
+    | (Expr <<= wf_lift_to_rule_exprs)
+    | (Merge <<= Var)
+    | (Enumerate <<= Expr)
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_functions =
+    wf_pass_lift_to_rule
+    | (UnifyExpr <<= Var * (Val >>= Var | Scalar | Array | Set | Object | Function))
+    | (Function <<= JSONString * ArgSeq)
+    | (ArgSeq <<= (Scalar | Object | Array | Set | Var | wf_arith_op | wf_bin_op | wf_bool_op | NestedBody | VarSeq)++)
+    | (Input <<= Key * (Val >>= Term | Undefined))[Key]
+    | (Array <<= Term++)
+    | (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]
+    ;
+  // clang-format on
+
+  // clang-format off
+  inline const auto wf_pass_unify =
+    wf_pass_functions
+    | (Query <<= (Term | Binding)++)
+    | (Binding <<= Var * Term)[Var]
+    | (Term <<= Scalar | Array | Object | Set)
+    ;
+  // clang-format on
+
+  PassDef strings();
+  PassDef merge_data();
+  PassDef lift_refheads();
+  PassDef symbols();
+  PassDef replace_argvals();
+  PassDef lift_query();
+  PassDef expand_imports(BuiltIns builtins);
+  PassDef constants();
+  PassDef explicit_enums();
+  PassDef body_locals(BuiltIns builtins);
+  PassDef value_locals(BuiltIns builtins);
+  PassDef compr_locals(BuiltIns builtins);
+  PassDef rules_to_compr();
+  PassDef compr();
+  PassDef absolute_refs();
+  PassDef merge_modules();
+  PassDef datarule();
+  PassDef skips();
+  PassDef infix();
+  PassDef assign(BuiltIns builtins);
+  PassDef skip_refs(BuiltIns builtins);
+  PassDef simple_refs();
+  PassDef init();
+  PassDef implicit_enums();
+  PassDef enum_locals();
+  PassDef rulebody();
+  PassDef lift_to_rule();
+  PassDef functions();
+  PassDef unifier(BuiltIns builtins);
+  PassDef result();
+
+  class ValueDef;
+  using rank_t = std::size_t;
+  using Value = std::shared_ptr<ValueDef>;
+  using Values = std::vector<Value>;
+  using RankedNode = std::pair<rank_t, Node>;
+
+  class ValueDef
+  {
+  public:
+    void mark_as_valid();
+    void mark_as_invalid();
+    void reduce_set();
+
+    const std::string& str() const;
+    const std::string& json() const;
+    bool depends_on(const Value& value) const;
+    bool invalid() const;
+    const Location& var() const;
+    const Node& node() const;
+    const Values& sources() const;
+    Node to_term() const;
+    rank_t rank() const;
+    friend std::ostream& operator<<(std::ostream& os, const Value& value);
+    friend std::ostream& operator<<(std::ostream& os, const ValueDef& value);
+    friend bool operator==(const Value& lhs, const Value& rhs);
+    friend bool operator<(const Value& lhs, const Value& rhs);
+
+    static Value create(const RankedNode& value);
+    static Value create(const Location& var, const RankedNode& value);
+    static Value create(
+      const Location& var, const RankedNode& value, const Values& sources);
+    static Value create(const Node& value);
+    static Value create(const Location& var, const Node& value);
+    static Value create(
+      const Location& var, const Node& value, const Values& sources);
+    static Value copy_to(const Value& value, const Location& var);
+    static Values filter_by_rank(const Values& values);
+    static rank_t get_rank(const Node& node);
+
+  private:
+    static void build_string(
+      std::ostream& buf,
+      const ValueDef& current,
+      const Location& root,
+      bool first);
+    Location m_var;
+    Node m_node;
+    Values m_sources;
+    bool m_invalid;
+    rank_t m_rank;
+    std::string m_str;
+    std::string m_json;
+    ValueDef(
+      const Location& var,
+      const Node& value,
+      const Values& sources,
+      rank_t rank);
+    ValueDef(const Node& value);
+    ValueDef(const RankedNode& value);
+    ValueDef(const Location& var, const Node& value);
+    ValueDef(const Location& var, const RankedNode& value);
+    ValueDef(const Location& var, const Node& value, const Values& sources);
+    ValueDef(
+      const Location& var, const RankedNode& value, const Values& sources);
+  };
+
+  class ValueMap
+  {
+  public:
+    ValueMap();
+    ValueMap(
+      const Values::const_iterator& begin, const Values::const_iterator& end);
+    bool contains(const Value& value) const;
+    bool intersect_with(const Values& values);
+    bool remove_values_not_contained_in(const Values& values);
+    void clear();
+    bool insert(const Value& value);
+    bool erase(const std::string& json);
+    bool empty() const;
+    Nodes to_terms() const;
+    Values valid_values() const;
+    bool remove_invalid_values();
+    void mark_valid_values(bool include_falsy);
+    void mark_invalid_values();
+
+    friend std::ostream& operator<<(std::ostream& os, const ValueMap& values);
+
+  private:
+    std::multimap<std::string, Value> m_map;
+    std::set<std::pair<std::string, std::string>> m_values;
+    std::set<std::string> m_keys;
+  };
+
+  /**
+   * The Args class provides an interable interface over the Cartesian product
+   * of all sets of arguments to a function. It will contain N sets of argument
+   * sources, and will produce s_0 * s_1 * ... * s_n arguments, where s_i is the
+   * size of the i-th argument source.
+   */
+  class Args
+  {
+  public:
+    Args();
+    void push_back_source(const Values& values);
+    void mark_invalid(const std::set<Value>& active) const;
+    void mark_invalid_except(const std::set<Value>& active) const;
+    Values at(std::size_t index) const;
+    std::size_t size() const;
+    std::string str() const;
+    std::size_t source_size() const;
+    const Values& source_at(std::size_t index) const;
+    Args subargs(std::size_t start) const;
+    friend std::ostream& operator<<(std::ostream& os, const Args& args);
+
+  private:
+    std::vector<Values> m_values;
+    std::vector<size_t> m_stride;
+    std::size_t m_size;
+  };
+
+  class Variable
+  {
+  public:
+    Variable(const Node& local, std::size_t id);
+    bool unify(const Values& others);
+    std::string str() const;
+    bool remove_invalid_values();
+    void mark_invalid_values();
+    void mark_valid_values();
+    Values valid_values() const;
+    Node to_term() const;
+    Node bind();
+    void reset();
+
+    bool is_unify() const;
+    bool is_user_var() const;
+    Location name() const;
+    std::size_t id() const;
+
+    static bool is_unify(const std::string_view& name);
+    static bool is_user_var(const std::string_view& name);
+
+    friend std::ostream& operator<<(std::ostream& os, const Variable& variable);
+    friend std::ostream& operator<<(
+      std::ostream& os, const std::map<Location, Variable>& variables);
+
+  private:
+    bool intersect_with(const Values& others);
+    bool initialize(const Values& others);
+
+    Node m_local;
+    ValueMap m_values;
+    bool m_unify;
+    bool m_user_var;
+    bool m_initialized;
+    std::size_t m_id;
+  };
+
+  enum class UnifierType
+  {
+    RuleBody,
+    RuleValue
+  };
+
+  struct UnifierKey
+  {
+    Location key;
+    UnifierType type;
+    bool operator<(const UnifierKey& other) const;
+  };
+
+  class UnifierDef;
+  using Unifier = std::shared_ptr<UnifierDef>;
+  using CallStack = std::shared_ptr<std::vector<Location>>;
+  using ValuesLookup = std::map<std::string, Values>;
+  using WithStack = std::shared_ptr<std::vector<ValuesLookup>>;
+  using UnifierCache = std::shared_ptr<std::map<UnifierKey, Unifier>>;
+
+  class UnifierDef
+  {
+  public:
+    UnifierDef(
+      const Location& rule,
+      const Node& rulebody,
+      CallStack call_stack,
+      WithStack with_stack,
+      BuiltIns builtins,
+      UnifierCache unifier_cache);
+    Node unify();
+    Nodes expressions() const;
+    Nodes bindings() const;
+    std::string str() const;
+    std::string dependency_str() const;
+    static Unifier create(
+      const UnifierKey& key,
+      const Location& rule,
+      const Node& rulebody,
+      const CallStack& call_stack,
+      const WithStack& with_stack,
+      BuiltIns builtins,
+      const UnifierCache& unifier_cache);
+    std::optional<Node> resolve_rule(const Nodes& defs) const;
+    std::optional<Node> resolve_rulecomp(const Nodes& rulecomp) const;
+    std::optional<Node> resolve_ruleset(const Nodes& ruleset) const;
+    std::optional<Node> resolve_ruleobj(const Nodes& ruleobj) const;
+    std::optional<RankedNode> resolve_rulefunc(
+      const Node& rulefunc, const Nodes& args) const;
+    Node resolve_module(const Node& module) const;
+
+    struct Dependency
+    {
+      std::string name;
+      std::set<std::size_t> dependencies;
+      std::size_t score;
+    };
+
+    struct Statement
+    {
+      std::size_t id;
+      Node node;
+    };
+
+  private:
+    Unifier rule_unifier(
+      const UnifierKey& key, const Location& rule, const Node& rulebody) const;
+    void init_from_body(
+      const Node& rulebody,
+      std::vector<Statement>& statements,
+      std::size_t root);
+    std::size_t add_variable(const Node& local);
+    std::size_t add_unifyexpr(const Node& unifyexpr);
+    void add_withpush(const Node& withpush);
+    void add_withpop(const Node& withpop);
+    void reset();
+
+    void compute_dependency_scores();
+    std::size_t compute_dependency_score(
+      std::size_t index, std::set<size_t>& visited);
+    std::size_t dependency_score(const Variable& var) const;
+    std::size_t dependency_score(const Statement& stmt) const;
+    std::size_t detect_cycles() const;
+    bool has_cycle(std::size_t id) const;
+
+    Values evaluate(const Location& var, const Node& value);
+    Values evaluate_function(
+      const Location& var, const std::string& func_name, const Args& args);
+    Values resolve_var(const Node& var, bool exclude_with = false);
+    Values enumerate(const Location& var, const Node& container);
+    Values resolve_skip(const Node& skip);
+    Values check_with(const Node& var, bool bypass_recurse_check = false);
+    Values apply_access(const Location& var, const Values& args);
+    std::optional<Value> call_function(
+      const Location& var, const Values& args) const;
+    std::optional<Value> call_named_function(
+      const Location& var, const std::string& name, const Values& args);
+    bool is_local(const Node& var);
+    std::size_t scan_vars(const Node& expr, std::vector<Location>& locals);
+    void pass();
+    void execute_statements(
+      std::vector<Statement>::iterator begin,
+      std::vector<Statement>::iterator end);
+    void remove_invalid_values();
+    void mark_invalid_values();
+    Variable& get_variable(const Location& name);
+    bool is_variable(const Location& name) const;
+    Node bind_variables();
+    Args create_args(const Node& args);
+    bool would_recurse(const Node& node);
+
+    bool push_rule(const Location& rule);
+    void pop_rule(const Location& rule);
+
+    void push_with(const Node& withseq);
+    void pop_with();
+
+    void push_not();
+    void pop_not();
+
+    Location m_rule;
+    std::map<Location, Variable> m_variables;
+    std::vector<Statement> m_statements;
+    std::map<std::size_t, std::vector<Statement>> m_nested_statements;
+    CallStack m_call_stack;
+    WithStack m_with_stack;
+    BuiltIns m_builtins;
+    UnifierCache m_cache;
+    std::size_t m_retries;
+    Token m_parent_type;
+    std::vector<Dependency> m_dependency_graph;
+    bool m_negate;
+  };
+
+  Node expr_infix(Token op_token, Node lhs, Node rhs);
+}
+
+// Use ADL to extend Trieste logging capabilities with additional types.
+namespace trieste::logging
+{
+  inline void append(
+    Log& log, const std::vector<rego::UnifierDef::Dependency>& deps)
+  {
+    for (auto it = deps.begin(); it != deps.end(); ++it)
+    {
+      auto& dep = *it;
+      log << "[" << dep.name << "](" << dep.score << ") -> {";
+
+      logging::Sep sep{", "};
+      for (auto& idx : dep.dependencies)
+      {
+        log << sep << deps[idx].name;
+      }
+      log << "}" << std::endl;
+    }
+  }
+
+  template <typename T>
+  inline void append(Log& log, const std::vector<T>& values)
+  {
+    log << "[";
+    logging::Sep sep{", "};
+    for (auto& value : values)
+    {
+      log << sep << value;
+    }
+    log << "]" << std::endl;
+  }
+
+  inline void append(Log& log, const Location& loc)
+  {
+    log << loc.view();
+  }
+
+  inline void append(Log& log, const rego::UnifierDef::Statement& statement)
+  {
+    rego::Resolver::stmt_str(log, statement.node);
+  }
+
+  template <typename K, typename V>
+  inline void append(Log& log, const rego::MapValuesStr<K, V>& map)
+  {
+    log << "{" << std::endl;
+    for (auto& [_, value] : map.values)
+    {
+      log << value << std::endl;
+    }
+    log << "}" << std::endl;
+  }
+} // trieste::logging
\ No newline at end of file
diff --git a/src/passes/absolute_refs.cc b/src/unify/absolute_refs.cc
similarity index 92%
rename from src/passes/absolute_refs.cc
rename to src/unify/absolute_refs.cc
index a1b9c80f..f656f347 100644
--- a/src/passes/absolute_refs.cc
+++ b/src/unify/absolute_refs.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -57,7 +57,7 @@ namespace rego
       dir::topdown,
       {
         In(RefTerm, RuleRef) * T(Var)[Var]([](auto& n) {
-          return is_ref_to_type(*n.first, RuleTypes);
+          return is_ref_to_type(n.front(), RuleTypes);
         }) >>
           [](Match& _) {
             ACTION();
@@ -67,10 +67,10 @@ namespace rego
             return build_ref(rule);
           },
 
-        In(RefTerm, RuleRef) *
+        In(RefTerm, RuleRef, ExprCall) *
             (T(Ref)
              << ((T(RefHead) << T(Var)[Var]([](auto& n) {
-                    return is_ref_to_type(*n.first, RuleTypes);
+                    return is_ref_to_type(n.front(), RuleTypes);
                   })) *
                  T(RefArgSeq)[RefArgSeq])) >>
           [](Match& _) {
diff --git a/src/unify/assign.cc b/src/unify/assign.cc
new file mode 100644
index 00000000..39638458
--- /dev/null
+++ b/src/unify/assign.cc
@@ -0,0 +1,192 @@
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+
+  using FunctionArity = std::shared_ptr<std::map<std::string, std::size_t>>;
+
+  std::string concat(const Node& ref)
+  {
+    std::ostringstream path;
+    path << (ref / RefHead)->front()->location().view();
+    Node refargseq = ref / RefArgSeq;
+    for (auto& refarg : *refargseq)
+    {
+      if (refarg->type() == RefArgDot)
+      {
+        path << "." << refarg->front()->location().view();
+      }
+      else if (refarg->type() == RefArgBrack)
+      {
+        path << "[" << strip_quotes(to_key(refarg->front())) << "]";
+      }
+    }
+
+    return path.str();
+  }
+
+  bool needs_rewrite(const NodeRange& n, const FunctionArity& func_arity)
+  {
+    Node ref = n[0]->front();
+    assert(ref == Ref);
+    Node exprseq = n[1];
+    assert(exprseq == ExprSeq);
+
+    std::string path = concat(ref);
+    if (!contains(func_arity, path))
+    {
+      return false;
+    }
+
+    bool result = func_arity->at(path) == exprseq->size() - 1;
+    return result;
+  }
+}
+
+namespace rego
+{
+  const inline auto BoolInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
+    T(ArithInfix) / T(Term) / T(ExprCall) / T(BinInfix);
+
+  // Transforms unification expressions into AssignInfix nodes.
+  PassDef assign(BuiltIns builtins)
+  {
+    FunctionArity func_arity =
+      std::make_shared<std::map<std::string, std::size_t>>();
+
+    PassDef assign{
+      "assign",
+      wf_pass_assign,
+      dir::bottomup | dir::once,
+      {
+        T(ExprInfix)
+            << (T(Expr)[Lhs] * (T(InfixOperator) << T(AssignOperator)) *
+                T(Expr)[Rhs]) >>
+          [](Match& _) {
+            ACTION();
+            return AssignInfix << (AssignArg << _(Lhs))
+                               << (AssignArg << _(Rhs));
+          },
+
+        In(Literal) *
+            (T(Expr)
+             << (T(ExprCall)
+                 << ((T(RuleRef) << T(Ref)[Ref]) *
+                     T(ExprSeq)[ExprSeq])([func_arity](auto& n) {
+                      // tests if this is a function call that has an extra
+                      // argument that should be unified with the result.
+                      return needs_rewrite(n, func_arity);
+                    }))) >>
+          [](Match& _) {
+            ACTION();
+            // A convention in the Golang implementation of Rego is
+            // that you can call any function with an additional argument
+            // that will be unified with its result. This is not documented
+            // anywhere but is expected behavior used in tests. This
+            // rewrite turns these back into the documented form which we
+            // expect.
+            Node ref = Ref << (RefHead << (Var ^ concat(_(Ref)))) << RefArgSeq;
+            Node exprseq = _(ExprSeq);
+            Node var = exprseq->pop_back();
+            return Expr
+              << (AssignInfix
+                  << (AssignArg << var)
+                  << (AssignArg
+                      << (Expr << (ExprCall << (RuleRef << ref) << exprseq))));
+          },
+
+        In(Literal) *
+            (T(Expr)
+             << (T(AssignInfix)
+                 << (T(AssignArg)[Lhs] *
+                     (T(AssignArg)
+                      << (T(ExprCall)
+                          << ((T(RuleRef) << T(Ref)[Ref]) *
+                              T(ExprSeq)[ExprSeq] *
+                              In(UnifyBody)++)([func_arity](auto& n) {
+                               return needs_rewrite(n, func_arity);
+                             })))))) >>
+          [](Match& _) {
+            ACTION();
+            // see above comment.
+            Node ref = Ref << (RefHead << (Var ^ concat(_(Ref)))) << RefArgSeq;
+            Node exprseq = _(ExprSeq);
+            Node var = exprseq->pop_back();
+            return Seq << (Lift
+                           << UnifyBody
+                           << (Literal
+                               << (Expr
+                                   << (AssignInfix
+                                       << (AssignArg << var)
+                                       << (AssignArg
+                                           << (Expr
+                                               << (ExprCall << (RuleRef << ref)
+                                                            << exprseq)))))))
+                       << (Expr
+                           << (AssignInfix << _(Lhs)
+                                           << (AssignArg << var->clone())));
+          },
+
+        In(Literal) * (T(Expr)[Expr] << !T(AssignInfix)) >>
+          [](Match& _) {
+            ACTION();
+            // bare expressions are treated as unification requirements
+            // in non-query rules.
+            std::string prefix = in_query(_(Expr)) ? "value" : "unify";
+            Location temp = _.fresh({prefix});
+            return Seq << (Lift << UnifyBody
+                                << (Local << (Var ^ temp) << Undefined))
+                       << (Expr
+                           << (AssignInfix
+                               << (AssignArg
+                                   << (Expr << (RefTerm << (Var ^ temp))))
+                               << (AssignArg << _(Expr))));
+          },
+
+        // errors
+
+        In(Expr) * T(Set, SetCompr)[Set] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Set), "Invalid set argument");
+          },
+
+        In(Expr) * T(ExprInfix)[ExprInfix] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(ExprInfix), "Invalid infix expression");
+          },
+      }};
+
+    assign.pre(Rego, [func_arity, builtins](Node node) {
+      if (!func_arity->empty())
+      {
+        return 0;
+      }
+
+      Nodes rules;
+      node->get_symbols(rules, [](Node n) { return n->type() == RuleFunc; });
+      for (auto rule : rules)
+      {
+        std::string path = std::string((rule / Var)->location().view());
+        std::size_t arity = (rule / RuleArgs)->size();
+        func_arity->insert({path, arity});
+      }
+
+      for (auto& [loc, builtin] : *builtins)
+      {
+        func_arity->insert({std::string(loc.view()), builtin->arity});
+      }
+
+      return 0;
+    });
+
+    assign.post([func_arity](Node) {
+      func_arity->clear();
+      return 0;
+    });
+
+    return assign;
+  }
+}
\ No newline at end of file
diff --git a/src/passes/compr.cc b/src/unify/compr.cc
similarity index 69%
rename from src/passes/compr.cc
rename to src/unify/compr.cc
index 0780ae27..b9d663fa 100644
--- a/src/passes/compr.cc
+++ b/src/unify/compr.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
@@ -24,13 +24,14 @@ namespace rego
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
-            return RuleSet
-              << _(Var) << Empty
-              << (UnifyBody
-                  << (Local << (Var ^ value) << Undefined)
-                  << (Literal
-                      << (Expr << (RefTerm << (Var ^ value)) << Unify
-                               << (Expr << (Term << (Set << _(Val)))))));
+            return RuleSet << _(Var) << Empty
+                           << (UnifyBody
+                               << (Local << (Var ^ value) << Undefined)
+                               << (Literal
+                                   << (Expr << expr_infix(
+                                         Unify,
+                                         RefTerm << (Var ^ value),
+                                         Term << (Set << _(Val))))));
           },
 
         In(Policy) * T(RuleSet)
@@ -42,11 +43,12 @@ namespace rego
             Node body = NestedBody << (Key ^ compr) << _(Body);
             return RuleSet
               << _(Var) << Empty
-              << (UnifyBody
-                  << (Local << (Var ^ value) << Undefined)
-                  << (Literal
-                      << (Expr << (RefTerm << (Var ^ value)) << Unify
-                               << (Term << (SetCompr << _(Val) << body)))));
+              << (UnifyBody << (Local << (Var ^ value) << Undefined)
+                            << (Literal
+                                << (Expr << expr_infix(
+                                      Unify,
+                                      RefTerm << (Var ^ value),
+                                      Term << (SetCompr << _(Val) << body)))));
           },
 
         In(Policy) *
@@ -61,17 +63,18 @@ namespace rego
               << (UnifyBody
                   << (Local << (Var ^ value) << Undefined)
                   << (Local << (Var ^ key) << Undefined)
-                  << (Literal
-                      << (Expr << (RefTerm << (Var ^ key)) << Unify << _(Key)))
                   << (Literal
                       << (Expr
-                          << (RefTerm << (Var ^ value)) << Unify
-                          << (Expr
-                              << (Term
-                                  << (Object
-                                      << (ObjectItem
-                                          << (Expr << (RefTerm << (Var ^ key)))
-                                          << _(Val))))))));
+                          << expr_infix(Unify, RefTerm << (Var ^ key), _(Key))))
+                  << (Literal
+                      << (Expr << expr_infix(
+                            Unify,
+                            RefTerm << (Var ^ value),
+                            Term
+                              << (Object
+                                  << (ObjectItem
+                                      << (Expr << (RefTerm << (Var ^ key)))
+                                      << _(Val)))))));
           },
 
         In(Policy) *
@@ -103,16 +106,16 @@ namespace rego
 
             body = NestedBody << (Key ^ compr) << body;
 
-            return RuleObj
-              << _(Var) << Empty
-              << (UnifyBody
-                  << (Local << (Var ^ value) << Undefined)
-                  << (Literal
-                      << (Expr << (RefTerm << (Var ^ value)) << Unify
-                               << (Expr
-                                   << (Term
-                                       << (ObjectCompr << _(Key) << _(Val)
-                                                       << body))))));
+            return RuleObj << _(Var) << Empty
+                           << (UnifyBody
+                               << (Local << (Var ^ value) << Undefined)
+                               << (Literal
+                                   << (Expr << expr_infix(
+                                         Unify,
+                                         RefTerm << (Var ^ value),
+                                         Term
+                                           << (ObjectCompr << _(Key) << _(Val)
+                                                           << body)))));
           },
 
         // errors
@@ -161,7 +164,8 @@ namespace rego
             }
             innermost
               << (Literal
-                  << (Expr << (RefTerm << (Var ^ out)) << Unify << _(Expr)));
+                  << (Expr << expr_infix(
+                        Unify, RefTerm << (Var ^ out), _(Expr))));
             (_(NestedBody) / Val)
               ->push_front(Local << (Var ^ out) << Undefined);
             return Seq << (Var ^ out) << _(NestedBody);
@@ -182,8 +186,10 @@ namespace rego
 
             innermost
               << (Literal
-                  << (Expr << (RefTerm << (Var ^ out)) << Unify
-                           << (Expr << (Term << (Array << _(Key) << _(Val))))));
+                  << (Expr << expr_infix(
+                        Unify,
+                        RefTerm << (Var ^ out),
+                        Term << (Array << _(Key) << _(Val)))));
             (_(NestedBody) / Val)
               ->push_front(Local << (Var ^ out) << Undefined);
 
diff --git a/src/passes/constants.cc b/src/unify/constants.cc
similarity index 83%
rename from src/passes/constants.cc
rename to src/unify/constants.cc
index b1d7d61d..c2b3b13d 100644
--- a/src/passes/constants.cc
+++ b/src/unify/constants.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
@@ -11,45 +11,40 @@ namespace rego
       dir::topdown,
       {
         In(RuleComp, RuleFunc, RuleSet, DefaultRule) *
-            T(Term)[Term]([](auto& n) { return is_constant(*n.first); }) >>
+            T(Term)[Term]([](auto& n) { return is_constant(n.front()); }) >>
           [](Match& _) {
             ACTION();
             return DataTerm << *_[Term];
           },
 
         In(RuleComp, RuleFunc) *
-            T(Term)[Term]([](auto& n) { return !is_constant(*n.first); }) >>
+            T(Term)[Term]([](auto& n) { return !is_constant(n.front()); }) >>
           [](Match& _) {
             ACTION();
             Location value = _.fresh({"value"});
             return UnifyBody
               << (Literal
-                  << (Expr << (RefTerm << (Var ^ value)) << Unify << _(Term)));
+                  << (Expr << expr_infix(
+                        Unify, RefTerm << (Var ^ value), _(Term))));
           },
 
         In(RuleSet) *
-            T(Term)[Term]([](auto& n) { return !is_constant(*n.first); }) >>
+            T(Term)[Term]([](auto& n) { return !is_constant(n.front()); }) >>
           [](Match& _) {
             ACTION();
             return Expr << _(Term);
           },
 
         In(RuleObj) * (T(Term)[Key] * T(Term)[Val])([](auto& n) {
-          Node key = n.first[0];
-          Node val = n.first[1];
-          return is_constant(key) && is_constant(val);
+          return is_constant(n.front()) && is_constant(n.back());
         }) >>
           [](Match& _) {
             ACTION();
-            std::string key = strip_quotes(to_json(_(Key)));
+            std::string key = strip_quotes(to_key(_(Key)));
             return Seq << (DataTerm << *_[Key]) << (DataTerm << *_[Val]);
           },
 
-        In(RuleObj) * (T(Term)[Key] * T(Term)[Val])([](auto& n) {
-          Node key = n.first[0];
-          Node val = n.first[1];
-          return !is_constant(key) || !is_constant(val);
-        }) >>
+        In(RuleObj) * (T(Term)[Key] * T(Term)[Val]) >>
           [](Match& _) {
             ACTION();
             return Seq << (Expr << _(Key)) << (Expr << _(Val));
@@ -147,8 +142,8 @@ namespace rego
           },
 
         In(ModuleSeq) * (T(Module)[Lhs] * T(Module)[Rhs])([](auto& n) {
-          Node lhs = n.first[0];
-          Node rhs = n.first[1];
+          Node lhs = n.front();
+          Node rhs = n.back();
           auto lhs_package = flatten_ref((lhs / Package)->front());
           auto rhs_package = flatten_ref((rhs / Package)->front());
 
diff --git a/src/passes/enumerate.cc b/src/unify/enumerate.cc
similarity index 97%
rename from src/passes/enumerate.cc
rename to src/unify/enumerate.cc
index 210f7685..9817ffa0 100644
--- a/src/passes/enumerate.cc
+++ b/src/unify/enumerate.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -91,9 +91,8 @@ namespace
   {
     std::set<Location> vars;
     vars_from(itemseq, vars);
-    for (auto it = tail.first; it != tail.second; ++it)
+    for (Node stmt : tail)
     {
-      Node stmt = *it;
       if (stmt->type() == LiteralInit)
       {
         std::set<Location> init_vars;
@@ -151,8 +150,10 @@ namespace rego
             }
             return Seq << (Local << (Var ^ itemseq) << Undefined)
                        << (Literal
-                           << (Expr << (RefTerm << (Var ^ itemseq)) << Unify
-                                    << *_[ItemSeq]))
+                           << (Expr << expr_infix(
+                                 Unify,
+                                 RefTerm << (Var ^ itemseq),
+                                 _(ItemSeq))))
                        << (LiteralEnum << _(Item)->clone() << (Var ^ itemseq)
                                        << body);
           },
@@ -196,7 +197,6 @@ namespace rego
              LiteralToken++[Tail] * End) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "val = ref[idx]";
 
             Node idx = _(Idx)->front();
             if (idx->type() == Expr)
@@ -275,7 +275,6 @@ namespace rego
              LiteralToken++[Tail] * End) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "val = ref[idx]";
 
             Node idx = _(Idx)->front();
             if (idx->type() == Expr)
@@ -427,6 +426,7 @@ namespace rego
         target->push_front(local);
       }
 
+      moves->clear();
       return 0;
     });
 
diff --git a/src/unify/expand_imports.cc b/src/unify/expand_imports.cc
new file mode 100644
index 00000000..bceaebcb
--- /dev/null
+++ b/src/unify/expand_imports.cc
@@ -0,0 +1,59 @@
+#include "rego.hh"
+#include "unify.hh"
+
+namespace rego
+{
+  // Expands imports to their fully-qualified values.
+  PassDef expand_imports(BuiltIns builtins)
+  {
+    return {
+      "expand_imports",
+      wf_pass_expand_imports,
+      dir::bottomup | dir::once,
+      {In(RefTerm, RuleRef) * T(Var)[Var] >> [](Match& _) -> Node {
+         // <import>
+         ACTION();
+         Nodes defs = _(Var)->lookup();
+         if (defs.empty())
+           return NoChange;
+         Node import = defs[0];
+         if (import->type() != Import)
+           return NoChange;
+         Node ref = import / Ref;
+         return ref->clone();
+       },
+
+       In(RefTerm, RuleRef) *
+           (T(Ref)
+            << ((T(RefHead) << T(Var)[Var]) * T(RefArgSeq)[RefArgSeq])) >>
+         [](Match& _) -> Node {
+         // <import>.dot <import>[brack]
+         ACTION();
+         Nodes defs = _(Var)->lookup();
+         if (defs.empty())
+           return NoChange;
+         Node import = defs[0];
+         if (import->type() != Import)
+           return NoChange;
+
+         Node ref = import / Ref;
+         Node refhead = (ref / RefHead)->clone();
+         Node refargseq = (ref / RefArgSeq)->clone();
+         refargseq->insert(
+           refargseq->end(), _(RefArgSeq)->begin(), _(RefArgSeq)->end());
+         return Ref << refhead << refargseq;
+       },
+
+       In(RuleRef, RefTerm) * T(Ref)[Ref]([builtins](auto n) {
+         std::string flatten = flatten_ref(n.front());
+         return builtins->is_builtin({flatten});
+       }) >>
+         [](Match& _) {
+           auto ref = _(Ref);
+           std::string flatten = flatten_ref(ref);
+           ref / RefHead = RefHead << (Var ^ flatten);
+           (ref / RefArgSeq) = NodeDef::create(RefArgSeq);
+           return ref;
+         }}};
+  }
+}
\ No newline at end of file
diff --git a/src/passes/functions.cc b/src/unify/functions.cc
similarity index 89%
rename from src/passes/functions.cc
rename to src/unify/functions.cc
index ecdcd2f7..e5792665 100644
--- a/src/passes/functions.cc
+++ b/src/unify/functions.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -95,19 +95,24 @@ namespace
   Node unwrap_term(Node term)
   {
     Node node = term;
-    if (node->type() == Term)
+    if (node == Term)
     {
       node = node->front();
     }
 
-    if (node->type() == Scalar)
+    if (node == Scalar)
+    {
+      return node;
+    }
+
+    if (node == Membership)
     {
       return node;
     }
 
     // analyze whether these are constant. Only need the functions if there are
     // expressions to analyze.
-    if (node->type() == Array)
+    if (node == Array)
     {
       if (is_constant(node))
       {
@@ -122,7 +127,7 @@ namespace
       return Function << (JSONString ^ "array") << argseq;
     }
 
-    if (node->type() == Set)
+    if (node == Set)
     {
       if (is_constant(node))
       {
@@ -137,7 +142,7 @@ namespace
       return Function << (JSONString ^ "set") << argseq;
     }
 
-    if (node->type() == Object)
+    if (node == Object)
     {
       if (is_constant(node))
       {
@@ -194,12 +199,31 @@ namespace
     return result;
   }
 
+  const char* compr_name(Node node)
+  {
+    if (node == ArrayCompr)
+    {
+      return "array-compr";
+    }
+
+    if (node == SetCompr)
+    {
+      return "set-compr";
+    }
+
+    if (node == ObjectCompr)
+    {
+      return "object-compr";
+    }
+
+    throw std::runtime_error("Invalid comprehension type");
+  }
+
   const auto inline VarOrTerm = T(Var) / T(Term);
   const auto inline RefArg = T(RefArgDot) / T(RefArgBrack);
 }
 namespace rego
 {
-
   // Converts all UnifyExpr statements to be of either <var> = <var>,
   // <var> = <term>, or <var> = <function> forms, where <function> is a named
   // function that takes either <var> or <term> arguments.
@@ -231,7 +255,8 @@ namespace rego
 
         In(UnifyExpr, ArgSeq) *
             (T(Membership)
-             << (T(Expr)[Idx] * T(Expr)[Item] * T(Expr)[ItemSeq])) >>
+             << ((T(ExprSeq) << (T(Expr)[Idx] * T(Expr)[Item] * End)) *
+                 T(Expr)[ItemSeq])) >>
           [](Match& _) {
             ACTION();
             return Function
@@ -242,7 +267,7 @@ namespace rego
 
         In(UnifyExpr, ArgSeq) *
             (T(Membership)
-             << (T(Undefined) * T(Expr)[Item] * T(Expr)[ItemSeq])) >>
+             << ((T(ExprSeq) << (T(Expr)[Item] * End)) * T(Expr)[ItemSeq])) >>
           [](Match& _) {
             ACTION();
             return Function
@@ -250,11 +275,10 @@ namespace rego
               << (ArgSeq << unwrap_node(_(Item)) << unwrap_node(_(ItemSeq)));
           },
 
-        In(UnifyExpr, ArgSeq) *
-            (T(ArrayCompr) / T(SetCompr) / T(ObjectCompr))[Compr] >>
+        In(UnifyExpr, ArgSeq) * (T(ArrayCompr, SetCompr, ObjectCompr))[Compr] >>
           [](Match& _) {
             ACTION();
-            std::string name = _(Compr)->type().str();
+            std::string name = compr_name(_(Compr));
             std::transform(
               name.begin(), name.end(), name.begin(), [](unsigned char c) {
                 return static_cast<char>(std::tolower(c));
@@ -269,11 +293,10 @@ namespace rego
           },
 
         In(UnifyExpr, ArgSeq) *
-            (T(Term)
-             << (T(ArrayCompr) / T(SetCompr) / T(ObjectCompr)))[Compr] >>
+            (T(Term) << (T(ArrayCompr, SetCompr, ObjectCompr)))[Compr] >>
           [](Match& _) {
             ACTION();
-            std::string name = _(Compr)->type().str();
+            std::string name = compr_name(_(Compr));
             std::transform(
               name.begin(), name.end(), name.begin(), [](unsigned char c) {
                 return static_cast<char>(std::tolower(c));
@@ -315,16 +338,15 @@ namespace rego
                             << (ArgSeq << unwrap_node(_(Expr)));
           },
 
-        In(UnifyExpr, ArgSeq) * (T(UnaryExpr) << T(ArithArg)[ArithArg]) >>
+        In(UnifyExpr, ArgSeq) * (T(UnaryExpr) << T(Expr)[Expr]) >>
           [](Match& _) {
             ACTION();
             return Function << (JSONString ^ "unary")
-                            << (ArgSeq << unwrap_node(_(ArithArg)->front()));
+                            << (ArgSeq << unwrap_node(_(Expr)->front()));
           },
 
         In(UnifyExpr, ArgSeq) *
-            (T(ArithInfix)
-             << (T(ArithArg)[Lhs] * Any[Op] * T(ArithArg)[Rhs])) >>
+            (T(ArithInfix) << (T(Expr)[Lhs] * Any[Op] * T(Expr)[Rhs])) >>
           [](Match& _) {
             ACTION();
             return Function << (JSONString ^ "arithinfix")
@@ -333,7 +355,7 @@ namespace rego
           },
 
         In(UnifyExpr, ArgSeq) *
-            (T(BinInfix) << (T(BinArg)[Lhs] * Any[Op] * T(BinArg)[Rhs])) >>
+            (T(BinInfix) << (T(Expr)[Lhs] * Any[Op] * T(Expr)[Rhs])) >>
           [](Match& _) {
             ACTION();
             return Function << (JSONString ^ "bininfix")
@@ -342,7 +364,7 @@ namespace rego
           },
 
         In(UnifyExpr, ArgSeq) *
-            (T(BoolInfix) << (T(BoolArg)[Lhs] * Any[Op] * T(BoolArg)[Rhs])) >>
+            (T(BoolInfix) << (T(Expr)[Lhs] * Any[Op] * T(Expr)[Rhs])) >>
           [](Match& _) {
             ACTION();
             return Function << (JSONString ^ "boolinfix")
@@ -404,19 +426,18 @@ namespace rego
           },
 
         In(UnifyExpr, ArgSeq) *
-            (T(ExprCall) << (T(Var)[Var] * T(ArgSeq)[ArgSeq])) >>
+            (T(ExprCall) << (T(Var)[Var] * T(ExprSeq)[ExprSeq])) >>
           [](Match& _) {
             ACTION();
             Node argseq = ArgSeq << _(Var);
-            for (auto& child : *_(ArgSeq))
+            for (auto& child : *_(ExprSeq))
             {
               argseq << unwrap_node(child);
             }
             return Function << (JSONString ^ "call") << argseq;
           },
 
-        In(RuleComp, RuleFunc, RuleObj, RuleSet, DataItem) *
-            T(DataTerm)[DataTerm] >>
+        In(RuleComp, RuleFunc, RuleObj, RuleSet) * T(DataTerm)[DataTerm] >>
           [](Match& _) {
             ACTION();
             return Term << convert_data(_(DataTerm));
diff --git a/src/unify/infix.cc b/src/unify/infix.cc
new file mode 100644
index 00000000..b78e346a
--- /dev/null
+++ b/src/unify/infix.cc
@@ -0,0 +1,51 @@
+#include "rego.hh"
+#include "unify.hh"
+
+namespace rego
+{
+  PassDef infix()
+  {
+    return PassDef{
+      "infix",
+      wf_pass_infix,
+      dir::bottomup | dir::once,
+      {
+        In(Expr) * (T(ExprParens) << T(Expr)[Expr]) >>
+          [](Match& _) { return _(Expr)->front(); },
+
+        T(ExprInfix)
+            << (T(Expr)[Lhs] * (T(InfixOperator) << T(ArithOperator)[Op]) *
+                T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return ArithInfix << _(Lhs) << _(Op)->front() << _(Rhs);
+          },
+
+        T(ExprInfix)
+            << (T(Expr)[Lhs] * (T(InfixOperator) << T(BinOperator)[Op]) *
+                T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return BinInfix << _(Lhs) << _(Op)->front() << _(Rhs);
+          },
+
+        T(ExprInfix)
+            << (T(Expr)[Lhs] * (T(InfixOperator) << T(BoolOperator)[Op]) *
+                T(Expr)[Rhs]) >>
+          [](Match& _) {
+            return BoolInfix << _(Lhs) << _(Op)->front() << _(Rhs);
+          },
+
+        T(Literal) << (T(NotExpr) << T(Expr)[Expr]) >>
+          [](Match& _) {
+            ACTION();
+            return LiteralNot << (UnifyBody << (Literal << _[Expr]));
+          },
+
+        // errors
+
+        In(Expr) * T(ExprParens)[ExprParens] >>
+          [](Match& _) {
+            return err(_(ExprParens), "missing expression", EvalTypeError);
+          },
+      }};
+  }
+}
\ No newline at end of file
diff --git a/src/passes/init.cc b/src/unify/init.cc
similarity index 99%
rename from src/passes/init.cc
rename to src/unify/init.cc
index 3ddfc191..5b11095b 100644
--- a/src/passes/init.cc
+++ b/src/unify/init.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 #include <algorithm>
 #include <cstddef>
@@ -291,8 +291,6 @@ namespace
 
 namespace rego
 {
-  using namespace wf::ops;
-
   // This pass performs initialization analysis on the program and finds
   // all assignments which initialize a variable (as opposed to unifying it
   // with an additional result). It wraps these in special LiteralInit nodes,
diff --git a/src/passes/lift_query.cc b/src/unify/lift_query.cc
similarity index 97%
rename from src/passes/lift_query.cc
rename to src/unify/lift_query.cc
index a8366206..12567a72 100644
--- a/src/passes/lift_query.cc
+++ b/src/unify/lift_query.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
diff --git a/src/passes/lift_refheads.cc b/src/unify/lift_refheads.cc
similarity index 89%
rename from src/passes/lift_refheads.cc
rename to src/unify/lift_refheads.cc
index fb1a65e1..9feb81ef 100644
--- a/src/passes/lift_refheads.cc
+++ b/src/unify/lift_refheads.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -48,6 +48,11 @@ namespace
     {
       if (rego::starts_with(ref, refhead))
       {
+        if (ref.size() > refhead.size())
+        {
+          return ref[refhead.size()] == '.';
+        }
+
         return true;
       }
     }
@@ -57,7 +62,7 @@ namespace
 
   void prepend_refs(const Node& node, Node& prefix_ref, RefHeads refheads)
   {
-    if (node->type() == Term || node->type() == RuleRef)
+    if (node == Term || node == ExprCall)
     {
       Node ref = node->front();
       if (ref->type() != Ref)
@@ -80,6 +85,11 @@ namespace
           node->replace(ref, new_ref);
         }
       }
+
+      if (node == ExprCall)
+      {
+        prepend_refs(node / ExprSeq, prefix_ref, refheads);
+      }
     }
     else
     {
@@ -106,7 +116,7 @@ namespace rego
       wf_pass_lift_refheads,
       dir::topdown,
       {
-        In(RefHead) *
+        In(RuleRef) *
             (T(Ref) << ((T(RefHead) << T(Var)[Var]) * (T(RefArgSeq) << End))) >>
           [](Match& _) {
             ACTION();
@@ -114,7 +124,7 @@ namespace rego
           },
 
         In(Policy) * T(Rule)[Rule]([](auto& n) {
-          Node rule = *n.first;
+          Node rule = n.front();
           Node ruleref = (rule / RuleHead / RuleRef)->front();
           if (ruleref->type() == Ref)
           {
@@ -142,7 +152,7 @@ namespace rego
               return prefix_ref;
             }
 
-            prepend_refs(_(Rule) / Body, prefix_ref, refheads);
+            prepend_refs(_(Rule) / RuleBodySeq, prefix_ref, refheads);
             prepend_refs(rulehead / RuleHeadType, prefix_ref, refheads);
 
             Node new_package_ref = concat_refs(package_ref, ref);
@@ -189,11 +199,16 @@ namespace rego
         return 0;
       }
 
-      prepend_refs(node / Body, prefix_ref, refheads);
+      prepend_refs(node / RuleBodySeq, prefix_ref, refheads);
       prepend_refs(rulehead / RuleHeadType, prefix_ref, refheads);
       return 0;
     });
 
+    lift_refheads.post([refheads](Node) {
+      refheads->clear();
+      return 0;
+    });
+
     return lift_refheads;
   }
 }
\ No newline at end of file
diff --git a/src/passes/lift_to_rule.cc b/src/unify/lift_to_rule.cc
similarity index 96%
rename from src/passes/lift_to_rule.cc
rename to src/unify/lift_to_rule.cc
index 12914c8d..c2505abd 100644
--- a/src/passes/lift_to_rule.cc
+++ b/src/unify/lift_to_rule.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -192,11 +192,11 @@ namespace rego
 
             // create the arguments for the rule and call
             Node ruleargs = NodeDef::create(RuleArgs);
-            Node argseq = NodeDef::create(ArgSeq);
+            Node exprseq = NodeDef::create(ExprSeq);
             for (auto& var : invars)
             {
               ruleargs << (ArgVar << (Var ^ var) << Undefined);
-              argseq << (Expr << (RefTerm << (Var ^ var)));
+              exprseq << (Expr << (RefTerm << (Var ^ var)));
             }
 
             Node rulename = Var ^ _.fresh({"enum"});
@@ -233,7 +233,7 @@ namespace rego
                   << (Expr << (Enumerate << (Expr << (RefTerm << _(ItemSeq))))))
               << (UnifyExpr
                   << _(Var)
-                  << (Expr << (ExprCall << rulename->clone() << argseq)));
+                  << (Expr << (ExprCall << rulename->clone() << exprseq)));
             for (auto var : outvars)
             {
               // we need to unify the results with the variables in the source
@@ -298,11 +298,11 @@ namespace rego
             {
               // similar to Enum above, but with a single known output
               Node ruleargs = NodeDef::create(RuleArgs);
-              Node argseq = NodeDef::create(ArgSeq);
+              Node exprseq = NodeDef::create(ExprSeq);
               for (auto& var : invars)
               {
                 ruleargs << (ArgVar << (Var ^ var) << Undefined);
-                argseq << (Expr << (RefTerm << (Var ^ var)));
+                exprseq << (Expr << (RefTerm << (Var ^ var)));
               }
 
               Node rulevalue = UnifyBody
@@ -322,7 +322,7 @@ namespace rego
                 << (Local << (Var ^ partial) << Undefined)
                 << (UnifyExpr
                     << (Var ^ partial)
-                    << (Expr << (ExprCall << rulename->clone() << argseq)))
+                    << (Expr << (ExprCall << rulename->clone() << exprseq)))
                 << (UnifyExpr << _(Var)
                               << (Expr << (Merge << (Var ^ partial))));
             }
@@ -347,18 +347,18 @@ namespace rego
             }
 
             Node ruleargs = NodeDef::create(RuleArgs);
-            Node argseq = NodeDef::create(ArgSeq);
+            Node exprseq = NodeDef::create(ExprSeq);
             for (auto& var : invars)
             {
               ruleargs << (ArgVar << (Var ^ var) << Undefined);
-              argseq << (Expr << (RefTerm << (Var ^ var)));
+              exprseq << (Expr << (RefTerm << (Var ^ var)));
             }
 
             return Seq << (Lift << DataModule
                                 << (RuleFunc << (Var ^ rulename) << ruleargs
                                              << rulebody << rulevalue
                                              << (Int ^ "0")))
-                       << (ExprCall << (Var ^ rulename) << argseq);
+                       << (ExprCall << (Var ^ rulename) << exprseq);
           },
 
         // errors
diff --git a/src/passes/locals.cc b/src/unify/locals.cc
similarity index 86%
rename from src/passes/locals.cc
rename to src/unify/locals.cc
index eeabe3fb..e9b76525 100644
--- a/src/passes/locals.cc
+++ b/src/unify/locals.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -6,10 +6,10 @@ namespace
   using Scope = std::map<Location, bool>;
 
   void add_locals(
-    Node unifybody, std::vector<Scope>& scopes, const BuiltIns& builtins);
+    Node unifybody, std::vector<Scope>& scopes, BuiltIns builtins);
 
   void find_free_vars(
-    const Node& node, std::vector<Scope>& scopes, const BuiltIns& builtins)
+    const Node& node, std::vector<Scope>& scopes, BuiltIns builtins)
   {
     std::set<Token> exclude_parents = {
       RefArgDot, RuleRef, VarSeq, ArrayCompr, SetCompr, ObjectCompr, WithSeq};
@@ -33,7 +33,7 @@ namespace
 
     if (node->type() == Var)
     {
-      if (builtins.is_builtin(node->location()))
+      if (builtins->is_builtin(node->location()))
       {
         return;
       }
@@ -73,8 +73,7 @@ namespace
     }
   }
 
-  void add_locals(
-    Node unifybody, std::vector<Scope>& scopes, const BuiltIns& builtins)
+  void add_locals(Node unifybody, std::vector<Scope>& scopes, BuiltIns builtins)
   {
     if (unifybody->type() == NestedBody)
     {
@@ -103,14 +102,14 @@ namespace
     scopes.pop_back();
   }
 
-  int preprocess_body(Node node, const BuiltIns& builtins)
+  int preprocess_body(Node node, BuiltIns builtins)
   {
     std::vector<Scope> scopes;
     add_locals(node / Body, scopes, builtins);
     return 0;
   }
 
-  int preprocess_value(Node node, const BuiltIns& builtins)
+  int preprocess_value(Node node, BuiltIns builtins)
   {
     std::vector<Scope> scopes;
     add_locals(node / Val, scopes, builtins);
@@ -122,7 +121,7 @@ namespace rego
 {
   // Discovers undeclared local variables from rule bodies and comprehensions
   // inserts Local nodes for them at the appropriate scope.
-  PassDef body_locals(const BuiltIns& builtins)
+  PassDef body_locals(BuiltIns builtins)
   {
     PassDef locals = {"body_locals", wf_pass_locals, dir::bottomup | dir::once};
 
@@ -138,7 +137,7 @@ namespace rego
     return locals;
   }
 
-  PassDef compr_locals(const BuiltIns& builtins)
+  PassDef compr_locals(BuiltIns builtins)
   {
     PassDef locals = {
       "compr_locals", wf_pass_locals, dir::bottomup | dir::once};
@@ -154,7 +153,7 @@ namespace rego
 
   // Discovers undeclared local variables from rule values and inserts Local
   // nodes for them at the appropriate scope.
-  PassDef value_locals(const BuiltIns& builtins)
+  PassDef value_locals(BuiltIns builtins)
   {
     PassDef locals = {
       "value_locals", wf_pass_locals, dir::bottomup | dir::once};
diff --git a/src/passes/merge_data.cc b/src/unify/merge_data.cc
similarity index 63%
rename from src/passes/merge_data.cc
rename to src/unify/merge_data.cc
index c0a9e734..00578b02 100644
--- a/src/passes/merge_data.cc
+++ b/src/unify/merge_data.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
@@ -11,79 +11,47 @@ namespace rego
       wf_pass_merge_data,
       dir::topdown,
       {
-        In(Input) * T(Term)[DataTerm] >>
+        T(Input) << T(DataTerm, Undefined)[Val] >>
           [](Match& _) {
             ACTION();
-            return DataTerm << *_[DataTerm];
+            return Input << (Key ^ "input") << _(Val);
           },
 
-        In(DataSeq) * (T(Data) << T(ObjectItemSeq)[Data]) >>
+        In(DataSeq) * (T(Data) << (T(DataTerm) << T(DataObject)[Data])) >>
           [](Match& _) {
             ACTION();
             return DataModule << *_[Data];
           },
 
         In(DataSeq) *
-            (T(DataModule)[Lhs] * (T(Data) << T(ObjectItemSeq)[Rhs])) >>
+            (T(DataModule)[Lhs] *
+             (T(Data) << (T(DataTerm) << T(DataObject)[Rhs]))) >>
           [](Match& _) {
             ACTION();
             return DataModule << *_[Lhs] << *_[Rhs];
           },
 
         In(DataModule) *
-            (T(ObjectItem)
-             << ((T(Expr) << (T(Term) << T(Scalar)[Scalar])) *
-                 (T(Expr) << (T(Term) << T(Object)[DataModule])))) >>
+            (T(DataObjectItem)
+             << ((T(DataTerm) << T(Scalar)[Scalar]) *
+                 (T(DataTerm) << T(DataObject)[DataModule]))) >>
           [](Match& _) {
             ACTION();
-            std::string key = strip_quotes(to_json(_(Scalar)));
+            std::string key = strip_quotes(to_key(_(Scalar)));
             return Submodule << (Key ^ key) << (DataModule << *_[DataModule]);
           },
 
         In(DataModule) *
-            (T(ObjectItem)
-             << ((T(Expr) << (T(Term) << T(Scalar)[Scalar])) *
-                 (T(Expr)
-                  << (T(Term) << (T(Array) / T(Set) / T(Scalar))[Term])))) >>
+            (T(DataObjectItem)
+             << ((T(DataTerm) << T(Scalar)[Scalar]) *
+                 (T(DataTerm)
+                  << (T(DataArray) / T(DataSet) / T(Scalar))[Term]))) >>
           [](Match& _) {
             ACTION();
-            std::string key = strip_quotes(to_json(_(Scalar)));
+            std::string key = strip_quotes(to_key(_(Scalar)));
             return DataRule << (Var ^ key) << (DataTerm << _(Term));
           },
 
-        In(DataObject) *
-            (T(ObjectItem)
-             << ((T(Expr) << T(Term)[Key]) * (T(Expr) << T(Term)[Val]))) >>
-          [](Match& _) {
-            ACTION();
-            return DataObjectItem << (DataTerm << _(Key)->front())
-                                  << (DataTerm << _(Val)->front());
-          },
-
-        In(DataTerm) * T(Array)[Array] >>
-          [](Match& _) {
-            ACTION();
-            return DataArray << *_[Array];
-          },
-
-        In(DataTerm) * T(Set)[Set] >>
-          [](Match& _) {
-            ACTION();
-            return DataSet << *_[Set];
-          },
-
-        In(DataTerm) * T(Object)[Object] >>
-          [](Match& _) {
-            ACTION();
-            return DataObject << *_[Object];
-          },
-
-        In(DataArray, DataSet) * (T(Expr) << T(Term)[Term]) >>
-          [](Match& _) {
-            ACTION();
-            return DataTerm << _(Term)->front();
-          },
-
         In(Rego) * (T(DataSeq) << (T(DataModule)[DataModule] * End)) >>
           [](Match& _) {
             ACTION();
@@ -117,12 +85,6 @@ namespace rego
             return err(_(ObjectItem), "Syntax error: unexpected object item");
           },
 
-        In(DataItem) * T(Expr)[Expr] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(Expr), "Syntax error: unexpected expression");
-          },
-
         In(DataTerm) * T(Var)[Var] >>
           [](Match& _) {
             ACTION();
@@ -158,6 +120,22 @@ namespace rego
             ACTION();
             return err(_(Term), "Invalid rule function argument");
           },
+
+        In(DataSeq) *
+            (T(Data) << (T(DataTerm) << T(DataArray, DataSet, Scalar)[Data])) >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Data), "Invalid data node");
+          },
+
+        In(DataModule) *
+            (T(DataObjectItem)
+             << (T(DataTerm) << T(DataArray, DataSet, DataObject)[Scalar])) >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Scalar), "Invalid data rule name");
+          },
+
       }};
   }
 
diff --git a/src/passes/merge_modules.cc b/src/unify/merge_modules.cc
similarity index 99%
rename from src/passes/merge_modules.cc
rename to src/unify/merge_modules.cc
index 3153e357..7f999ad3 100644
--- a/src/passes/merge_modules.cc
+++ b/src/unify/merge_modules.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
diff --git a/src/passes/replace_argvals.cc b/src/unify/replace_argvals.cc
similarity index 89%
rename from src/passes/replace_argvals.cc
rename to src/unify/replace_argvals.cc
index cdd3b019..9757360c 100644
--- a/src/passes/replace_argvals.cc
+++ b/src/unify/replace_argvals.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -39,13 +39,15 @@ namespace
     if (argval->type() == Scalar || argval->type() == Set)
     {
       stmts.push_back(
-        Literal << (Expr << (RefTerm << argref) << Unify << (Term << argval)));
+        Literal
+        << (Expr << expr_infix(Unify, RefTerm << argref, Term << argval)));
     }
     else if (argval->type() == Var)
     {
       stmts.push_back(
         Literal
-        << (Expr << (RefTerm << argval) << Unify << (RefTerm << argref)));
+        << (Expr << expr_infix(
+              Unify, (RefTerm << argval), (RefTerm << argref))));
     }
     else if (argval->type() == Array)
     {
@@ -86,7 +88,7 @@ namespace rego
       {
         In(RuleFunc) *
             (T(RuleArgs)[RuleArgs](
-               [](auto& n) { return contains_argval(*n.first); }) *
+               [](auto& n) { return contains_argval(n.front()); }) *
              (T(UnifyBody) / T(Empty))[Body]) >>
           [](Match& _) {
             ACTION();
@@ -124,16 +126,16 @@ namespace rego
           },
 
         // errors
-        In(Literal) * T(SomeExpr)[SomeExpr] >>
+        T(UnifyBody)[UnifyBody] << End >>
           [](Match& _) {
             ACTION();
-            return err(_(SomeExpr), "Invalid some expression");
+            return err(_(UnifyBody), "Invalid unification body");
           },
 
-        T(UnifyBody)[UnifyBody] << End >>
+        In(Literal) * T(SomeDecl)[SomeDecl] >>
           [](Match& _) {
             ACTION();
-            return err(_(UnifyBody), "Invalid unification body");
+            return err(_(SomeDecl), "Invalid some declaration");
           },
       }};
   }
diff --git a/src/passes/query.cc b/src/unify/result.cc
similarity index 75%
rename from src/passes/query.cc
rename to src/unify/result.cc
index 49937d3b..e126a4ee 100644
--- a/src/passes/query.cc
+++ b/src/unify/result.cc
@@ -1,24 +1,19 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
   // Extracts the query result from the AST.
-  PassDef query()
+  PassDef result()
   {
     return {
-      "query",
-      wf_pass_query,
+      "result",
+      wf_result,
       dir::topdown,
       {
         In(Top) * (T(Rego) << T(Query)[Query]) >>
           [](Match& _) {
             ACTION();
-            Node seq = NodeDef::create(Seq);
-            for (auto& child : *_(Query))
-            {
-              seq->push_back(child);
-            }
-            return seq;
+            return Result << *_[Query];
           },
 
         (T(Array) / T(Set)) * T(Error)[Error] >>
diff --git a/src/passes/rulebody.cc b/src/unify/rulebody.cc
similarity index 90%
rename from src/passes/rulebody.cc
rename to src/unify/rulebody.cc
index ecf2bac6..0191ed64 100644
--- a/src/passes/rulebody.cc
+++ b/src/unify/rulebody.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -44,7 +44,7 @@ namespace
 
       if (is_constant(key))
       {
-        rhsmap[to_json(key)] = val;
+        rhsmap[to_key(key)] = val;
       }
     }
 
@@ -59,7 +59,7 @@ namespace
       if (is_constant(key))
       {
         // constant keys have values that can be determined at compile time
-        std::string key_str = to_json(key);
+        std::string key_str = to_key(key);
 
         // gather variables to initialization from the lvalue
         Node lhs_init_vars = NodeDef::create(VarSeq);
@@ -93,7 +93,7 @@ namespace
           // if nothing is being initialized, we just need to perform
           // an equality check.
           boolinfixes.push_back(
-            BoolInfix << (BoolArg << lhsval) << Equals << (BoolArg << rhsval));
+            BoolInfix << (Expr << lhsval) << Equals << (Expr << rhsval));
         }
       }
       else
@@ -101,8 +101,8 @@ namespace
         // if the key is not constant, no initialization can take place
         // so we simple perofrm an equality check.
         boolinfixes.push_back(
-          BoolInfix << (BoolArg << lhsval) << Equals
-                    << (BoolArg
+          BoolInfix << (Expr << lhsval) << Equals
+                    << (Expr
                         << (RefTerm
                             << (SimpleRef << (Var ^ obj)
                                           << (RefArgBrack << key)))));
@@ -113,8 +113,6 @@ namespace
 
 namespace rego
 {
-  using namespace wf::ops;
-
   // Convert all Literal nodes into UnifyExpr nodes of the form <var> = <expr> |
   // <not-expr>.
   PassDef rulebody()
@@ -163,7 +161,6 @@ namespace rego
                       T(AssignArg)[Rhs])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<array> := indexable";
             Node seq = NodeDef::create(Seq);
             Node rhs = _(Rhs)->front();
             if (rhs->type() != RefTerm || rhs->front()->type() != Var)
@@ -207,11 +204,10 @@ namespace rego
                 // Otherwise create a BoolInfix.
                 Location unify = _.fresh({"unify"});
                 seq << (Local << (Var ^ unify) << Undefined)
-                    << (UnifyExpr
-                        << (Var ^ unify)
-                        << (Expr
-                            << (BoolInfix << (BoolArg << item) << Equals
-                                          << (BoolArg << rhsval))));
+                    << (UnifyExpr << (Var ^ unify)
+                                  << (Expr
+                                      << (BoolInfix << (Expr << item) << Equals
+                                                    << (Expr << rhsval))));
               }
             }
 
@@ -233,7 +229,6 @@ namespace rego
                       T(AssignArg)[Rhs])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<object> := indexable";
             Node seq = NodeDef::create(Seq);
             Node rhs = _(Rhs)->front();
             if (rhs->type() != RefTerm || rhs->front()->type() != Var)
@@ -284,11 +279,10 @@ namespace rego
                 // Otherwise create a Literal << BoolInfix.
                 Location unify = _.fresh({"unify"});
                 seq << (Local << (Var ^ unify) << Undefined)
-                    << (UnifyExpr
-                        << (Var ^ unify)
-                        << (Expr
-                            << (BoolInfix << (BoolArg << val) << Equals
-                                          << (BoolArg << rhsval))));
+                    << (UnifyExpr << (Var ^ unify)
+                                  << (Expr
+                                      << (BoolInfix << (Expr << val) << Equals
+                                                    << (Expr << rhsval))));
               }
             }
 
@@ -310,7 +304,6 @@ namespace rego
                       (T(AssignArg) << (T(Term) << T(Array)[Rhs])))))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<array> :=: <array>";
 
             if (_(Lhs)->size() != _(Rhs)->size())
             {
@@ -355,8 +348,8 @@ namespace rego
                     << (UnifyExpr
                         << (Var ^ unify)
                         << (Expr
-                            << (BoolInfix << (BoolArg << lhs_term) << Equals
-                                          << (BoolArg << rhs_term))));
+                            << (BoolInfix << (Expr << lhs_term) << Equals
+                                          << (Expr << rhs_term))));
               }
             }
 
@@ -378,7 +371,6 @@ namespace rego
                       (T(AssignArg) << (T(Term) << T(Object)[Rhs])))))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<object> :=: <object>";
             // first, we set up all equalities going lhs <- rhs
             // so for example, if we have {"a": x, "b": 2, c: 3, "d": 4} = {"a":
             // 1, "b": y, "c": 3, "d": 4} then this will generate: x =
@@ -518,7 +510,6 @@ namespace rego
         In(With) * T(Expr)[Expr] * In(UnifyBody)++ >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "with";
             Location temp = _.fresh({"with"});
             return Seq << (Lift << UnifyBody
                                 << (Local << (Var ^ temp) << Undefined))
@@ -542,16 +533,15 @@ namespace rego
             (T(AssignInfix) << (T(AssignArg)[Lhs] * T(AssignArg)[Rhs])) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<any> = <any>";
             Node seq = NodeDef::create(Seq);
             Location unify = _.fresh({"unify"});
             return Seq << (Local << (Var ^ unify) << Undefined)
                        << (UnifyExpr
                            << (Var ^ unify)
                            << (Expr
-                               << (BoolInfix << (BoolArg << _(Lhs)->front())
+                               << (BoolInfix << (Expr << _(Lhs)->front())
                                              << Equals
-                                             << (BoolArg << _(Rhs)->front()))));
+                                             << (Expr << _(Rhs)->front()))));
           },
 
         // not any = any
@@ -562,15 +552,14 @@ namespace rego
                      << (T(AssignArg)[Lhs] * T(AssignArg)[Rhs])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "not any = any";
             Location unify = _.fresh({"unify"});
             return Seq << (Local << (Var ^ unify) << Undefined)
                        << (UnifyExpr
                            << (Var ^ unify)
                            << (Expr
-                               << (BoolInfix << (BoolArg << _(Lhs)->front())
+                               << (BoolInfix << (Expr << _(Lhs)->front())
                                              << NotEquals
-                                             << (BoolArg << _(Rhs)->front()))));
+                                             << (Expr << _(Rhs)->front()))));
           },
 
         // <notexpr>
@@ -587,7 +576,6 @@ namespace rego
                  (T(AssignArg) << (T(Term) << T(Array)[Rhs])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<array> = <array>";
             Node lhs = _(Lhs);
             Node rhs = _(Rhs);
             if (lhs->size() != rhs->size())
@@ -600,9 +588,9 @@ namespace rego
                        << (UnifyExpr
                            << (Var ^ unify)
                            << (Expr
-                               << (BoolInfix << (BoolArg << (Term << lhs))
+                               << (BoolInfix << (Expr << (Term << lhs))
                                              << Equals
-                                             << (BoolArg << (Term << rhs)))));
+                                             << (Expr << (Term << rhs)))));
           },
 
         // <object> = <object>
@@ -612,7 +600,6 @@ namespace rego
                  (T(AssignArg) << (T(Term) << T(Object)[Rhs])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<object> = <object>";
 
             Node lhs = _(Lhs);
             Node rhs = _(Rhs);
@@ -626,9 +613,9 @@ namespace rego
                        << (UnifyExpr
                            << (Var ^ unify)
                            << (Expr
-                               << (BoolInfix << (BoolArg << (Term << lhs))
+                               << (BoolInfix << (Expr << (Term << lhs))
                                              << Equals
-                                             << (BoolArg << (Term << rhs)))));
+                                             << (Expr << (Term << rhs)))));
           },
 
         In(UnifyBody) * (T(LiteralEnum) << T(UnifyExpr)[UnifyExpr]) >>
@@ -647,7 +634,6 @@ namespace rego
                           T(ObjectCompr))[Compr])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<compr>";
             return UnifyExprCompr << _(Var)
                                   << (_(Compr)->type() << (_(Compr) / Var))
                                   << (_(Compr) / NestedBody);
@@ -661,7 +647,6 @@ namespace rego
                   << (T(ArrayCompr) / T(SetCompr) / T(ObjectCompr))[Compr]))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<compr>";
             return UnifyExprCompr << _(Var)
                                   << (_(Compr)->type() << (_(Compr) / Var))
                                   << (_(Compr) / NestedBody);
@@ -672,7 +657,6 @@ namespace rego
               In(UnifyBody)++ >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<compr> (other)";
             Location term = _.fresh({"term"});
             return Seq << (Lift << UnifyBody
                                 << (Local << (Var ^ term) << Undefined))
@@ -682,11 +666,10 @@ namespace rego
                        << (RefTerm << (Var ^ term));
           },
 
-        // <binarg>.<setcompr>
-        In(BinArg) * T(SetCompr)[SetCompr] * In(UnifyBody)++ >>
+        // <Expr>.<setcompr>
+        In(Expr) * T(SetCompr)[SetCompr] * In(UnifyBody)++ >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "<binarg>.<setcompr>";
             Location set = _.fresh({"setcompr"});
             return Seq << (Lift << UnifyBody
                                 << (Local << (Var ^ set) << Undefined))
@@ -704,13 +687,7 @@ namespace rego
 
         // errors
 
-        In(BoolArg) * T(BoolInfix)[BoolInfix] >>
-          [](Match& _) {
-            ACTION();
-            return err(_(BoolInfix), "Invalid boolean expression");
-          },
-
-        In(Term, BinArg) * T(SetCompr)[SetCompr] >>
+        In(Term, Expr) * T(SetCompr)[SetCompr] >>
           [](Match& _) {
             ACTION();
             return err(_(SetCompr), "Invalid set comprehension");
@@ -746,13 +723,13 @@ namespace rego
             return err(_(AssignInfix), "Invalid assignment");
           },
 
-        In(BoolArg) * (T(Membership) / T(AssignInfix))[Arg] >>
+        In(Expr) * T(AssignInfix)[Arg] >>
           [](Match& _) {
             ACTION();
             return err(_(Arg), "Invalid boolean argument");
           },
 
-        In(RefArgBrack) * (T(Membership) / T(AssignInfix))[Arg] >>
+        In(RefArgBrack) * T(AssignInfix)[Arg] >>
           [](Match& _) {
             ACTION();
             return err(_(Arg), "Invalid index");
diff --git a/src/passes/simple_refs.cc b/src/unify/simple_refs.cc
similarity index 77%
rename from src/passes/simple_refs.cc
rename to src/unify/simple_refs.cc
index 315723c4..ee9d005b 100644
--- a/src/passes/simple_refs.cc
+++ b/src/unify/simple_refs.cc
@@ -1,4 +1,14 @@
-#include "internal.hh"
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+
+  const auto inline RefArg = T(RefArgDot, RefArgBrack);
+
+  const inline auto AssignInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
+    T(ArithInfix) / T(BinInfix) / T(Term) / T(BoolInfix) / T(ExprCall);
+}
 
 namespace rego
 {
@@ -11,16 +21,21 @@ namespace rego
       wf_pass_simple_refs,
       dir::topdown,
       {
+        In(AssignArg) * (T(Expr) << (AssignInfixArg[AssignArg] * End)) >>
+          [](Match& _) {
+            ACTION();
+            return _(AssignArg);
+          },
+
         In(Module) * T(Import) >> [](Match&) -> Node { return {}; },
 
         // non-var refhead
         In(Ref) * T(RefHead)[RefHead]([](auto& n) {
-          Node head = *n.first;
+          Node head = n.front();
           return is_in(head, {UnifyBody}) && head->front()->type() != Var;
         }) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "non-var refhead";
             Location refhead = _.fresh({"refhead"});
             Node head = _(RefHead)->front();
             if (head->type() != ExprCall)
@@ -41,21 +56,19 @@ namespace rego
           },
 
         // var
-        In(RefTerm) *
+        In(RefTerm, RuleRef) *
             (T(Ref) << ((T(RefHead) << T(Var)[Var]) * (T(RefArgSeq) << End))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "var";
             return _(Var);
           },
 
         // expr-call refhead
         In(RefHead) * T(ExprCall)[ExprCall]([](auto& n) {
-          return is_in(*n.first, {UnifyBody});
+          return is_in(n.front(), {UnifyBody});
         }) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "expr-call refhead";
             Location call = _.fresh({"call"});
             return Seq
               << (Lift << UnifyBody << (Local << (Var ^ call) << Undefined))
@@ -77,7 +90,6 @@ namespace rego
                          (T(AssignArg) << (T(RefTerm)[Rhs] << T(Ref))))))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "ref = ref";
             Node seq = NodeDef::create(Seq);
             Location ref0 = _.fresh({"ref"});
             Location ref1 = _.fresh({"ref"});
@@ -104,12 +116,11 @@ namespace rego
 
         // ref.a/ref[a]
         T(RefTerm)
-            << (T(Ref)([](auto& n) { return is_in(*n.first, {UnifyBody}); })
+            << (T(Ref)([](auto& n) { return is_in(n.front(), {UnifyBody}); })
                 << ((T(RefHead) << T(Var)[Var]) *
                     (T(RefArgSeq) << (RefArg[Head] * RefArg++[Tail])))) >>
           [](Match& _) {
             ACTION();
-            logging::Debug() << "ref.a/ref[a]";
             NodeRange tail = _[Tail];
             Location ref = _.fresh({"ref"});
             Node seq = Seq
@@ -124,7 +135,7 @@ namespace rego
                                   << (RefTerm
                                       << (SimpleRef << _(Var) << _(Head))))))));
 
-            if (tail.first == tail.second)
+            if (tail.empty())
             {
               return seq << (RefTerm << (Var ^ ref));
             }
@@ -134,30 +145,39 @@ namespace rego
                   << (Ref << (RefHead << (Var ^ ref)) << (RefArgSeq << tail)));
           },
 
-        In(ExprCall) * (T(RuleRef) << T(Var)[Var]) >>
+        In(ExprCall) *
+            (T(RuleRef)
+             << (T(Ref)
+                 << ((T(RefHead) << T(Var)[Var]) * (T(RefArgSeq) << End)))) >>
           [](Match& _) {
             ACTION();
             return _(Var);
           },
 
-        In(ExprCall) * ((T(RuleRef)[RuleRef] * In(UnifyBody)++) << T(Ref)) >>
+        In(ExprCall) * (T(RuleRef) << T(Ref)[Ref]) >>
           [](Match& _) {
             ACTION();
             Location call_func = _.fresh({"call_func"});
-            return Seq
-              << (Lift << UnifyBody
-                       << (Local << (Var ^ call_func) << Undefined))
-              << (Lift
-                  << UnifyBody
-                  << (Literal
-                      << (Expr
-                          << (AssignInfix
-                              << (AssignArg << (RefTerm << (Var ^ call_func)))
-                              << (AssignArg << (RefTerm << *_[RuleRef]))))))
-              << (Var ^ call_func);
+            return Seq << (Lift << UnifyBody
+                                << (Local << (Var ^ call_func) << Undefined))
+                       << (Lift
+                           << UnifyBody
+                           << (Literal
+                               << (Expr
+                                   << (AssignInfix
+                                       << (AssignArg
+                                           << (RefTerm << (Var ^ call_func)))
+                                       << (AssignArg << (RefTerm << _(Ref)))))))
+                       << (Var ^ call_func);
           },
 
         // errors
+        In(AssignArg) * T(Expr)[Expr] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Expr), "Invalid assignment argument");
+          },
+
         T(Expr)[Expr] << (Any * Any) >>
           [](Match& _) {
             ACTION();
diff --git a/src/passes/skip_refs.cc b/src/unify/skip_refs.cc
similarity index 81%
rename from src/passes/skip_refs.cc
rename to src/unify/skip_refs.cc
index 4c1d73d5..f3f33b29 100644
--- a/src/passes/skip_refs.cc
+++ b/src/unify/skip_refs.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -30,7 +30,7 @@ namespace
       }
       else if (refarg->type() == RefArgBrack)
       {
-        refarg_str = strip_quotes(to_json(refarg->front()));
+        refarg_str = strip_quotes(to_key(refarg->front()));
         if (!all_alnum(refarg_str))
         {
           refarg_str = "[\"" + refarg_str + "\"]";
@@ -57,14 +57,12 @@ namespace
   }
 
   void find_used_builtins(
-    const Node& node,
-    const BuiltIns& builtins,
-    std::set<std::string>& used_builtins)
+    const Node& node, BuiltIns builtins, std::set<std::string>& used_builtins)
   {
     if (node->type() == Var)
     {
       std::string path = std::string(node->location().view());
-      if (builtins.is_builtin(path))
+      if (builtins->is_builtin(path))
       {
         used_builtins.insert(path);
       }
@@ -84,12 +82,12 @@ namespace
           }
           else if (refarg->type() == RefArgBrack)
           {
-            oss << "[" << strip_quotes(to_json(refarg->front())) << "]";
+            oss << "[" << strip_quotes(to_key(refarg->front())) << "]";
           }
         }
 
         std::string path = oss.str();
-        if (builtins.is_builtin(path))
+        if (builtins->is_builtin(path))
         {
           used_builtins.insert(path);
         }
@@ -112,17 +110,17 @@ namespace
 namespace rego
 {
   // Elides references to skips for efficiency. Includes built-ins.
-  PassDef skip_refs(const BuiltIns& builtins)
+  PassDef skip_refs(BuiltIns builtins)
   {
     SkipSet skips = std::make_shared<std::set<std::string>>();
 
     PassDef skip_refs{
       "skip_refs",
       wf_pass_skip_refs,
-      dir::topdown,
+      dir::bottomup | dir::once,
       {
-        In(RuleRef, RefTerm) * T(Ref)[Ref]([skips](auto& n) {
-          return skip_prefix_ref(skips, *n.first).length > 0;
+        T(Ref)[Ref]([skips](auto& n) {
+          return skip_prefix_ref(skips, n.front()).length > 0;
         }) >>
           [skips](Match& _) {
             ACTION();
@@ -130,10 +128,6 @@ namespace rego
             Node refargseq = (_(Ref) / RefArgSeq);
             refargseq->erase(
               refargseq->begin(), refargseq->begin() + skip.length);
-            if (refargseq->size() == 0)
-            {
-              return Var ^ skip.path;
-            }
 
             return Ref << (RefHead << (Var ^ skip.path)) << refargseq;
           },
@@ -159,14 +153,12 @@ namespace rego
       }
 
       // get all module symbols
-      std::set<Token> module_types = {DataItem, Submodule};
-      Nodes dataitems;
-      node->get_symbols(dataitems, [module_types](Node n) {
-        return contains(module_types, n->type());
-      });
-      for (auto dataitem : dataitems)
+      std::set<Token> module_types = {Submodule};
+      Nodes submodules;
+      node->get_symbols(submodules, [](Node n) { return n == Submodule; });
+      for (auto submodule : submodules)
       {
-        std::string path = std::string((dataitem / Key)->location().view());
+        std::string path = std::string((submodule / Key)->location().view());
         skips->insert(path);
       }
 
@@ -190,6 +182,8 @@ namespace rego
         }
       }
 
+      skips->clear();
+
       return changes;
     });
 
diff --git a/src/passes/skips.cc b/src/unify/skips.cc
similarity index 96%
rename from src/passes/skips.cc
rename to src/unify/skips.cc
index 1c6d34ba..74c538b3 100644
--- a/src/passes/skips.cc
+++ b/src/unify/skips.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -135,6 +135,11 @@ namespace rego
       return 0;
     });
 
+    skips.post([skip_links](Node) {
+      skip_links->clear();
+      return 0;
+    });
+
     return skips;
   }
 }
diff --git a/src/unify/strings.cc b/src/unify/strings.cc
new file mode 100644
index 00000000..28e66e9e
--- /dev/null
+++ b/src/unify/strings.cc
@@ -0,0 +1,32 @@
+#include "unify.hh"
+
+#include <trieste/json.h>
+
+namespace rego
+{
+  // Convert all RawString nodes to JSONString nodes.
+  PassDef strings()
+  {
+    return {
+      "strings",
+      wf_pass_strings,
+      dir::topdown,
+      {
+        In(String) * T(RawString)[RawString] >>
+          [](Match& _) {
+            ACTION();
+            std::string raw_string =
+              std::string(_(RawString)->location().view());
+            raw_string = raw_string.substr(1, raw_string.size() - 2);
+            std::string json_string = '"' + json::escape(raw_string) + '"';
+            return JSONString ^ json_string;
+          },
+
+        In(Scalar) * (T(String) << T(JSONString)[JSONString]) >>
+          [](Match& _) {
+            ACTION();
+            return _(JSONString);
+          },
+      }};
+  }
+}
\ No newline at end of file
diff --git a/src/unify/symbols.cc b/src/unify/symbols.cc
new file mode 100644
index 00000000..ef83512e
--- /dev/null
+++ b/src/unify/symbols.cc
@@ -0,0 +1,790 @@
+#include "rego.hh"
+#include "trieste/ast.h"
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+  using namespace wf::ops;
+
+  void find_assigned_vars(const Node& node, Nodes& vars)
+  {
+    if (node->type() == LiteralWith || node->type() == SomeDecl)
+    {
+      return;
+    }
+
+    if (node->type() == Var)
+    {
+      Nodes defs = node->lookup();
+      if (defs.size() == 0)
+      {
+        vars.push_back(node->clone());
+      }
+    }
+    else
+    {
+      for (auto& child : *node)
+      {
+        find_assigned_vars(child, vars);
+      }
+    }
+  }
+
+  const inline auto LocalToken = T(RefTerm) / T(Term);
+}
+
+namespace rego
+{
+  // Process most nodes that will be referenced by symbols so that they can be
+  // appropriately indexed. Also processes some nodes into forms that will
+  // be easier to process for later passes.
+  PassDef symbols()
+  {
+    return {
+      "symbols",
+      wf_pass_symbols,
+      dir::topdown,
+      {
+        In(Query) * (T(Literal)[Head] * T(Literal)++[Tail]) >>
+          [](Match& _) {
+            ACTION();
+            return UnifyBody << _(Head) << _[Tail];
+          },
+
+        In(Module) * (T(ImportSeq)[ImportSeq] * T(Policy)[Policy]) >>
+          [](Match& _) {
+            ACTION();
+            return Policy << *_[ImportSeq] << *_[Policy];
+          },
+
+        T(Literal)
+            << (T(SomeDecl, Expr, NotExpr)[Expr] * (T(WithSeq) << End)) >>
+          [](Match& _) {
+            ACTION();
+            return Literal << _(Expr);
+          },
+
+        T(Literal)
+            << (T(SomeDecl, Expr, NotExpr)[Expr] * T(WithSeq)[WithSeq]) >>
+          [](Match& _) {
+            ACTION();
+            return LiteralWith << (UnifyBody << (Literal << _(Expr)))
+                               << _(WithSeq);
+          },
+
+        In(With) * (T(Term) << T(Var, Ref)[RuleRef]) >>
+          [](Match& _) { return RuleRef << _(RuleRef); },
+
+        In(ExprCall) * T(Ref)[Ref] >>
+          [](Match& _) {
+            ACTION();
+            return RuleRef << _(Ref);
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(False) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadComp) << T(Expr)[Expr]))) *
+                 T(RuleBodySeq)[RuleBodySeq])) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t num_items = _(RuleBodySeq)->size();
+
+            Node val = _(Expr);
+            if (is_constant(val->front()))
+            {
+              val = val->front();
+            }
+
+            if (num_items == 0)
+            {
+              return RuleComp << _(Id) << Empty << val << (Int ^ "0");
+            }
+
+            if (num_items == 1)
+            {
+              Node item = _(RuleBodySeq)->front();
+              Node body = UnifyBody ^ item;
+              body->insert(body->end(), item->begin(), item->end());
+              return RuleComp << _(Id) << body << val << (Int ^ "0");
+            }
+
+            // Else precedence is per rule chain. This means that
+            // each set of else sequences must be evaluated individually
+            // and resolve to a value (or undefined). This is to deal
+            // with rules of the following kind:
+            //   conflict_1 {
+            //     false
+            //   } else = true {
+            //     true
+            //   }
+            //
+            //   conflict_1 = false
+            //
+            // In this case, conflict_1 will resolve to two (conflicting)
+            // values. This is why we have to create a proxy rule below.
+            // Do NOT optimise this away.
+            Node rule_seq = NodeDef::create(Seq);
+
+            // first we create all the variants of the rule
+            Node rule_id = Var ^ _.fresh(_(Id)->location());
+            for (std::size_t i = 0; i < num_items; ++i)
+            {
+              Node item = _(RuleBodySeq)->at(i);
+              Node body;
+              if (item == Query)
+              {
+                body = UnifyBody ^ item;
+                body->insert(body->end(), item->begin(), item->end());
+                rule_seq
+                  << (RuleComp << rule_id->clone() << body << val->clone()
+                               << (Int ^ std::to_string(i)));
+              }
+              else if (item == Else)
+              {
+                Node item_val = item / Expr;
+                if (is_constant(item_val->front()))
+                {
+                  item_val = item_val->front();
+                }
+
+                Node query = item / Query;
+                body = UnifyBody ^ query;
+                body->insert(body->end(), query->begin(), query->end());
+                rule_seq
+                  << (RuleComp << rule_id->clone() << body << item_val
+                               << (Int ^ std::to_string(i)));
+              }
+            }
+
+            // then we create the proxy rule
+            rule_seq
+              << (RuleComp << _(Id) << Empty
+                           << (Expr << (Term << (Var ^ rule_id)))
+                           << (Int ^ "0"));
+
+            return rule_seq;
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(True) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadComp) << T(Expr)[Expr]))) *
+                 (T(RuleBodySeq) << End))) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t rank = std::numeric_limits<std::uint16_t>::max();
+            return RuleComp << _(Id) << Empty << _(Expr)
+                            << (Int ^ std::to_string(rank));
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(False) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadFunc)
+                       << (T(RuleArgs)[RuleArgs] * T(Expr)[Expr])))) *
+                 T(RuleBodySeq)[RuleBodySeq])) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t num_items = _(RuleBodySeq)->size();
+
+            Node val = _(Expr);
+            if (is_constant(val->front()))
+            {
+              val = val->front();
+            }
+
+            if (num_items == 0)
+            {
+              return RuleFunc << _(Id) << _(RuleArgs) << Empty << val
+                              << (Int ^ "0");
+            }
+
+            if (num_items == 1)
+            {
+              Node item = _(RuleBodySeq)->front();
+              Node body = UnifyBody ^ item;
+              body->insert(body->end(), item->begin(), item->end());
+              return RuleFunc << _(Id) << _(RuleArgs) << body << val
+                              << (Int ^ "0");
+            }
+
+            // See above comment. Do not optimise this away.
+            Node rule_seq = NodeDef::create(Seq);
+
+            // create rule variants
+            Node rule_id = Var ^ _.fresh(_(Id)->location());
+            for (std::size_t i = 0; i < num_items; ++i)
+            {
+              Node item = _(RuleBodySeq)->at(i);
+              Node body;
+              if (item == Query)
+              {
+                body = UnifyBody ^ item;
+                body->insert(body->end(), item->begin(), item->end());
+                rule_seq
+                  << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
+                               << body << val->clone()
+                               << (Int ^ std::to_string(i)));
+              }
+              else if (item == Else)
+              {
+                Node item_val = item / Expr;
+                if (is_constant(item_val->front()))
+                {
+                  item_val = item_val->front();
+                }
+
+                Node query = item / Query;
+                body = UnifyBody ^ query;
+                body->insert(body->end(), query->begin(), query->end());
+                rule_seq
+                  << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
+                               << body << item_val
+                               << (Int ^ std::to_string(i)));
+              }
+              else if (item == Literal)
+              {
+                body = UnifyBody << item;
+                rule_seq
+                  << (RuleFunc << rule_id->clone() << _(RuleArgs)->clone()
+                               << body << val->clone()
+                               << (Int ^ std::to_string(i)));
+              }
+            }
+
+            // create proxy rule
+            // need to be careful here, as we need to pass through the rule args
+            Node exprseq = NodeDef::create(ExprSeq);
+            for (auto& arg : *_(RuleArgs))
+            {
+              if (arg == ArgVar)
+              {
+                exprseq->push_back(Expr << (Term << (arg / Var)->clone()));
+              }
+              else if (arg == ArgVal)
+              {
+                exprseq->push_back(Expr << (Term << arg->front()->clone()));
+              }
+              else
+              {
+                exprseq->push_back(err(arg, "Invalid rule argument"));
+              }
+            }
+
+            Location out = _.fresh({"out"});
+            Node proxy_body = UnifyBody
+              << (Literal
+                  << (Expr
+                      << (ExprInfix
+                          << (Expr << (Term << (Var ^ out)))
+                          << (InfixOperator << (AssignOperator << Assign))
+                          << (Expr
+                              << (ExprCall
+                                  << (Ref << (RefHead << rule_id) << RefArgSeq)
+                                  << exprseq)))));
+            Node proxy_val = Expr << (Term << (Var ^ out));
+            rule_seq
+              << (RuleFunc << _(Id) << _(RuleArgs) << proxy_body << proxy_val
+                           << (Int ^ "0"));
+
+            return rule_seq;
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(True) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadFunc)
+                       << (T(RuleArgs)[RuleArgs] * T(Expr)[Expr])))) *
+                 (T(RuleBodySeq) << End))) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t rank = std::numeric_limits<std::uint16_t>::max();
+            return RuleFunc << _(Id) << _(RuleArgs) << Empty << _(Expr)
+                            << (Int ^ std::to_string(rank));
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(False) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadSet) << T(Expr)[Expr]))) *
+                 T(RuleBodySeq)[RuleBodySeq])) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t num_items = _(RuleBodySeq)->size();
+
+            Node val = _(Expr);
+            if (is_constant(val->front()))
+            {
+              val = val->front();
+            }
+
+            if (num_items == 0)
+            {
+              return RuleSet << _(Id) << Empty << val << (Int ^ "0");
+            }
+
+            Node rule_seq = NodeDef::create(Seq);
+
+            for (std::size_t i = 0; i < num_items; ++i)
+            {
+              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 rule_seq;
+          },
+
+        In(Policy) *
+            (T(Rule)
+             << (T(False) *
+                 (T(RuleHead)
+                  << ((T(RuleRef) << T(Var)[Id]) *
+                      (T(RuleHeadObj) << (T(Expr)[Key] * T(Expr)[Val])))) *
+                 T(RuleBodySeq)[RuleBodySeq])) >>
+          [](Match& _) {
+            ACTION();
+            std::size_t num_items = _(RuleBodySeq)->size();
+
+            Node key = _(Key);
+            if (is_constant(key->front()))
+            {
+              key = key->front();
+            }
+            Node val = _(Val);
+            if (is_constant(val->front()))
+            {
+              val = val->front();
+            }
+
+            if (num_items == 0)
+            {
+              return RuleObj << _(Id) << Empty << key << val << (Int ^ "0");
+            }
+
+            Node rule_seq = NodeDef::create(Seq);
+
+            for (std::size_t i = 0; i < num_items; ++i)
+            {
+              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)));
+              }
+            }
+
+            return rule_seq;
+          },
+
+        In(Expr) * (T(Term) << (T(Ref) / T(Var))[Val]) >>
+          [](Match& _) {
+            ACTION();
+            return RefTerm << _(Val);
+          },
+
+        In(Expr) * (T(Term) << (T(Scalar) << (T(Int) / T(Float))[Val])) >>
+          [](Match& _) {
+            ACTION();
+            return NumTerm << _(Val);
+          },
+
+        In(RefArgBrack) * T(Var)[Var] >>
+          [](Match& _) {
+            ACTION();
+            return RefTerm << _(Var);
+          },
+
+        In(RefArgBrack) * T(ExprInfix)[ExprInfix] >>
+          [](Match& _) {
+            ACTION();
+            return Expr << _(ExprInfix);
+          },
+
+        In(UnifyBody) *
+            (T(Literal)
+             << (T(SomeDecl)
+                 << ((T(ExprSeq)[ExprSeq]
+                      << ((T(Expr) << (T(Term) << T(Var)))++ * End)) *
+                     T(Undefined)))) >>
+          [](Match& _) {
+            ACTION();
+            Node seq = NodeDef::create(Seq);
+            for (auto& var : *_(ExprSeq))
+            {
+              seq->push_back(Local << var->front()->front() << Undefined);
+            }
+            return seq;
+          },
+
+        In(UnifyBody) *
+            (T(Literal)
+             << (T(SomeDecl)
+                 << ((T(ExprSeq) << (T(Expr)[Item] * End)) *
+                     T(Expr)[ItemSeq]))) >>
+          [](Match& _) {
+            ACTION();
+            Location item = _.fresh({"item"});
+            return Seq << (Local << (Var ^ item) << Undefined)
+                       << (LiteralEnum << (Var ^ item) << _(ItemSeq))
+                       << (Literal
+                           << (Expr << expr_infix(
+                                 Unify,
+                                 _(Item),
+                                 (RefTerm
+                                  << (Ref
+                                      << (RefHead << (Var ^ item))
+                                      << (RefArgSeq
+                                          << (RefArgBrack
+                                              << (Scalar << (Int ^ "1")))))))));
+          },
+
+        In(UnifyBody) *
+            (T(Literal)
+             << (T(SomeDecl)
+                 << ((T(ExprSeq) << (T(Expr)[Idx] * T(Expr)[Item] * End)) *
+                     T(Expr)[ItemSeq]))) >>
+          [](Match& _) {
+            ACTION();
+            Location item = _.fresh({"item"});
+            return Seq << (Local << (Var ^ item) << Undefined)
+                       << (LiteralEnum << (Var ^ item) << _(ItemSeq))
+                       << (Literal
+                           << (Expr << expr_infix(
+                                 Unify,
+                                 _(Idx),
+                                 (RefTerm
+                                  << (Ref
+                                      << (RefHead << (Var ^ item))
+                                      << (RefArgSeq
+                                          << (RefArgBrack
+                                              << (Scalar << (Int ^ "0")))))))))
+                       << (Literal
+                           << (Expr << expr_infix(
+                                 Unify,
+                                 _(Item),
+                                 (RefTerm
+                                  << (Ref
+                                      << (RefHead << (Var ^ item))
+                                      << (RefArgSeq
+                                          << (RefArgBrack
+                                              << (Scalar << (Int ^ "1")))))))));
+          },
+
+        In(UnifyBody, LiteralWith) *
+            (T(Literal)
+             << (T(Expr)
+                 << (T(ExprInfix)
+                     << (T(Expr)[Lhs] *
+                         (T(InfixOperator)
+                          << (T(AssignOperator) << T(Assign))) *
+                         T(Expr)[Rhs])))) >>
+          [](Match& _) {
+            ACTION();
+            // this is now an infinite loop
+            Node seq = NodeDef::create(Seq);
+            Nodes vars;
+            find_assigned_vars(_(Lhs), vars);
+
+            for (auto& var : vars)
+            {
+              seq->push_back(Lift << UnifyBody << (Local << var << Undefined));
+            }
+
+            seq << (Literal << (Expr << expr_infix(Unify, _(Lhs), _(Rhs))));
+
+            return seq;
+          },
+
+        In(RuleComp, RuleFunc) * (T(Empty) * T(Expr)[Expr]) >>
+          [](Match& _) {
+            ACTION();
+            Location out = _.fresh({"out"});
+            Location value = _.fresh({"value"});
+            return Seq
+              << (UnifyBody
+                  << (Literal
+                      << (Expr << expr_infix(
+                            Unify, (RefTerm << (Var ^ out)), _(Expr)))))
+              << (UnifyBody
+                  << (Literal
+                      << (Expr << expr_infix(
+                            Unify,
+                            (RefTerm << (Var ^ value)),
+                            (RefTerm << (Var ^ out))))));
+          },
+
+        In(RuleComp, RuleFunc) * T(Expr)[Expr] >>
+          [](Match& _) {
+            ACTION();
+            Location value = _.fresh({"value"});
+            return UnifyBody
+              << (Literal
+                  << (Expr << expr_infix(
+                        Unify, (RefTerm << (Var ^ value)), _(Expr))));
+          },
+
+        T(ArrayCompr) << (T(Expr)[Val] * T(Query)[Query]) >>
+          [](Match& _) {
+            ACTION();
+            Node compr = ArrayCompr << _(Val);
+            Node body = UnifyBody ^ _(Query);
+            body->insert(body->end(), _(Query)->begin(), _(Query)->end());
+            return compr
+              << (NestedBody << (Key ^ _.fresh({"arraycompr"})) << body);
+          },
+
+        T(SetCompr) << (T(Expr)[Val] * T(Query)[Query]) >>
+          [](Match& _) {
+            ACTION();
+            Node compr = SetCompr << _(Val);
+            Node body = UnifyBody ^ _(Query);
+            body->insert(body->end(), _(Query)->begin(), _(Query)->end());
+            return compr
+              << (NestedBody << (Key ^ _.fresh({"setcompr"})) << body);
+          },
+
+        T(ObjectCompr) << (T(Expr)[Key] * T(Expr)[Val] * T(Query)[Query]) >>
+          [](Match& _) {
+            ACTION();
+            Node compr = ObjectCompr << _(Key) << _(Val);
+            Node body = UnifyBody ^ _(Query);
+            body->insert(body->end(), _(Query)->begin(), _(Query)->end());
+            return compr
+              << (NestedBody << (Key ^ _.fresh({"objectcompr"})) << body);
+          },
+
+        In(Expr) *
+            ((T(ExprEvery) * In(UnifyBody)++)
+             << ((T(VarSeq) << (T(Var)[Val] * End)) *
+                 T(Term, ExprCall, ExprInfix)[IsIn] * T(Query)[Query])) >>
+          [](Match& _) {
+            ACTION();
+            Location item = _.fresh({"item"});
+            Location itemseq = _.fresh({"itemseq"});
+            Node query =
+              Query << (Local << (Var ^ item) << Undefined)
+                    << (LiteralEnum << (Var ^ item)
+                                    << (Expr << (RefTerm << (Var ^ itemseq))))
+                    << (Literal
+                        << (Expr << expr_infix(
+                              Unify,
+                              (RefTerm << _(Val)->clone()),
+                              (RefTerm
+                               << (Ref << (RefHead << (Var ^ item))
+                                       << (RefArgSeq
+                                           << (RefArgBrack
+                                               << (Scalar << (Int ^ "1")))))))))
+                    << *_[Query];
+
+            return Seq
+              << (Lift << UnifyBody << (Local << (Var ^ itemseq) << Undefined))
+              << (Lift
+                  << UnifyBody
+                  << (Literal
+                      << (Expr << expr_infix(
+                            Unify, (RefTerm << (Var ^ itemseq)), _(IsIn)))))
+              << expr_infix(
+                     Equals,
+                     ExprCall
+                       << (Ref << (RefHead << (Var ^ "count")) << RefArgSeq)
+                       << (ExprSeq
+                           << (Expr
+                               << (Term
+                                   << (SetCompr
+                                       << (Expr << (RefTerm << _(Val)->clone()))
+                                       << query)))),
+                     ExprCall
+                       << (Ref << (RefHead << (Var ^ "count")) << RefArgSeq)
+                       << (ExprSeq
+                           << (Expr
+                               << (ExprCall
+                                   << (Ref << (RefHead << (Var ^ "cast_set"))
+                                           << RefArgSeq)
+                                   << (ExprSeq
+                                       << (Expr
+                                           << (RefTerm
+                                               << (Var ^ itemseq))))))));
+          },
+
+        In(Expr) *
+            ((T(ExprEvery) * In(UnifyBody)++)
+             << ((T(VarSeq) << (T(Var)[Idx] * T(Var)[Val] * End)) *
+                 T(Term, ExprCall, ExprInfix)[IsIn] * T(Query)[Query])) >>
+          [](Match& _) {
+            ACTION();
+            Location item = _.fresh({"item"});
+            Location itemseq = _.fresh({"itemseq"});
+            Node query =
+              Query << (Local << (Var ^ item) << Undefined)
+                    << (LiteralEnum << (Var ^ item)
+                                    << (Expr << (RefTerm << (Var ^ itemseq))))
+                    << (Literal
+                        << (Expr << expr_infix(
+                              Unify,
+                              (RefTerm << _(Idx)->clone()),
+                              (RefTerm
+                               << (Ref << (RefHead << (Var ^ item))
+                                       << (RefArgSeq
+                                           << (RefArgBrack
+                                               << (Scalar << (Int ^ "0")))))))))
+                    << (Literal
+                        << (Expr << expr_infix(
+                              Unify,
+                              (RefTerm << _(Val)->clone()),
+                              (RefTerm
+                               << (Ref << (RefHead << (Var ^ item))
+                                       << (RefArgSeq
+                                           << (RefArgBrack
+                                               << (Scalar << (Int ^ "1")))))))))
+                    << *_[Query];
+
+            return Seq
+              << (Lift << UnifyBody << (Local << (Var ^ itemseq) << Undefined))
+              << (Lift
+                  << UnifyBody
+                  << (Literal
+                      << (Expr << expr_infix(
+                            Unify, (RefTerm << (Var ^ itemseq)), _(IsIn)))))
+              << expr_infix(
+                     Equals,
+                     ExprCall
+                       << (Ref << (RefHead << (Var ^ "count")) << RefArgSeq)
+                       << (ExprSeq
+                           << (Expr
+                               << (Term
+                                   << (ArrayCompr
+                                       << (Expr << (RefTerm << _(Val)->clone()))
+                                       << query)))),
+                     ExprCall
+                       << (Ref << (RefHead << (Var ^ "count")) << RefArgSeq)
+                       << (ExprSeq << (Expr << (RefTerm << (Var ^ itemseq)))));
+          },
+
+        In(RuleComp, RuleFunc, RuleObj, RuleSet) * (T(Term) << T(Ref)[Ref]) >>
+          [](Match& _) {
+            ACTION();
+            return UnifyBody << (Literal << (Expr << (RefTerm << _(Ref))));
+          },
+
+        T(Placeholder)[Placeholder] * In(UnifyBody)++ >>
+          [](Match& _) {
+            ACTION();
+            auto name = _.fresh({"_"});
+            return Seq << (Lift << UnifyBody
+                                << (Local << (Var ^ name) << Undefined))
+                       << (Var ^ name);
+          },
+
+        // errors
+
+        In(Policy) * T(Rule)[Rule] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Rule), "Invalid rule");
+          },
+
+        In(UnifyBody) * (T(RuleHead)[RuleHead] << (T(Var) * T(RuleHeadFunc))) >>
+          [](Match& _) {
+            ACTION();
+            return err(_(RuleHead), "No rule functions allowed in rule bodies");
+          },
+
+        In(Term) * T(Ref)[Ref] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Ref), "Invalid ref term");
+          },
+
+        In(Term) * T(Var)[Var] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Var), "Invalid var term");
+          },
+
+        In(Literal) * T(SomeDecl)[SomeDecl] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(SomeDecl), "Invalid some declaration");
+          },
+
+        In(Expr) * T(Assign)[Assign] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Assign), "Invalid assignment");
+          },
+
+        T(UnifyBody)[UnifyBody] << End >>
+          [](Match& _) {
+            ACTION();
+            return err(_(UnifyBody), "Invalid unification body");
+          },
+
+        In(Expr) * T(Dot)[Dot] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Dot), "Invalid dot expression");
+          },
+
+        In(Expr) * T(ExprEvery)[ExprEvery] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(ExprEvery), "Invalid every expression");
+          },
+
+        In(With) * T(Term)[Term] >>
+          [](Match& _) {
+            ACTION();
+            return err(_(Term), "Invalid with term");
+          },
+      }};
+  }
+
+}
diff --git a/src/passes/unify.cc b/src/unify/unify.cc
similarity index 78%
rename from src/passes/unify.cc
rename to src/unify/unify.cc
index 43008036..b6e20f5d 100644
--- a/src/passes/unify.cc
+++ b/src/unify/unify.cc
@@ -1,19 +1,23 @@
-#include "internal.hh"
+#include "unify.hh"
+
+namespace
+{
+  using namespace rego;
+
+  const inline auto ScalarToken =
+    T(Int) / T(Float) / T(True) / T(False) / T(Null);
+}
 
 namespace rego
 {
   // Performs unification.
-  PassDef unify(const BuiltIns& builtins)
+  PassDef unifier(BuiltIns builtins)
   {
     PassDef unify = {
       "unify",
       wf_pass_unify,
       dir::bottomup | dir::once,
       {
-        In(Input) * T(Term) >>
-          ([](Match&) -> Node { return Term << (Scalar << Null); }),
-        In(Data) * T(DataModule) >> ([](Match&) -> Node { return DataModule; }),
-        In(Rego) * T(SkipSeq) >> ([](Match&) -> Node { return SkipSeq; }),
         In(Query, Array, Set, ObjectItem) *
             (ScalarToken / T(JSONString))[Scalar] >>
           [](Match& _) {
@@ -46,11 +50,6 @@ namespace rego
             ACTION();
             return Object << *_[DataObject];
           },
-        In(Object) * T(DataItem)[DataItem] >>
-          [](Match& _) {
-            ACTION();
-            return ObjectItem << *_[DataItem];
-          },
       }};
 
     unify.pre(Rego, [builtins](Node node) {
@@ -70,6 +69,13 @@ namespace rego
       return 0;
     });
 
+    unify.post(Rego, [](Node node) {
+      node / Input / Val = Term << (Scalar << (Null ^ "null"));
+      node / Data / Val = DataModule;
+      node / SkipSeq = SkipSeq;
+      return 0;
+    });
+
     return unify;
   }
 }
\ No newline at end of file
diff --git a/src/value.cc b/src/value.cc
index 81d26283..ef3ab3ad 100644
--- a/src/value.cc
+++ b/src/value.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace
 {
@@ -21,7 +21,7 @@ namespace rego
     m_invalid(false),
     m_rank(rank)
   {
-    m_json = to_json(m_node);
+    m_json = to_key(m_node);
     std::ostringstream os;
     ValueDef::build_string(os, *this, m_var, true);
     m_str = os.str();
@@ -282,7 +282,7 @@ namespace rego
 
   rank_t ValueDef::get_rank(const Node& node)
   {
-    return std::stoul(to_json(node));
+    return std::stoul(to_key(node));
   }
 
   void ValueDef::reduce_set()
diff --git a/src/value_map.cc b/src/value_map.cc
index 1407edd6..ae86273b 100644
--- a/src/value_map.cc
+++ b/src/value_map.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
diff --git a/src/variable.cc b/src/variable.cc
index e8c06e3f..304f0bac 100644
--- a/src/variable.cc
+++ b/src/variable.cc
@@ -1,4 +1,4 @@
-#include "internal.hh"
+#include "unify.hh"
 
 namespace rego
 {
diff --git a/src/yaml.cc b/src/yaml.cc
new file mode 100644
index 00000000..90a08e50
--- /dev/null
+++ b/src/yaml.cc
@@ -0,0 +1,123 @@
+#include "trieste/yaml.h"
+
+#include "trieste/json.h"
+#include "unify.hh"
+
+namespace
+{
+  using namespace trieste;
+  using namespace rego;
+  using namespace wf::ops;
+
+  // clang-format off
+  const auto wf_binding_term =
+    (Top <<= Term)
+    | (Term <<= Scalar | Array | Object | Set)
+    | (Scalar <<= Int | Float | True | False | Null | JSONString)
+    | (Array <<= Term++)
+    | (Object <<= ObjectItem++)
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
+    | (Set <<= Term++)
+    ;
+  // clang-format on
+
+  PassDef to_yaml_()
+  {
+    return {
+      "to_yaml",
+      yaml::wf,
+      dir::topdown,
+      {
+        In(Top) *
+            T(yaml::Int,
+              yaml::Float,
+              yaml::Value,
+              yaml::True,
+              yaml::False,
+              yaml::Null,
+              yaml::Mapping,
+              yaml::Sequence)[yaml::Document] >>
+          [](Match& _) {
+            return yaml::Stream
+              << yaml::Directives
+              << (yaml::Documents
+                  << (yaml::Document << yaml::Directives << yaml::DocumentStart
+                                     << _(yaml::Document)
+                                     << yaml::DocumentEnd));
+          },
+
+        (T(Term) << (T(Scalar) << T(Int)[Int])) >>
+          [](Match& _) { return yaml::Int ^ _(Int); },
+
+        (T(Term) << (T(Scalar) << T(Float)[Float])) >>
+          [](Match& _) { return yaml::Float ^ _(Float); },
+
+        (T(Term) << (T(Scalar) << T(JSONString)[JSONString])) >>
+          [](Match& _) { return yaml::Value ^ _(JSONString); },
+
+        (T(Term) << (T(Scalar) << T(True)[True])) >>
+          [](Match& _) { return yaml::True ^ _(True); },
+
+        (T(Term) << (T(Scalar) << T(False)[False])) >>
+          [](Match& _) { return yaml::False ^ _(False); },
+
+        (T(Term) << (T(Scalar) << T(Null)[Null])) >>
+          [](Match& _) { return yaml::Null ^ _(Null); },
+
+        (T(Term) << (T(Object)[Object])) >>
+          [](Match& _) { return yaml::Mapping << *_[Object]; },
+
+        T(ObjectItem)[ObjectItem] >>
+          [](Match& _) { return yaml::MappingItem << *_[ObjectItem]; },
+
+        (T(Term) << (T(Array, Set)[Array])) >>
+          [](Match& _) {
+            Node array = _(Array);
+            Nodes children(array->begin(), array->end());
+            if (array == Set)
+            {
+              NodeMap<std::string> strs;
+              std::sort(
+                children.begin(), children.end(), [&strs](Node a, Node b) {
+                  std::string lhs;
+                  if (contains(strs, a))
+                  {
+                    lhs = strs[a];
+                  }
+                  else
+                  {
+                    lhs = rego::to_key(a);
+                    strs[a] = lhs;
+                  }
+
+                  std::string rhs;
+                  if (contains(strs, b))
+                  {
+                    rhs = strs[b];
+                  }
+                  else
+                  {
+                    rhs = rego::to_key(b);
+                    strs[b] = rhs;
+                  }
+
+                  return lhs < rhs;
+                });
+            }
+            return yaml::Sequence << children;
+          },
+      }};
+  }
+}
+
+namespace rego
+{
+  Rewriter to_yaml()
+  {
+    return {
+      "to_yaml",
+      {to_yaml_()},
+      wf_binding_term,
+    };
+  }
+}
\ No newline at end of file
diff --git a/templates/vscode/launch.json b/templates/vscode/launch.json
index fa9d8d2d..813d0124 100644
--- a/templates/vscode/launch.json
+++ b/templates/vscode/launch.json
@@ -222,6 +222,7 @@
       "args": [
         "-d",
         "./examples/every.rego",
+        "./examples/data2.json",
         "-q",
         "data.every.output"
       ],
@@ -304,10 +305,10 @@
     {
       "type": "lldb",
       "request": "launch",
-      "name": "trieste",
-      "program": "${workspaceFolder}/build/dist/bin/rego_trieste",
+      "name": "fuzzer",
+      "program": "${workspaceFolder}/build/dist/bin/rego_fuzzer",
       "args": [
-        "test",
+        "reader",
         "-f"
       ],
       "cwd": "${workspaceFolder}/build/dist/"
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 5da3ec61..4fc372be 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -6,9 +6,9 @@ target_link_libraries(rego_interpreter
 
 set_target_properties(rego_interpreter PROPERTIES OUTPUT_NAME "rego")
 
-add_executable(rego_trieste driver.cc)
+add_executable(rego_fuzzer fuzzer.cc)
 
-target_link_libraries(rego_trieste
+target_link_libraries(rego_fuzzer
   PRIVATE
   regocpp::rego)
 
diff --git a/tools/driver.cc b/tools/driver.cc
deleted file mode 100644
index 2fb10b95..00000000
--- a/tools/driver.cc
+++ /dev/null
@@ -1,10 +0,0 @@
-#include <rego/rego.hh>
-#include <trieste/driver.h>
-
-int main(int argc, char** argv)
-{
-  auto builtins = rego::BuiltIns().register_standard_builtins();
-  trieste::Driver driver(
-    "rego", nullptr, rego::parser(), rego::passes(builtins));
-  return driver.run(argc, argv);
-}
diff --git a/tools/examples/keywords.rego b/tools/examples/keywords.rego
index 7c726f34..3a15e2e0 100644
--- a/tools/examples/keywords.rego
+++ b/tools/examples/keywords.rego
@@ -28,6 +28,8 @@ e contains "0" if {
 
 e contains "a" if {
     input.a > a
+} {
+    input.a == a
 }
 
 e contains "1" if input.key == "sum"
@@ -42,7 +44,11 @@ f["a"] := a
 f["b"] := b
 f["c"] if c
 f["sum"] := sum {
+    input.a > a
     sum := a + b
+} {
+    input.a <= a
+    sum := a * 2 + b
 }
 
 app_to_hostnames_obj[app_name] := hostnames if {
diff --git a/tools/fuzzer.cc b/tools/fuzzer.cc
new file mode 100644
index 00000000..279b7214
--- /dev/null
+++ b/tools/fuzzer.cc
@@ -0,0 +1,64 @@
+#include "trieste/logging.h"
+
+#include <CLI/CLI.hpp>
+#include <rego/rego.hh>
+#include <trieste/fuzzer.h>
+
+using namespace trieste;
+
+int main(int argc, char** argv)
+{
+  CLI::App app;
+
+  app.set_help_all_flag("--help-all", "Expand all help");
+
+  std::string transform;
+  app.add_option("transform", transform, "Transform to test")
+    ->check(CLI::IsMember({"reader", "unify"}))
+    ->required(true);
+
+  uint32_t seed = std::random_device()();
+  app.add_option("-s,--seed", seed, "Random seed");
+
+  uint32_t count = 100;
+  app.add_option("-c,--count", count, "Number of seed to test");
+
+  bool failfast = false;
+  app.add_flag("-f,--failfast", failfast, "Stop on first failure");
+
+  std::string log_level;
+  app
+    .add_option(
+      "-l,--log_level",
+      log_level,
+      "Set Log Level to one of "
+      "Trace, Debug, Info, "
+      "Warning, Output, Error, "
+      "None")
+    ->check(logging::set_log_level_from_string);
+
+  try
+  {
+    app.parse(argc, argv);
+  }
+  catch (const CLI::ParseError& e)
+  {
+    return app.exit(e);
+  }
+
+  logging::Output() << "Testing x" << count << ", seed: " << seed << std::endl;
+
+  Fuzzer fuzzer;
+  Reader reader = rego::reader();
+  if (transform == "reader")
+  {
+    fuzzer = Fuzzer(reader);
+  }
+  else if (transform == "unify")
+  {
+    auto builtins = rego::BuiltInsDef::create();
+    fuzzer = Fuzzer(rego::unify(builtins), reader.parser().generators());
+  }
+
+  return fuzzer.start_seed(seed).seed_count(count).failfast(failfast).test();
+}
diff --git a/tools/main.cc b/tools/main.cc
index b10e63f3..4c9bff71 100644
--- a/tools/main.cc
+++ b/tools/main.cc
@@ -1,5 +1,6 @@
 #include <CLI/CLI.hpp>
 #include <rego/rego.hh>
+#include <trieste/logging.h>
 
 int main(int argc, char** argv)
 {
@@ -26,8 +27,16 @@ int main(int argc, char** argv)
   std::filesystem::path output;
   app.add_option("-a,--ast", output, "Output the AST");
 
-  rego::LogLevel loglevel{rego::LogLevel::None};
-  app.add_option("-l,--loglevel", loglevel, "Enable logging");
+  std::string log_level;
+  app
+    .add_option(
+      "-l,--log_level",
+      log_level,
+      "Set Log Level to one of "
+      "Trace, Debug, Info, "
+      "Warning, Output, Error, "
+      "None")
+    ->check(rego::set_log_level_from_string);
 
   try
   {
@@ -38,17 +47,20 @@ int main(int argc, char** argv)
     return app.exit(e);
   }
 
-  rego::set_log_level(loglevel);
-
   auto interpreter = rego::Interpreter();
-  interpreter.well_formed_checks_enabled(wf_checks);
+  interpreter.wf_check_enabled(wf_checks);
+  if (!output.empty())
+  {
+    interpreter.debug_enabled(true);
+    interpreter.debug_path(output);
+  }
 
   if (!input_path.empty())
   {
     if (!std::filesystem::exists(input_path))
     {
-      std::cerr << std::filesystem::weakly_canonical(input_path)
-                << " does not exist" << std::endl;
+      trieste::logging::Error() << std::filesystem::weakly_canonical(input_path)
+                                << " does not exist" << std::endl;
       return 1;
     }
 
@@ -59,8 +71,8 @@ int main(int argc, char** argv)
   {
     if (!std::filesystem::exists(path))
     {
-      std::cerr << std::filesystem::weakly_canonical(path) << " does not exist"
-                << std::endl;
+      trieste::logging::Error() << std::filesystem::weakly_canonical(path)
+                                << " does not exist" << std::endl;
       return 1;
     }
 
@@ -74,20 +86,14 @@ int main(int argc, char** argv)
     }
   }
 
-  if (!output.empty())
-  {
-    interpreter.debug_enabled(true);
-    interpreter.debug_path(output);
-  }
-
   try
   {
-    std::cout << interpreter.query(query_expr) << std::endl;
+    trieste::logging::Output() << interpreter.query(query_expr) << std::endl;
     return 0;
   }
   catch (const std::exception& e)
   {
-    std::cout << e.what() << std::endl;
+    trieste::logging::Error() << e.what() << std::endl;
     return 1;
   }
 }
diff --git a/wrappers/python/.gitignore b/wrappers/python/.gitignore
index bac992e5..eb404d99 100644
--- a/wrappers/python/.gitignore
+++ b/wrappers/python/.gitignore
@@ -7,6 +7,7 @@ __pycache__/
 
 # C extensions
 *.so
+*.so.*
 
 # Distribution / packaging
 .Python
diff --git a/wrappers/python/CMakeLists.txt b/wrappers/python/CMakeLists.txt
index 00ad729b..191e6fb6 100644
--- a/wrappers/python/CMakeLists.txt
+++ b/wrappers/python/CMakeLists.txt
@@ -30,9 +30,10 @@ FetchContent_Declare(
 FetchContent_Declare(
   pybind11
   GIT_REPOSITORY https://github.com/pybind/pybind11/
-  GIT_TAG        v2.11.1
+  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)
diff --git a/wrappers/python/README.md b/wrappers/python/README.md
index ba479f56..9a9aa277 100644
--- a/wrappers/python/README.md
+++ b/wrappers/python/README.md
@@ -164,6 +164,8 @@ all the standard builtins. The following builtins are currently supported:
 - `arrays`
 - `bits`
 - `casts`
+- `encoding`
+- `graphs`
 - `numbers`
 - `objects`
 - `regex`
@@ -173,9 +175,6 @@ all the standard builtins. The following builtins are currently supported:
 - `types`
 - `units`
 - miscellaneous
-    * `base64_encode`
-    * `base64_decode`
-    * `json.marshal`
     * `opa.runtime`
     * `print`
     * `time.now_ns`
@@ -192,16 +191,14 @@ To build with the OPA tests available for testing, use one of the following pres
 - `release-opa`
 
 At present, we are **NOT** passing the following test suites in full:
-- `base64*`
 - `crypto*`
 - `glob*`
 - `graphql`
 - `invalidkeyerror`
-- `json*`
+- `json*`  (except `jsonbuiltins`)
 - `jwt*`
 - `net*`
 - `planner-ir`
 - `providers-aws`
-- `reachable`
-- `urlbuiltins`
+- `time`
 - `walkbuiltin`
diff --git a/wrappers/python/src/_regopy.cc b/wrappers/python/src/_regopy.cc
index a2fca230..39103a2e 100644
--- a/wrappers/python/src/_regopy.cc
+++ b/wrappers/python/src/_regopy.cc
@@ -76,6 +76,11 @@ PYBIND11_MODULE(_regopy, m)
     &regoSetLogLevel,
     "Sets the level of logging.",
     "level"_a);
+  m.def(
+    "regoSetLogLevelFromString",
+    &regoSetLogLevelFromString,
+    "Sets the level of logging.",
+    "level"_a);
   m.def("regoNew", &regoNew, "Returns a pointer to a new rego instance.");
   m.def("regoFree", &regoFree, "Deletes a rego instance.");
   m.def(
@@ -115,6 +120,12 @@ PYBIND11_MODULE(_regopy, m)
     "Sets the input document for the rego instance.",
     "rego"_a,
     "contents"_a);
+  m.def(
+    "regoSetInputTerm",
+    &regoSetInputTerm,
+    "Sets the input document for the rego instance.",
+    "rego"_a,
+    "contents"_a);
   m.def(
     "regoSetDebugEnabled",
     &regoSetDebugEnabled,
diff --git a/wrappers/python/src/regopy/__init__.py b/wrappers/python/src/regopy/__init__.py
index b7abab4a..08c933a6 100644
--- a/wrappers/python/src/regopy/__init__.py
+++ b/wrappers/python/src/regopy/__init__.py
@@ -15,7 +15,8 @@
     REGOCPP_BUILD_TOOLCHAIN,
     REGOCPP_PLATFORM,
     REGOCPP_VERSION,
-    regoSetLogLevel
+    regoSetLogLevel,
+    regoSetLogLevelFromString
 )
 from .interpreter import Interpreter, RegoError
 from .node import Node, NodeKind
@@ -50,6 +51,17 @@ def set_log_level(level: LogLevel):
     regoSetLogLevel(level)
 
 
+def set_log_level_from_string(level: str):
+    """Sets the log level from a string.
+
+    Args:
+        level (str): The log level. One of "None",
+                     "Error", "Output", "Warn", "Info",
+                     "Debug", "Trace".
+    """
+    regoSetLogLevelFromString(level)
+
+
 def build_info() -> str:
     """Returns the build information as a string.
 
diff --git a/wrappers/python/src/regopy/interpreter.py b/wrappers/python/src/regopy/interpreter.py
index 87c34925..41f3ab78 100644
--- a/wrappers/python/src/regopy/interpreter.py
+++ b/wrappers/python/src/regopy/interpreter.py
@@ -16,7 +16,7 @@
     regoQuery,
     regoSetDebugEnabled,
     regoSetDebugPath,
-    regoSetInputJSON,
+    regoSetInputTerm,
     regoSetStrictBuiltInErrors,
     regoSetWellFormedChecksEnabled
 )
@@ -199,6 +199,37 @@ def add_data(self, data: Any):
         """
         self.add_data_json(json.dumps(data))
 
+    def set_input_term(self, term: str):
+        """Sets the input term of the interpreter.
+
+        This can be called several times during the lifetime of the interpreter.
+
+        Args:
+            term (str): The input term.
+
+        Raises:
+            RegoError: If an error occurs in the Rego interpreter.
+
+        Example:
+            >>> from regopy import Interpreter
+            >>> input = '''
+            ...   {
+            ...     "a": 10,
+            ...     "b": "20",
+            ...     "c": 30.0,
+            ...     "d": true
+            ...   }
+            ... '''
+            >>> rego = Interpreter()
+            >>> rego.set_input_json(input)
+            >>> output = rego.query("input.a")
+            >>> print(output)
+            10
+        """
+        err = regoSetInputTerm(self._impl, term)
+        if err == REGO_ERROR:
+            raise RegoError(regoGetError(self._impl))
+
     def set_input_json(self, json: str):
         """Sets the input document of the interpreter.
 
@@ -227,7 +258,7 @@ def set_input_json(self, json: str):
             >>> print(output)
             10
         """
-        err = regoSetInputJSON(self._impl, json)
+        err = regoSetInputTerm(self._impl, json)
         if err == REGO_ERROR:
             raise RegoError(regoGetError(self._impl))
 
@@ -246,7 +277,7 @@ def set_input(self, value: Any):
         See Also:
             :func:`~regopy.Interpreter.set_input_json`
         """
-        self.set_input_json(json.dumps(value))
+        self.set_input_term(json.dumps(value))
 
     @property
     def debug_enabled(self) -> bool:
diff --git a/wrappers/rust/regorust/Cargo.toml b/wrappers/rust/regorust/Cargo.toml
index 67352ac3..b713a3f3 100644
--- a/wrappers/rust/regorust/Cargo.toml
+++ b/wrappers/rust/regorust/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "regorust"
-version = "0.3.11"
+version = "0.4.0"
 edition = "2021"
 description = "Rust bindings for the rego-cpp Rego compiler and interpreter"
 license = "MIT"
diff --git a/wrappers/rust/regorust/README.md b/wrappers/rust/regorust/README.md
index d7e442b3..f7293062 100644
--- a/wrappers/rust/regorust/README.md
+++ b/wrappers/rust/regorust/README.md
@@ -206,6 +206,8 @@ all the standard builtins. The following builtins are currently supported:
 - `arrays`
 - `bits`
 - `casts`
+- `encoding`
+- `graphs`
 - `numbers`
 - `objects`
 - `regex`
@@ -215,9 +217,6 @@ all the standard builtins. The following builtins are currently supported:
 - `types`
 - `units`
 - miscellaneous
-    * `base64_encode`
-    * `base64_decode`
-    * `json.marshal`
     * `opa.runtime`
     * `print`
     * `time.now_ns`
@@ -234,16 +233,14 @@ To build with the OPA tests available for testing, use one of the following pres
 - `release-opa`
 
 At present, we are **NOT** passing the following test suites in full:
-- `base64*`
 - `crypto*`
 - `glob*`
 - `graphql`
 - `invalidkeyerror`
-- `json*`
+- `json*`  (except `jsonbuiltins`)
 - `jwt*`
 - `net*`
 - `planner-ir`
 - `providers-aws`
-- `reachable`
-- `urlbuiltins`
+- `time`
 - `walkbuiltin`
diff --git a/wrappers/rust/regorust/build.rs b/wrappers/rust/regorust/build.rs
index 52d45a36..7420c49a 100644
--- a/wrappers/rust/regorust/build.rs
+++ b/wrappers/rust/regorust/build.rs
@@ -84,6 +84,8 @@ fn main() {
     println!("cargo:rustc-link-lib=static=re2");
     println!("cargo:rustc-link-lib=static=snmallocshim-static");
     println!("cargo:rustc-link-lib=static=rego");
+    println!("cargo:rustc-link-lib=static=json");
+    println!("cargo:rustc-link-lib=static=yaml");
     if cfg!(windows) {
         println!("cargo:rustc-link-arg=mincore.lib");
     } else if cfg!(target_os = "macos") {
diff --git a/wrappers/rust/regorust/src/lib.rs b/wrappers/rust/regorust/src/lib.rs
index 151c11a2..c342ff79 100644
--- a/wrappers/rust/regorust/src/lib.rs
+++ b/wrappers/rust/regorust/src/lib.rs
@@ -340,8 +340,8 @@ pub enum LogLevel {
 }
 
 /// Sets the level of logging produced by the library.
-pub fn set_log_level(level: LogLevel) {
-    match level {
+pub fn set_log_level(level: LogLevel) -> Result<(), &'static str> {
+    let result: regoEnum = match level {
         LogLevel::None => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_NONE) },
         LogLevel::Debug => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_DEBUG) },
         LogLevel::Info => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_INFO) },
@@ -349,6 +349,12 @@ pub fn set_log_level(level: LogLevel) {
         LogLevel::Error => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_ERROR) },
         LogLevel::Output => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_OUTPUT) },
         LogLevel::Trace => unsafe { regoSetLogLevel(REGO_LOG_LEVEL_TRACE) },
+    };
+
+    match result {
+        REGO_OK => Ok(()),
+        REGO_LOG_LEVEL_ERROR => Err("Invalid log level"),
+        _ => Err("Unknown error"),
     }
 }
 


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f7092d17..aed403fa 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,31 +1,28 @@
-set( SOURCES
-test_case.cc
-parse.cc
-rego_test.cc
-)
-
-add_executable(rego_test main.cc ${SOURCES})
+add_executable(rego_test main.cc test_case.cc)
 target_link_libraries(rego_test
   PRIVATE 
   regocpp::rego)
 
-add_executable(yaml_trieste driver.cc ${SOURCES})
-
-target_link_libraries(yaml_trieste
-  PRIVATE
+add_executable(rego_test_c_api c_api.cc)
+target_link_libraries(rego_test_c_api
+  PRIVATE 
   regocpp::rego)
 
 if(REGOCPP_BUILD_TOOLS)
-  add_test(NAME rego_trieste COMMAND rego_trieste test -f WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_trieste>)
+  add_test(NAME rego_fuzzer_reader COMMAND rego_fuzzer reader -f WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_fuzzer>)
+  add_test(NAME rego_fuzzer_unify COMMAND rego_fuzzer unify -f WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_fuzzer>)
 endif()
 
-add_test(NAME yaml_trieste COMMAND yaml_trieste test -f WORKING_DIRECTORY $<TARGET_FILE_DIR:yaml_trieste>)
-add_test(NAME rego_test COMMAND rego_test regocpp.yaml WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
+add_test(NAME rego_test_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_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}/bugs.yaml $<TARGET_FILE_DIR:rego_test>/bugs.yaml)
 add_custom_command(TARGET rego_test POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/aci $<TARGET_FILE_DIR:rego_test>/aci)
 
@@ -37,6 +34,8 @@ if(REGOCPP_OPA_TESTS)
   arithmetic
   array
   assignments
+  base64builtins
+  base64urlbuiltins
   baseandvirtualdocs
   bitsand
   bitsnegate
@@ -64,10 +63,12 @@ if(REGOCPP_OPA_TESTS)
   functionerrors
   functions
   helloworld
+  hexbuiltins
   indexing
   indirectreferences
   inputvalues
   intersection
+  jsonbuiltins
   negation
   nestedreferences
   numbersrange
@@ -86,6 +87,7 @@ if(REGOCPP_OPA_TESTS)
   partialobjectdoc
   partialsetdoc
   rand
+  reachable
   replacen
   refheads
   regexfind
@@ -116,6 +118,7 @@ if(REGOCPP_OPA_TESTS)
   undos
   union
   units
+  urlbuiltins
   varreferences
   virtualdocs
   withkeyword
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index de1140a1..00000000
--- a/tests/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-start
-- launching place for tokens etc.
-- if spaces, count them
\ No newline at end of file
diff --git a/tests/aci/aci.yaml b/tests/aci/aci.yaml
index cb518541..8c76ee59 100644
--- a/tests/aci/aci.yaml
+++ b/tests/aci/aci.yaml
@@ -62,7 +62,7 @@ cases:
   - framework.rego
   - policy.rego
   input:
-    encrypted: true,
+    encrypted: true
     target: /mnt/layer6
   data:
     metadata: {}
diff --git a/tests/bugs.yaml b/tests/bugs.yaml
new file mode 100644
index 00000000..e650608f
--- /dev/null
+++ b/tests/bugs.yaml
@@ -0,0 +1,159 @@
+# Tests for fixed bugs to prevent regression
+cases:
+- note: regocpp/bug95
+  modules:
+  - |
+    package test
+
+    x = c {
+      a = b
+      b = c
+      a = 12
+    }
+  query: data.test.x = x
+  want_result:
+    - x: 12
+- note: regocpp/bug97
+  modules:
+  - |
+    package test
+
+    x = y {
+      a = [1, 2, 3]
+      y = {z | z = a[_]}
+    }
+  query: data.test.x = x
+  want_result:
+    - x:
+      - 1
+      - 2
+      - 3
+- note: regocpp/bug101
+  modules:
+  - |
+    package test
+
+    x = y {
+      y = [z |
+        z = a[_]
+        a = [1, 2, 3]
+      ]
+    }
+  query: data.test.x = x
+  want_result:
+    - x:
+      - 1
+      - 2
+      - 3
+- note: regocpp/bug112
+  modules:
+  - |
+    package test
+    x = {
+      "a"
+      : 1
+    }
+  query: data.test.x = x
+  want_result:
+    - x:
+        a: 1
+- note: regocpp/bug114
+  modules:
+  - |
+    package test
+
+    x = {1} | {2} | {3}
+  query: data.test.x = x
+  want_result:
+    - x:
+      - 1
+      - 2
+      - 3
+- note: regocpp/bug115
+  modules:
+  - |
+    package test
+
+    x = [{1} | {2} | {3}]
+  query: data.test.x = x
+  want_result:
+    - x:
+      - - 1
+- note: regocpp/bug116
+  query: x = (12/3)%3
+  want_result:
+    - x: 1
+- note: regocpp/bug124
+  modules:
+  - |
+    package test
+
+    x := 1 == {"a": 1}.a
+  query: data.test.x = x
+  want_result:
+    - x: true
+- note: regocpp/bug126
+  modules:
+  - |
+    package test
+
+    a = {"a"} | {"b"}
+  query: data.test.a = x
+  want_result:
+    - x:
+      - a
+      - b
+- note: regocpp/bug127
+  modules:
+  - |
+    package test
+
+    a = y {
+      y = {1}
+      | {2}
+    }
+  query: data.test.a = x
+  want_result:
+    - x:
+      - 1
+      - 2
+- note: regocpp/bug129
+  modules:
+  - |
+    package example
+
+    a = 1 % 3
+    b = 2 % 3
+    c = 3 % 3
+    d = 4 % 3
+    e = 5 % 3
+    f = 6 % 3
+  query: "data.example = x"
+  want_result:
+    - x:
+        a: 1
+        b: 2
+        c: 0
+        d: 1
+        e: 2
+        f: 0
+- note: regocpp/bug130
+  modules:
+  - |
+    package example
+
+    x = 5 * y
+    y = input.a
+    z = 1
+  query: "data.example = x"
+  want_result:
+    - x:
+        z: 1
+- note: regocpp/bug134
+  query: "(1 - 7) % 3 = x"
+  want_result:
+    - x: 0
+- note: regocpp/bug135
+  query: "[1, 2][1/1] = x"
+  want_result:
+    - x: 2
diff --git a/tests/c_api.cc b/tests/c_api.cc
new file mode 100644
index 00000000..a93d5a44
--- /dev/null
+++ b/tests/c_api.cc
@@ -0,0 +1,141 @@
+#include <cstdio>
+#include <cstdlib>
+#include <rego/rego_c.h>
+
+const char* OBJECTS = R"(package objects
+
+rect := {`width`: 2, "height": 4}
+cube := {"width": 3, `height`: 4, "depth": 5}
+a := 42
+b := false
+c := null
+d := {"a": a, "x": [b, c]}
+index := 1
+shapes := [rect, cube]
+names := ["prod", `smoke1`, "dev"]
+sites := [{"name": "prod"}, {"name": names[index]}, {"name": "dev"}]
+e := {
+    a: "foo",
+    "three": c,
+    names[2]: b,
+    "four": d,
+}
+f := e["dev"]
+)";
+
+const char* DATA0 = R"({
+    "one": {
+        "bar": "Foo",
+        "baz": 5,
+        "be": true,
+        "bop": 23.4
+    },
+    "two": {
+        "bar": "Bar",
+        "baz": 12.3,
+        "be": false,
+        "bop": 42
+    }
+}
+)";
+
+const char* DATA1 = R"({
+    "three": {
+        "bar": "Baz",
+        "baz": 15,
+        "be": true,
+        "bop": 4.23
+    }
+}
+)";
+
+const char* INPUT0 = R"({
+    "a": 10,
+    "b": "20",
+    "c": 30.0,
+    "d": true
+}
+)";
+
+int main()
+{
+  regoEnum err;
+  int rc = REGO_OK;
+  regoOutput* output = NULL;
+  regoNode* node = NULL;
+  regoInterpreter* rego = regoNew();
+  regoSize size = 0;
+  char* buf = NULL;
+
+  err = regoAddModule(rego, "objects.rego", OBJECTS);
+  if (err != REGO_OK)
+  {
+    goto error;
+  }
+
+  err = regoAddDataJSON(rego, DATA0);
+  if (err != REGO_OK)
+  {
+    goto error;
+  }
+
+  err = regoAddDataJSON(rego, DATA1);
+  if (err != REGO_OK)
+  {
+    goto error;
+  }
+
+  err = regoSetInputTerm(rego, INPUT0);
+  if (err != REGO_OK)
+  {
+    goto error;
+  }
+
+  output = regoQuery(rego, "[data.one, input.b, data.objects.sites[1]] = x");
+  if (output == NULL)
+  {
+    goto error;
+  }
+
+  printf("%s\n", regoOutputString(output));
+
+  node = regoOutputBinding(output, "x");
+  if (node == NULL)
+  {
+    goto error;
+  }
+
+  size = regoNodeJSONSize(node);
+  if (size == 0)
+  {
+    goto error;
+  }
+
+  buf = (char*)malloc(size);
+  err = regoNodeJSON(node, buf, size);
+  if (err != REGO_OK)
+  {
+    goto error;
+  }
+
+  printf("x = %s\n", buf);
+  free(buf);
+  goto exit;
+
+error:
+  printf("%s\n", regoGetError(rego));
+  rc = REGO_ERROR;
+
+exit:
+  if (output != nullptr)
+  {
+    regoFreeOutput(output);
+  }
+
+  if (rego != nullptr)
+  {
+    regoFree(rego);
+  }
+
+  return rc;
+}
\ No newline at end of file
diff --git a/tests/driver.cc b/tests/driver.cc
deleted file mode 100644
index 3afc027a..00000000
--- a/tests/driver.cc
+++ /dev/null
@@ -1,10 +0,0 @@
-#include "trieste/driver.h"
-
-#include "rego_test.h"
-
-int main(int argc, char** argv)
-{
-  trieste::Driver driver(
-    "rego_test", nullptr, rego_test::parser(), rego_test::passes());
-  return driver.run(argc, argv);
-}
diff --git a/tests/main.cc b/tests/main.cc
index 88bc147b..ca77b1cc 100644
--- a/tests/main.cc
+++ b/tests/main.cc
@@ -66,8 +66,16 @@ int main(int argc, char** argv)
   app.add_option(
     "-a,--ast", debug_path, "Output the AST (debugging for test case parser)");
 
-  rego::LogLevel loglevel{rego::LogLevel::Output};
-  app.add_option("-l,--loglevel", loglevel, "Enable logging");
+  std::string log_level;
+  app
+    .add_option(
+      "-l,--log_level",
+      log_level,
+      "Set Log Level to one of "
+      "Trace, Debug, Info, "
+      "Warning, Output, Error, "
+      "None")
+    ->check(rego::set_log_level_from_string);
 
   bool wf_checks{false};
   app.add_flag("-w,--wf", wf_checks, "Enable well-formedness checks (slow)");
@@ -91,8 +99,6 @@ int main(int argc, char** argv)
     return app.exit(e);
   }
 
-  rego::set_log_level(loglevel);
-
   logging::Output() << "Loading test cases:";
   TestCases all_testcases;
   for (auto file_or_dir : case_paths)
diff --git a/tests/parse.cc b/tests/parse.cc
deleted file mode 100644
index 5e263b96..00000000
--- a/tests/parse.cc
+++ /dev/null
@@ -1,778 +0,0 @@
-#include "rego_test.h"
-
-namespace rego_test
-{
-  const std::string alpha =
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
-  const std::string numeric = "0123456789";
-  const std::string alphanumeric = alpha + numeric;
-  const std::size_t max_string_length = 10;
-
-  template <typename T>
-  std::string rand_string(T& rnd)
-  {
-    // TODO add valid non-alphanum characters
-    std::ostringstream buf;
-    std::size_t length = rnd() % max_string_length;
-    for (std::size_t i = 0; i < length; ++i)
-    {
-      buf << alphanumeric[rnd() % alphanumeric.size()];
-    }
-    return buf.str();
-  }
-
-  struct Block
-  {
-    std::size_t indent;
-  };
-
-  class Indent
-  {
-  public:
-    Indent() : m_enabled(true), m_current(0), m_string_start(0)
-    {
-      m_indents.push_back(0);
-    }
-
-    void enable()
-    {
-      m_enabled = true;
-    }
-
-    void disable()
-    {
-      m_enabled = false;
-    }
-
-    void newline()
-    {
-      m_current = 0;
-    }
-
-    bool at_string_start()
-    {
-      return m_string_start > 0 && m_string_start == m_current;
-    }
-
-    void end_string()
-    {
-      m_string_start = 0;
-    }
-
-    void inc(std::size_t n = 1)
-    {
-      m_current += n;
-    }
-
-    bool push()
-    {
-      if (!m_enabled)
-      {
-        return false;
-      }
-
-      if (m_current > m_indents.back())
-      {
-        m_indents.push_back(m_current);
-        return true;
-      }
-
-      return false;
-    }
-
-    template <typename M>
-    void pop_blocks(M& m)
-    {
-      if (!m_enabled)
-      {
-        return;
-      }
-
-      while (m_current < m_indents.back())
-      {
-        m.term({Block});
-        m_indents.pop_back();
-      }
-    }
-
-    template <typename M>
-    void cleanup(M& m)
-    {
-      while (!m_indents.empty())
-      {
-        m.term({Block});
-        m_indents.pop_back();
-      }
-    }
-
-    template <typename M>
-    bool complete(M& m, std::string mode)
-    {
-      if (!m_enabled)
-      {
-        m.mode(mode);
-        return true;
-      }
-
-      if (m.in(LiteralString) || m.in(FoldedString))
-      {
-        if (m_string_start == 0)
-        {
-          m_string_start = m_current;
-          m.mode("multiline-string");
-          m.add(String);
-          return false;
-        }
-
-        if (m_current < m_string_start)
-        {
-          m_string_start = 0;
-          if (m.in(LiteralString))
-          {
-            m.pop(LiteralString);
-          }
-          else if (m.in(FoldedString))
-          {
-            m.pop(FoldedString);
-          }
-          pop_blocks(m);
-          m.term();
-          m.mode(mode);
-          return true;
-        }
-
-        m.mode("multiline-string");
-        m.add(String);
-        return false;
-      }
-
-      if (m.in(SingleQuoteString) || m.in(DoubleQuoteString))
-      {
-        if (m_string_start == 0)
-        {
-          m_string_start = m_current;
-        }
-        m.mode("multiline-string");
-        m.add(String);
-        return false;
-      }
-
-      m.mode(mode);
-      return true;
-    }
-
-  private:
-    bool m_enabled;
-    std::size_t m_current;
-    std::size_t m_string_start;
-    std::vector<std::size_t> m_indents;
-  };
-
-  enum class Quote
-  {
-    None,
-    Single,
-    Double,
-  };
-
-  enum class Collection
-  {
-    None,
-    Braces,
-    Squares
-  };
-
-  Parse parser()
-  {
-    Parse p(depth::file, wf_parser);
-    auto indent = std::make_shared<Indent>();
-    auto quote = std::make_shared<Quote>(Quote::None);
-    auto stack = std::make_shared<std::vector<Collection>>();
-    auto in_value = std::make_shared<bool>(false);
-    stack->push_back(Collection::None);
-
-    p("start",
-      {
-        "---" >>
-          [](auto&) {
-            // TODO: add support for YAML front matter
-          },
-
-        // Line comment.
-        "#[^\n]*" >> [](auto&) {},
-
-        "\r*\n" >>
-          [indent, in_value](auto& m) {
-            *in_value = false;
-            indent->newline();
-            m.term();
-            m.mode("indent");
-          },
-
-        // Brace.
-        R"(\{)" >>
-          [stack, in_value, indent](auto& m) {
-            indent->disable();
-            *in_value = false;
-            m.term();
-            m.push(Brace);
-            stack->push_back(Collection::Braces);
-          },
-
-        R"(\})" >>
-          [stack, indent](auto& m) {
-            if (stack->back() == Collection::Braces)
-            {
-              m.term();
-              m.pop(Brace);
-              stack->pop_back();
-              if (stack->back() == Collection::None)
-              {
-                indent->enable();
-              }
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        // Square.
-        R"(\[)" >>
-          [stack, indent](auto& m) {
-            indent->disable();
-            m.term();
-            m.push(Square);
-            stack->push_back(Collection::Squares);
-          },
-
-        R"(\])" >>
-          [stack, indent](auto& m) {
-            if (stack->back() == Collection::Squares)
-            {
-              m.term();
-              m.pop(Square);
-              stack->pop_back();
-              if (stack->back() == Collection::None)
-              {
-                indent->enable();
-              }
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        // Comma.
-        ",[[:blank:]]*" >>
-          [stack, in_value](auto& m) {
-            if (stack->back() == Collection::None)
-            {
-              m.extend(String);
-            }
-            else
-            {
-              *in_value = false;
-              m.term();
-            }
-          },
-
-        // Double quote string
-        "\"" >>
-          [quote](auto& m) {
-            if (m.previous(Colon) || m.in(Block) || m.in(Square) || m.in(Brace))
-            {
-              m.push(DoubleQuoteString, 1);
-              m.mode("multiline-string");
-              *quote = Quote::Double;
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        // Single quote string
-        "'" >>
-          [quote](auto& m) {
-            if (m.previous(Colon) || m.in(Square) || m.in(Brace) || m.in(Block))
-            {
-              m.push(SingleQuoteString, 1);
-              m.mode("multiline-string");
-              *quote = Quote::Single;
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        // KeyValue.
-        ":[ ]*" >>
-          [in_value](auto& m) {
-            m.add(Colon);
-            *in_value = true;
-          },
-
-        // Literal string
-        "\\|" >> [](auto& m) { m.push(LiteralString); },
-
-        // Folded string
-        ">" >> [](auto& m) { m.push(FoldedString); },
-
-        // Entry.
-        "-[ \n][\r\n]*" >>
-          [indent](auto& m) {
-            indent->inc(2);
-            indent->push();
-            m.add(Hyphen);
-            m.push(Block);
-          },
-
-        // True.
-        "true\\b" >> [](auto& m) { m.add(True); },
-
-        // False.
-        "false\\b" >> [](auto& m) { m.add(False); },
-
-        // Null.
-        "null\\b" >> [](auto& m) { m.add(Null); },
-
-        // Not a number
-        R"((?:(?:[[:digit:]]+)\.\b){2})" >>
-          [](auto& m) {
-            m.add(String);
-            m.mode("string");
-          },
-
-        // Also not a number
-        R"(\-[[:digit:]\.]*\-)" >>
-          [](auto& m) {
-            m.add(String);
-            m.mode("string");
-          },
-
-        // String
-        R"([[:digit:]\.]*[^[:digit:]^\.,\-\[\]\{\}[:blank:]\r\n])" >>
-          [](auto& m) {
-            m.extend(String);
-            m.mode("string");
-          },
-
-        // Float.
-        R"(\-?[[:digit:]]+\.[[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
-          [](auto& m) { m.add(Float); },
-
-        // Int.
-        R"(\-?[[:digit:]]+\b)" >> [](auto& m) { m.add(Integer); },
-
-        // Space.
-        " " >> [](auto&) {},
-      });
-
-    p("indent",
-      {R"(\\ )" >> [](auto&) {},
-
-       "\r*\n" >>
-         [indent](auto& m) {
-           if (indent->at_string_start())
-           {
-             m.add(Blank);
-             m.term();
-           }
-           indent->newline();
-         },
-
-       // Space
-       " " >>
-         [indent](auto& m) {
-           if (indent->at_string_start())
-           {
-             m.mode("multiline-string");
-             m.add(String);
-           }
-           else
-           {
-             indent->inc();
-           }
-         },
-
-       // Hyphen
-       "-[ \n][\r\n]*" >>
-         [indent](auto& m) {
-           m.mode("start");
-           if (m.in(LiteralString) || m.in(FoldedString))
-           {
-             indent->end_string();
-             if (m.in(LiteralString))
-             {
-               m.pop(LiteralString);
-             }
-             else
-             {
-               m.pop(FoldedString);
-             }
-           }
-
-           indent->pop_blocks(m);
-           indent->inc(2);
-           indent->push();
-           m.add(Hyphen);
-           m.push(Block);
-         },
-
-       // Double quote string
-       "\"" >>
-         [quote, indent](auto& m) {
-           if (indent->push())
-           {
-             m.push(Block);
-           }
-           else
-           {
-             indent->pop_blocks(m);
-             m.term();
-           }
-           m.push(DoubleQuoteString, 1);
-           m.mode("multiline-string");
-           *quote = Quote::Double;
-         },
-
-       // Single quote string
-       "'" >>
-         [quote, indent](auto& m) {
-           if (indent->push())
-           {
-             m.push(Block);
-           }
-           else
-           {
-             indent->pop_blocks(m);
-             m.term();
-           }
-
-           m.push(SingleQuoteString, 1);
-           m.mode("multiline-string");
-           *quote = Quote::Single;
-         },
-
-       "#" >> [indent](auto& m) { indent->complete(m, "comment"); },
-
-       R"(\{)" >>
-         [indent, stack, in_value](auto& m) {
-           if (indent->complete(m, "start"))
-           {
-             indent->disable();
-             *in_value = false;
-             stack->push_back(Collection::Braces);
-             m.term();
-             m.push(Brace);
-           }
-         },
-
-       R"(\})" >>
-         [indent, stack](auto& m) {
-           if (stack->back() == Collection::Braces)
-           {
-             m.term();
-             m.pop(Brace);
-             stack->pop_back();
-             if (stack->back() == Collection::None)
-             {
-               indent->enable();
-             }
-             m.mode("start");
-           }
-           else if (indent->complete(m, "string"))
-           {
-             if (indent->push())
-             {
-               m.push(Block);
-             }
-             else
-             {
-               indent->pop_blocks(m);
-             }
-             m.term();
-             m.add(String);
-           }
-         },
-
-       R"(\[)" >>
-         [indent, stack](auto& m) {
-           if (indent->complete(m, "start"))
-           {
-             indent->disable();
-             stack->push_back(Collection::Squares);
-             m.term();
-             m.push(Square);
-           }
-         },
-
-       R"(\])" >>
-         [indent, stack](auto& m) {
-           if (stack->back() == Collection::Squares)
-           {
-             m.term();
-             m.pop(Square);
-             stack->pop_back();
-             if (stack->back() == Collection::None)
-             {
-               indent->enable();
-             }
-             m.mode("start");
-           }
-           else if (indent->complete(m, "string"))
-           {
-             if (indent->push())
-             {
-               m.push(Block);
-             }
-             else
-             {
-               indent->pop_blocks(m);
-             }
-             m.term();
-             m.add(String);
-           }
-         },
-
-       // Character
-       "." >>
-         [indent](auto& m) {
-           if (indent->complete(m, "string"))
-           {
-             if (indent->push())
-             {
-               m.push(Block);
-             }
-             else
-             {
-               indent->pop_blocks(m);
-             }
-             m.term();
-             m.add(String);
-           }
-         }});
-
-    p("string",
-      {
-        "\r*\n" >>
-          [indent, in_value](auto& m) {
-            indent->newline();
-            *in_value = false;
-            m.term();
-            m.mode("indent");
-          },
-
-        R"(\})" >>
-          [stack, quote, indent](auto& m) {
-            if (*quote == Quote::None && stack->back() == Collection::Braces)
-            {
-              m.term();
-              m.pop(Brace);
-              stack->pop_back();
-              if (stack->back() == Collection::None)
-              {
-                indent->enable();
-              }
-              m.mode("start");
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        R"(\])" >>
-          [stack, quote, indent](auto& m) {
-            if (*quote == Quote::None && stack->back() == Collection::Squares)
-            {
-              m.term();
-              m.pop(Square);
-              stack->pop_back();
-              if (stack->back() == Collection::None)
-              {
-                indent->enable();
-              }
-              m.mode("start");
-            }
-            else
-            {
-              m.extend(String);
-            }
-          },
-
-        // Comma.
-        ",[[:blank:]]*" >>
-          [stack, in_value](auto& m) {
-            if (stack->back() == Collection::None)
-            {
-              m.extend(String);
-            }
-            else
-            {
-              *in_value = false;
-              m.term();
-              m.mode("start");
-            }
-          },
-
-        ": " >>
-          [in_value](auto& m) {
-            if (*in_value)
-            {
-              m.extend(String);
-            }
-            else
-            {
-              *in_value = true;
-              m.add(Colon);
-              m.mode("start");
-            }
-          },
-
-        ":[ ]*\r*\n" >>
-          [in_value, indent](auto& m) {
-            if (*in_value)
-            {
-              m.extend(String);
-            }
-            else
-            {
-              m.add(Colon);
-              *in_value = false;
-              indent->newline();
-              m.term();
-              m.mode("indent");
-            }
-          },
-
-        // Character
-        "." >> [](auto& m) { m.extend(String); },
-      });
-
-    p("multiline-string",
-      {R"(\\?\r*\n)" >>
-         [indent](auto& m) {
-           indent->newline();
-           m.term();
-           m.mode("indent");
-         },
-
-       R"(\\("))" >>
-         [quote](auto& m) {
-           if (*quote == Quote::Double)
-           {
-             m.add(String, 1);
-           }
-           else if (*quote == Quote::None)
-           {
-             m.extend(String);
-           }
-           else
-           {
-             m.invalid();
-           }
-         },
-
-       R"(\\ )" >>
-         [quote](auto& m) {
-           if (*quote == Quote::Double)
-           {
-             m.add(String, 1);
-           }
-           else if (*quote == Quote::None)
-           {
-             m.extend(String);
-           }
-           else
-           {
-             m.invalid();
-           }
-         },
-
-       R"(\\n)" >>
-         [quote](auto& m) {
-           if (*quote == Quote::Double)
-           {
-             m.term();
-             m.add(NewLine);
-           }
-           else if (*quote == Quote::None)
-           {
-             m.extend(String);
-           }
-         },
-
-       "\"" >>
-         [indent, quote](auto& m) {
-           if (*quote == Quote::Double)
-           {
-             m.term();
-             *quote = Quote::None;
-             indent->end_string();
-             m.pop(DoubleQuoteString);
-             m.mode("start");
-           }
-           else
-           {
-             m.extend(String);
-           }
-         },
-
-       "'" >>
-         [indent, quote](auto& m) {
-           if (*quote == Quote::Single)
-           {
-             m.term();
-             *quote = Quote::None;
-             indent->end_string();
-             m.pop(SingleQuoteString);
-             m.mode("start");
-           }
-           else
-           {
-             m.extend(String);
-           }
-         },
-
-       // Character
-       "." >> [](auto& m) { m.extend(String); }});
-
-    p("comment",
-      {
-        R"(\\?\r*\n)" >>
-          [indent](auto& m) {
-            indent->newline();
-            m.term();
-            m.mode("indent");
-          },
-        "." >> [](auto&) {},
-      });
-
-    p.done([indent, stack](auto& m) {
-      if (stack->size() > 1)
-      {
-        m.error("Unclosed braces");
-      }
-      indent->cleanup(m);
-    });
-
-    p.gen({
-      Integer >> [](auto& rnd) { return std::to_string(rnd() % 100); },
-      Float >>
-        [](auto& rnd) {
-          std::uniform_real_distribution<> dist(-10.0, 10.0);
-          return std::to_string(dist(rnd));
-        },
-      True >> [](auto&) { return "true"; },
-      False >> [](auto&) { return "false"; },
-      Null >> [](auto&) { return "null"; },
-      String >> [](auto& rnd) { return rand_string(rnd); },
-    });
-
-    return p;
-  }
-}
\ No newline at end of file
diff --git a/tests/rego_test.cc b/tests/rego_test.cc
deleted file mode 100644
index 0affdbe7..00000000
--- a/tests/rego_test.cc
+++ /dev/null
@@ -1,486 +0,0 @@
-#include "rego_test.h"
-
-namespace rego_test
-{
-  const auto inline SequenceToken = T(Sequence) / T(Mapping) / T(Scalar);
-  const auto inline ScalarToken =
-    T(String) / T(Integer) / T(Float) / T(True) / T(False) / T(Null);
-  const auto inline ValToken = T(Mapping) / T(Sequence) / T(Scalar);
-
-  PassDef entry()
-  {
-    return {
-      "entry",
-      wf_pass_entry,
-      dir::topdown,
-      {
-        In(Top) * T(File)[File] >> [](Match& _) { return Block << *_[File]; },
-
-        In(Group) * (T(Brace)[Brace] << End) >>
-          [](Match& _) { return EmptyMapping ^ _(Brace); },
-
-        In(Group) * (T(Colon) * T(Integer)[Integer] * T(String)[String]) >>
-          [](Match& _) {
-            Location int_loc = _(Integer)->location();
-            Location str_loc = _(String)->location();
-            std::size_t end = str_loc.pos + str_loc.len;
-            Location loc = int_loc;
-            loc.len = end - loc.pos;
-            return Seq << Colon << (String ^ loc);
-          },
-
-        In(Block) *
-            ((T(Group) << (T(String) * T(Colon) * End)) *
-             (T(Group)[Group] << (T(String) * T(Colon)))) >>
-          [](Match& _) { return _(Group); },
-
-        In(Group) * T(Brace)[Brace] >>
-          [](Match& _) { return Block << *_[Brace]; },
-
-        In(Group) * T(Square)[Square] >>
-          [](Match& _) {
-            if (_(Square)->size() == 0)
-            {
-              return EmptySequence ^ _(Square);
-            }
-
-            Node seq = NodeDef::create(Seq);
-            for (auto group : *_(Square))
-            {
-              seq << (Entry << group);
-            }
-            return seq;
-          },
-
-        In(Group) * (T(Hyphen) * (T(Block) / T(Group))[Entry]) >>
-          [](Match& _) { return Entry << _(Entry); },
-
-        In(DoubleQuoteString) *
-            (T(Group) << (T(NewLine) * ~T(String)++[String])) >>
-          [](Match& _) {
-            std::ostringstream buf;
-            buf << "\n";
-            for (auto it = _[String].first; it != _[String].second; ++it)
-            {
-              Node str = *it;
-              buf << str->location().view();
-            }
-            return String ^ buf.str();
-          },
-
-        (In(LiteralString, FoldedString, SingleQuoteString) /
-         In(DoubleQuoteString)) *
-            (T(Group) << T(String)[String]) >>
-          [](Match& _) { return _(String); },
-
-        (In(LiteralString, FoldedString, SingleQuoteString) /
-         In(DoubleQuoteString)) *
-            (T(Group) << T(Blank)) >>
-          [](Match&) { return String ^ ""; },
-
-        T(Group) << (T(Blank) * End) >> [](Match&) { return Node{}; },
-
-        // errors
-        In(Group) * T(Blank)[Blank] >>
-          [](Match& _) { return err(_(Blank), "Invalid blank line."); },
-
-        In(Entry, Block) * (T(Group)[Group] << End) >>
-          [](Match& _) { return err(_(Group), "Syntax error: empty group"); },
-
-        In(Group) * T(Hyphen)[Hyphen] >>
-          [](Match& _) {
-            return err(_(Hyphen), "Invalid sequence entry declaration");
-          },
-
-        In(Group) * (T(Entry)[Entry] << End) >>
-          [](Match& _) { return err(_(Entry), "Invalid entry declaration"); },
-
-        In(LiteralString) * T(Group)[Group] >>
-          [](Match& _) {
-            return err(_(Group), "Invalid literal string element");
-          },
-
-        In(FoldedString) * T(Group)[Group] >>
-          [](Match& _) {
-            return err(_(Group), "Invalid folded string element");
-          },
-
-        In(SingleQuoteString) * T(Group)[Group] >>
-          [](Match& _) {
-            return err(_(Group), "Invalid single-quoted string element");
-          },
-
-        In(DoubleQuoteString) * T(Group)[Group] >>
-          [](Match& _) {
-            return err(_(Group), "Invalid double-quoted string element");
-          },
-
-        In(Group) * T(NewLine)[NewLine] >>
-          [](Match& _) { return err(_(NewLine), "Invalid newline"); },
-      }};
-  }
-
-  PassDef sequence()
-  {
-    return {
-      "sequence",
-      wf_pass_sequence,
-      dir::topdown,
-      {In(Group) * T(EmptySequence)[EmptySequence] >>
-         [](Match& _) { return Sequence ^ _(EmptySequence); },
-
-       In(Group) * (T(Entry)[Head] * T(Entry)++[Tail] * End) >>
-         [](Match& _) { return Sequence << _(Head) << _[Tail]; },
-
-       In(Group) * (T(LiteralString) << (T(String)++[String])) >>
-         [](Match& _) {
-           std::ostringstream buf;
-           for (auto it = _[String].first; it != _[String].second; ++it)
-           {
-             if (it != _[String].first)
-             {
-               buf << "\n";
-             }
-             buf << (*it)->location().view();
-           }
-           return String ^ buf.str();
-         },
-
-       In(Group) *
-           ((T(FoldedString) / T(SingleQuoteString) / T(DoubleQuoteString))
-            << (T(String)++[String])) >>
-         [](Match& _) {
-           std::ostringstream buf;
-           for (auto it = _[String].first; it != _[String].second; ++it)
-           {
-             if (it != _[String].first)
-             {
-               buf << " ";
-             }
-             buf << (*it)->location().view();
-           }
-           return String ^ buf.str();
-         },
-
-       // errors
-
-       In(LiteralString) * T(Group)[Group] >>
-         [](Match& _) {
-           return err(_(Group), "Invalid literal string element");
-         },
-
-       In(FoldedString) * T(Group)[Group] >>
-         [](Match& _) {
-           return err(_(Group), "Invalid folded string element");
-         },
-
-       In(Group) * T(Entry)[Entry] >>
-         [](Match& _) { return err(_(Entry), "Invalid entry location"); }}};
-  }
-
-  PassDef scalar()
-  {
-    return {
-      "scalar",
-      wf_pass_scalar,
-      dir::topdown,
-      {
-        In(Group) * ScalarToken[Scalar] >>
-          [](Match& _) { return Scalar << _(Scalar); },
-      }};
-  }
-
-  PassDef keyvalue()
-  {
-    return {
-      "keyvalue",
-      wf_pass_keyvalue,
-      {
-        In(Entry, Block) *
-            (T(Group)
-             << ((T(Scalar) << T(String))[Key] * T(Colon) * Any[Val])) >>
-          [](Match& _) {
-            Location key_loc = _(Key)->location();
-            std::string key = std::string(key_loc.view());
-            std::size_t length = key.find(' ');
-            if (length != std::string::npos)
-            {
-              key_loc.len = length;
-            }
-
-            return KeyValue << (Key ^ key_loc) << (Group << _(Val));
-          },
-
-        In(Entry, Block) *
-            ((T(Group) << ((T(Scalar) << T(String))[Key] * T(Colon) * End)) *
-             T(Group)[Val]) >>
-          [](Match& _) {
-            Location key_loc = _(Key)->location();
-            std::string key = std::string(key_loc.view());
-            std::size_t length = key.find(' ');
-            if (length != std::string::npos)
-            {
-              key_loc.len = length;
-            }
-
-            return KeyValue << (Key ^ key_loc) << _(Val);
-          },
-
-        In(Entry, Block) *
-            ((T(Group) << ((T(Scalar) << T(String))[Key] * End)) *
-             (T(Group) << (T(Colon) * Any[Val]))) >>
-          [](Match& _) {
-            Location key_loc = _(Key)->location();
-            std::string key = std::string(key_loc.view());
-            std::size_t length = key.find(' ');
-            if (length != std::string::npos)
-            {
-              key_loc.len = length;
-            }
-
-            return KeyValue << (Key ^ key_loc) << (Group << _[Val]);
-          },
-
-        In(Entry, Block) *
-            ((T(Group) << ((T(Scalar) << T(String))[Key] * T(Colon))) * End) >>
-          [](Match& _) {
-            Location key_loc = _(Key)->location();
-            std::string key = std::string(key_loc.view());
-            std::size_t length = key.find(' ');
-            if (length != std::string::npos)
-            {
-              key_loc.len = length;
-            }
-
-            return KeyValue << (Key ^ key_loc) << (Group << Sequence);
-          },
-
-        In(Entry) * T(KeyValue)[KeyValue] >>
-          [](Match& _) { return Group << (Block << _(KeyValue)); },
-
-        In(Entry) * (T(Block) << (T(Group)[Group] * End)) >>
-          [](Match& _) { return _(Group); },
-
-        In(Entry) * T(Block)[Block] >>
-          [](Match& _) { return Group << _(Block); },
-
-        // errors
-        In(Group) * T(Colon)[Colon] >>
-          [](Match& _) {
-            return err(_(Colon), "Invalid key/value declaration");
-          },
-
-        In(Group) * (Any * Any[Val]) >>
-          [](Match& _) { return err(_(Val), "Syntax error"); },
-      }};
-  }
-
-  PassDef mapping()
-  {
-    return {
-      "mapping",
-      wf_pass_mapping,
-      dir::topdown,
-      {
-        In(Group) * (T(Block) << (T(KeyValue)++[Mapping] * End)) >>
-          [](Match& _) { return Mapping << _[Mapping]; },
-
-        In(Group) * T(EmptyMapping)[EmptyMapping] >>
-          [](Match& _) { return Mapping ^ _(EmptyMapping); },
-
-        // errors
-        In(Entry, Group) * T(Block)[Block] >>
-          [](Match& _) { return err(_(Block), "Invalid indented block"); },
-
-        In(Group) * T(KeyValue)[KeyValue] >>
-          [](Match& _) {
-            return err(_(KeyValue), "Invalid key/value declaration");
-          },
-      }};
-  }
-
-  PassDef structure()
-  {
-    return {
-      "structure",
-      wf_pass_structure,
-      dir::topdown,
-      {
-        In(KeyValue) * (T(Group) << ValToken[Val]) >>
-          [](Match& _) { return _(Val); },
-
-        In(Entry) * (T(Group) << ValToken[Val]) >>
-          [](Match& _) { return _(Val); },
-
-        In(Top) * (T(Block) << (T(KeyValue)[KeyValue] * End)) >>
-          [](Match& _) { return Document << _(KeyValue); },
-
-        // errors
-        In(Top) * T(Block)[Block] >>
-          [](Match& _) { return err(_(Block), "Invalid rego test case"); },
-      }};
-  }
-
-  bool name_equals(const Node& node, const std::set<std::string>& names)
-  {
-    std::string name = std::string(node->location().view());
-    return names.find(name) != names.end();
-  }
-
-  PassDef to_rego()
-  {
-    return {
-      "to_rego",
-      wf_pass_to_rego,
-      dir::topdown,
-      {In(Mapping) *
-           (T(KeyValue)
-            << ((T(Key)[Key]([](auto& n) {
-                  return name_equals(*n.first, {"data", "input"});
-                })) *
-                T(Mapping)[Mapping])) >>
-         [](Match& _) {
-           return KeyValue
-             << _(Key)
-             << (rego::File
-                 << (Group << (rego::Brace << (rego::List << *_[Mapping]))));
-         },
-
-       In(Mapping) *
-           (T(KeyValue)
-            << ((T(Key)[Key]([](auto& n) {
-                  return name_equals(*n.first, {"data", "input"});
-                })) *
-                (T(Scalar) << T(Null)))) >>
-         [](Match&) { return Node{}; },
-
-       In(Mapping) *
-           (T(KeyValue)
-            << ((T(Key)[Key]([](auto& n) {
-                  return name_equals(*n.first, {"want_result"});
-                })) *
-                T(Sequence)[Sequence])) >>
-         [](Match& _) {
-           return KeyValue << _(Key) << (WantResult << *_[Sequence]);
-         },
-
-       In(rego::List) *
-           (T(KeyValue)
-            << (T(Key)[Key] * (T(Mapping) / T(Sequence) / T(Scalar))[Val])) >>
-         [](Match& _) { return Group << _(Key) << rego::Colon << _(Val); },
-
-       In(rego::List) * (T(Scalar) / T(Mapping) / T(Sequence))[Val] >>
-         [](Match& _) { return Group << _(Val); },
-
-       In(rego::List) * T(Entry)[Entry] >>
-         [](Match& _) { return _(Entry)->front(); },
-
-       In(Group) * T(Key)[Key] >>
-         [](Match& _) {
-           std::string key_str =
-             "\"" + std::string(_(Key)->location().view()) + "\"";
-           return rego::JSONString ^ key_str;
-         },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(String)[String]) >>
-         [](Match& _) {
-           std::string str =
-             "\"" + std::string(_(String)->location().view()) + "\"";
-           return rego::JSONString ^ str;
-         },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(Integer)[Integer]) >>
-         [](Match& _) { return rego::Int ^ _(Integer); },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(Float)[Float]) >>
-         [](Match& _) { return rego::Float ^ _(Float); },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(True)[True]) >>
-         [](Match& _) { return rego::True ^ _(True); },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(False)[False]) >>
-         [](Match& _) { return rego::False ^ _(False); },
-
-       In(rego::Scalar, Group) * (T(Scalar) << T(Null)[Null]) >>
-         [](Match& _) { return rego::Null ^ _(Null); },
-
-       In(Group) * T(Mapping)[Mapping] >>
-         [](Match& _) {
-           // test for set
-           return rego::Brace << (rego::List << *_[Mapping]);
-         },
-
-       In(Group) * T(Sequence)[Sequence] >>
-         [](Match& _) { return rego::Square << (rego::List << *_[Sequence]); },
-
-       In(WantResult) * (T(KeyValue) << (T(Key)[Key] * Any[Val])) >>
-         [](Match& _) {
-           return rego::Binding << (rego::Var ^ _(Key))
-                                << (rego::Term << _(Val));
-         },
-
-       In(WantResult) * (T(Entry) << T(Mapping)[Mapping]) >>
-         [](Match& _) { return Seq << *_[Mapping]; },
-
-       In(rego::Term) * T(Mapping)[Mapping] >>
-         [](Match& _) {
-           bool is_set = _(Mapping)->size() > 0;
-           for (auto& keyvalue : *_(Mapping))
-           {
-             Node val = keyvalue / Val;
-             if (val->type() != Scalar)
-             {
-               is_set = false;
-               break;
-             }
-             val = val->front();
-             if (val->type() != Null)
-             {
-               is_set = false;
-               break;
-             }
-           }
-
-           if (is_set)
-           {
-             Node set = NodeDef::create(rego::Set);
-             for (auto& keyvalue : *_(Mapping))
-             {
-               set
-                 << (rego::Term
-                     << (rego::Scalar << (rego::JSONString ^ keyvalue / Key)));
-             }
-             return set;
-           }
-
-           return rego::Object << *_[Mapping];
-         },
-
-       In(rego::Object) * (T(KeyValue) << (T(Key)[Key] * Any[Val])) >>
-         [](Match& _) {
-           return rego::ObjectItem << (rego::Key ^ _(Key))
-                                   << (rego::Term << _(Val));
-         },
-
-       In(rego::Term) * T(Scalar)[Scalar] >>
-         [](Match& _) { return rego::Scalar << _(Scalar); },
-
-       In(rego::Term) * T(Sequence)[Sequence] >>
-         [](Match& _) { return rego::Array << *_[Sequence]; },
-
-       In(rego::Array) * T(Entry)[Entry] >>
-         [](Match& _) { return rego::Term << *_[Entry]; }}};
-  }
-
-  std::vector<Pass> passes()
-  {
-    return {
-      entry(),
-      sequence(),
-      scalar(),
-      keyvalue(),
-      mapping(),
-      structure(),
-      to_rego()};
-  }
-}
\ No newline at end of file
diff --git a/tests/rego_test.h b/tests/rego_test.h
deleted file mode 100644
index 431c3f30..00000000
--- a/tests/rego_test.h
+++ /dev/null
@@ -1,166 +0,0 @@
-#pragma once
-
-// Trieste-based rewriter for Rego YAML test cases
-
-#include <rego/rego.hh>
-
-namespace rego_test
-{
-  using namespace trieste;
-  using namespace wf::ops;
-
-  inline const auto Document = TokenDef("yaml-document", flag::symtab);
-  inline const auto Block = TokenDef("yaml-block");
-  inline const auto Sequence = TokenDef("yaml-sequence");
-  inline const auto Entry = TokenDef("yaml-entry");
-  inline const auto KeyValue = TokenDef("yaml-key-value", flag::lookdown);
-  inline const auto Mapping = TokenDef("yaml-mapping", flag::symtab);
-  inline const auto Key = TokenDef("yaml-key", flag::print);
-  inline const auto Val = TokenDef("yaml-val");
-  inline const auto String = TokenDef("yaml-string", flag::print);
-  inline const auto Integer = TokenDef("yaml-integer", flag::print);
-  inline const auto Float = TokenDef("yaml-float", flag::print);
-  inline const auto True = TokenDef("yaml-true", flag::print);
-  inline const auto False = TokenDef("yaml-false", flag::print);
-  inline const auto Null = TokenDef("yaml-null", flag::print);
-  inline const auto LiteralString = TokenDef("yaml-literal-string");
-  inline const auto FoldedString = TokenDef("yaml-folded-string");
-  inline const auto SingleQuoteString = TokenDef("yaml-single-quote-string");
-  inline const auto DoubleQuoteString = TokenDef("yaml-double-quote-string");
-  inline const auto Scalar = TokenDef("yaml-scalar");
-  inline const auto Data = TokenDef("yaml-data");
-  inline const auto Input = TokenDef("yaml-input");
-  inline const auto Colon = TokenDef("yaml-:");
-  inline const auto Hyphen = TokenDef("yaml--");
-  inline const auto WantResult = TokenDef("yaml-want-result");
-  inline const auto Head = TokenDef("yaml-head");
-  inline const auto Tail = TokenDef("yaml-tail");
-  inline const auto Blank = TokenDef("yaml-blank");
-  inline const auto Brace = TokenDef("yaml-brace");
-  inline const auto Square = TokenDef("yaml-square");
-  inline const auto NewLine = TokenDef("yaml-newline");
-  inline const auto EmptySequence = TokenDef("yaml-empty-sequence");
-  inline const auto EmptyMapping = TokenDef("yaml-empty-mapping");
-
-  inline const auto wf_parse_tokens = Block | String | Integer | Float | True |
-    False | Null | Colon | LiteralString | FoldedString | SingleQuoteString |
-    DoubleQuoteString | Hyphen | Blank | Brace | Square | NewLine;
-
-  // clang-format off
-  inline const auto wf_parser =
-    (Top <<= File)
-    | (File <<= Group++[1])
-    | (Block <<= Group++)
-    | (LiteralString <<= Group++[1])
-    | (FoldedString <<= Group++[1])
-    | (SingleQuoteString <<= Group++)
-    | (DoubleQuoteString <<= Group++)
-    | (Brace <<= Group++)
-    | (Square <<= Group++)
-    | (Group <<= wf_parse_tokens++[1])
-    ;
-  // clang-format on
-
-  inline const auto wf_entry_tokens = Block | String | Integer | Float | True |
-    False | Null | Colon | Entry | LiteralString | FoldedString |
-    SingleQuoteString | DoubleQuoteString | EmptySequence | EmptyMapping;
-
-  // clang-format off
-  inline const auto wf_pass_entry =
-    wf_parser
-    | (Top <<= Block)
-    | (LiteralString <<= String++[1])
-    | (FoldedString <<= String++[1])
-    | (SingleQuoteString <<= String++)
-    | (DoubleQuoteString <<= String++)
-    | (Entry <<= Block | Group)
-    | (Group <<= wf_entry_tokens++[1])
-    ;
-  // clang-format on    
-
-  inline const auto wf_sequence_tokens =
-    Block | String | Integer | Float | True | False | Null | Colon | Sequence | EmptyMapping;
-
-  // clang-format off
-  inline const auto wf_pass_sequence =
-    wf_pass_entry
-    | (Sequence <<= Entry++)
-    | (Group <<= wf_sequence_tokens++[1])
-    ;
-  // clang-format on   
-
-  // clang-format off
-  inline const auto wf_pass_scalar =
-    wf_pass_sequence
-    | (Scalar <<= String | Integer | Float | True | False | Null)
-    | (Group <<= (Block | Scalar | Sequence | Colon | EmptyMapping)++[1])
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_keyvalue =
-    wf_pass_scalar
-    | (KeyValue <<= Key * Group)
-    | (Block <<= (Group | KeyValue)++)
-    | (Group <<= Block | Scalar | Sequence | KeyValue | EmptyMapping)
-    ;
-  // clang-format on
-
-  // clang-format off
-  inline const auto wf_pass_mapping =
-    wf_pass_keyvalue
-    | (Mapping <<= KeyValue++)
-    | (Entry <<= Group)
-    | (Group <<= Mapping | Scalar | Sequence)
-    ;
-
-  // clang-format off
-  inline const auto wf_pass_structure =
-    (Top <<= Document)
-    | (Document <<= KeyValue)
-    | (KeyValue <<= Key * (Val >>= Scalar | Sequence | Mapping))[Key]
-    | (Sequence <<= Entry++)
-    | (Mapping <<= KeyValue++)
-    | (Entry <<= Scalar | Sequence | Mapping)
-    | (Scalar <<= String | Integer | Float | True | False | Null)
-    ;
-  // clang-format on
-
-  const auto inline wf_rego_term_tokens =
-    rego::Scalar | rego::Object | rego::Array | rego::Set;
-  const auto inline wf_rego_scalar_tokens = rego::Int | rego::Float |
-    rego::JSONString | rego::True | rego::False | rego::Null;
-
-  // clang-format off
-  inline const auto wf_pass_to_rego =
-    wf_pass_structure
-    | (KeyValue <<= Key * (Val >>= Mapping | Sequence | Scalar | rego::File | WantResult))[Key]
-    | (rego::File <<= Group++)
-    | (rego::Brace <<= rego::List)
-    | (rego::Square <<= rego::List)
-    | (rego::List <<= Group++)
-    | (Group <<= rego::wf_parse_tokens++[1])
-    | (WantResult <<= rego::Binding++)
-    | (rego::Binding <<= rego::Var * rego::Term)
-    | (rego::Term <<= wf_rego_term_tokens)
-    | (rego::Object <<= rego::ObjectItem++)
-    | (rego::ObjectItem <<= rego::Key * rego::Term)
-    | (rego::Array <<= rego::Term++)
-    | (rego::Set <<= rego::Term++)
-    | (rego::Scalar <<= wf_rego_scalar_tokens)
-    ;
-  // clang-format on
-
-  inline auto err(Node node, const std::string& msg)
-  {
-    return Error << (ErrorMsg ^ msg) << (ErrorAst << node->clone());
-  }
-
-  inline auto err(const std::string& msg)
-  {
-    return Error << (ErrorMsg ^ msg);
-  }
-
-  Parse parser();
-  std::vector<Pass> passes();
-}
\ No newline at end of file
diff --git a/tests/regocpp.yaml b/tests/regocpp.yaml
index 1e1f82b1..313ca865 100644
--- a/tests/regocpp.yaml
+++ b/tests/regocpp.yaml
@@ -1,13 +1,15 @@
 cases:
 - modules:
-  - |
-    package scalars
-    
-    greeting := "Hello"
-    max_height := 42
-    pi := 3.14159
-    allowed := true
-    location := null
+  - |1
+       
+      
+     package scalars
+     
+     greeting := "Hello"
+     max_height := 42
+     pi := 3.14159
+     allowed := true
+     location := null
   note       : regocpp/scalars
   query      : data.scalars.greeting = x
   want_result:
@@ -251,9 +253,9 @@ cases:
   query: data.sets.string_set = x
   want_result:
     - x:
-        four: null
-        one: null
-        two: null
+        - four
+        - one
+        - two
 - note: regocpp/sets_membership
   modules:
   - |
@@ -1094,51 +1096,6 @@ cases:
   query: data.every_some.output = x
   want_result:
     - x: true
-- note: regocpp/bug95
-  modules:
-  - |
-    package test
-
-    x = c {
-      a = b
-      b = c
-      a = 12
-    }
-  query: data.test.x = x
-  want_result:
-    - x: 12
-- note: regocpp/bug97
-  modules:
-  - |
-    package test
-
-    x = y {
-      a = [1, 2, 3]
-      y = {z | z = a[_]}
-    }
-  query: data.test.x = x
-  want_result:
-    - x:
-      - 1
-      - 2
-      - 3
-- note: regocpp/bug101
-  modules:
-  - |
-    package test
-
-    x = y {
-      y = [z |
-        z = a[_]
-        a = [1, 2, 3]
-      ]
-    }
-  query: data.test.x = x
-  want_result:
-    - x:
-      - 1
-      - 2
-      - 3
 - note: regocpp/non-monotone
   modules:
   - |
@@ -1167,7 +1124,7 @@ cases:
         a = b
         }
 
-  query: [data.test.foo1, data.test.foo2] = x
+  query: "[data.test.foo1, data.test.foo2] = x"
   want_result:
     - x:
       - true
diff --git a/tests/test_case.cc b/tests/test_case.cc
index ecd55ccc..660d34fe 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -1,9 +1,100 @@
 #include "test_case.h"
 
-#include "rego_test.h"
+#include "trieste/json.h"
+#include "trieste/wf.h"
+#include "trieste/yaml.h"
+
+#include <stdexcept>
 
 namespace rego_test
 {
+  Node json_value_to_rego(Node node);
+
+  Node json_object_to_rego(Node node)
+  {
+    assert(node == json::Object);
+
+    Node result = NodeDef::create(rego::Object);
+    for (Node member : *node)
+    {
+      assert(member == json::Member);
+      Node key = member / json::Key;
+      Node value = member / json::Value;
+      result
+        << (ObjectItem << json_value_to_rego(key) << json_value_to_rego(value));
+    }
+
+    return result;
+  }
+
+  Node json_array_to_rego(Node node)
+  {
+    assert(node == json::Array);
+
+    Node result = NodeDef::create(rego::Array);
+    for (Node value : *node)
+    {
+      result << json_value_to_rego(value);
+    }
+
+    return result;
+  }
+
+  Node json_value_to_rego(Node node)
+  {
+    if (node == json::Object)
+    {
+      return json_object_to_rego(node);
+    }
+
+    if (node == json::Array)
+    {
+      return json_array_to_rego(node);
+    }
+
+    if (node == json::Number)
+    {
+      auto loc = node->location();
+      if (loc.view().find('.') != std::string::npos)
+      {
+        return Term << (Scalar << (Float ^ loc));
+      }
+      else
+      {
+        return Term << (Scalar << (Int ^ loc));
+      }
+    }
+
+    if (node == json::Key)
+    {
+      std::ostringstream key;
+      key << '"' << node->location().view() << '"';
+      return Term << (Scalar << (JSONString ^ key.str()));
+    }
+
+    if (node == json::String)
+    {
+      return Term << (Scalar << (JSONString ^ node->location()));
+    }
+
+    if (node == json::True)
+    {
+      return Term << (Scalar << (True ^ node->location()));
+    }
+
+    if (node == json::False)
+    {
+      return Term << (Scalar << (False ^ node->location()));
+    }
+
+    if (node == json::Null)
+    {
+      return Term << (Scalar << (Null ^ node->location()));
+    }
+
+    throw std::runtime_error("Invalid JSON node type");
+  }
+
   bool ends_with(const std::string_view& str, const std::string_view& suffix)
   {
     if (suffix.size() > str.size())
@@ -47,7 +138,14 @@ namespace rego_test
     return false;
   }
 
-  std::optional<Node> TestCase::maybe_get_file(
+  TestCase::TestCase() :
+    m_want_defined(false),
+    m_sort_bindings(false),
+    m_strict_error(false),
+    m_broken(false)
+  {}
+
+  std::optional<Node> TestCase::maybe_get_object(
     const Node& mapping, const std::string& name)
   {
     Location loc(name);
@@ -59,12 +157,9 @@ namespace rego_test
 
     if (defs.size() == 1)
     {
-      Node val = defs[0]->back();
-      if (val->type() != File)
-      {
-        return err(val, "Expected a File node");
-      }
-      return val;
+      assert(defs[0] == json::Member);
+      Node val = defs[0] / json::Value;
+      return Top << val;
     }
 
     return std::nullopt;
@@ -82,11 +177,11 @@ namespace rego_test
 
     if (defs.size() == 1)
     {
-      Node val = defs[0]->back();
-      assert(val->type() == rego_test::Scalar);
-      val = val->front();
-      assert(val->type() == rego_test::String);
-      return std::string(val->location().view());
+      assert(defs[0] == json::Member);
+      Node val = defs[0] / json::Value;
+      assert(val == json::String);
+      std::string val_string(val->location().view());
+      return json::unescape(val_string.substr(1, val_string.size() - 2));
     }
 
     return std::nullopt;
@@ -114,7 +209,7 @@ namespace rego_test
 
     if (defs.size() == 1)
     {
-      return defs[0]->back();
+      return json_value_to_rego(defs[0]->back());
     }
 
     return {};
@@ -131,11 +226,10 @@ namespace rego_test
 
     if (defs.size() == 1)
     {
-      Node val = defs[0]->back();
-      assert(val->type() == rego_test::Scalar);
-      val = val->front();
-      assert(val->type() == rego_test::True || val->type() == rego_test::False);
-      return val->type() == rego_test::True;
+      assert(defs[0] == json::Member);
+      Node val = defs[0] / json::Value;
+      assert(val->type() == json::True || val->type() == json::False);
+      return val->type() == json::True;
     }
 
     return false;
@@ -154,15 +248,14 @@ namespace rego_test
     std::vector<std::string> modules;
     if (defs.size() == 1)
     {
-      Node module_nodes = defs[0]->back();
-      assert(module_nodes->type() == rego_test::Sequence);
+      assert(defs[0] == json::Member);
+      Node module_nodes = defs[0] / json::Value;
+      assert(module_nodes == json::Array);
       for (auto& entry : *module_nodes)
       {
-        Node scalar = entry->front();
-        assert(scalar->type() == rego_test::Scalar);
-        Node module = scalar->front();
-        assert(module->type() == rego_test::String);
-        std::string code = std::string(module->location().view());
+        assert(entry == json::String);
+        std::string code(entry->location().view());
+        code = json::unescape(code.substr(1, code.size() - 2));
         if (ends_with(code, ".rego"))
         {
           if (std::filesystem::exists(dir / code))
@@ -180,7 +273,7 @@ namespace rego_test
         }
         else
         {
-          modules.push_back(std::string(module->location().view()));
+          modules.push_back(code);
         }
       }
     }
@@ -191,18 +284,38 @@ namespace rego_test
   BindingMap TestCase::to_binding_map(const Node& node) const
   {
     BindingMap bindings;
-    for (auto& binding : *node)
+    if (node == Array)
     {
-      if (binding->type() != rego::Binding)
+      for (auto& binding : *node)
       {
-        // raw term
-        continue;
+        assert(binding == Object);
+        for (auto& member : *binding)
+        {
+          Node string = member->front()->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;
+        }
       }
+    }
+    else
+    {
+      for (auto& binding : *node)
+      {
+        if (binding->type() != rego::Binding)
+        {
+          // raw term
+          continue;
+        }
 
-      std::string key = std::string((binding / rego::Var)->location().view());
-      std::string value =
-        rego::to_json((binding / rego::Term), true, m_sort_bindings);
-      bindings[key] = value;
+        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;
+      }
     }
 
     return bindings;
@@ -313,31 +426,41 @@ namespace rego_test
   {
     logging::Debug() << "Loading test cases from " << path;
 
-    auto ast = parser().parse(path);
+    auto result = yaml::reader()
+                    .file(path)
+                    .debug_enabled(!debug_path.empty())
+                    .debug_path(debug_path / "inyaml") >>
+      yaml::to_json()
+        .debug_enabled(!debug_path.empty())
+        .debug_path(debug_path / "tojson");
+    if (!result.ok)
+    {
+      std::cout << "Error processing tests cases in " << path << std::endl;
+      logging::Error err;
+      result.print_errors(err);
+      return {};
+    }
 
-    auto passes = rego_test::passes();
-    PassRange pass_range{passes, wf_parser, "parse"};
+    Node ast = result.ast;
+    std::vector<TestCase> test_cases;
 
-    bool ok;
+    WFContext context(json::wf);
+    Node tests = ast->front();
+    assert(tests == json::Object);
+    auto defs = tests->lookdown({"cases"});
+    if (defs.empty())
     {
-      logging::Info summary;
-      summary << "---------" << std::endl;
-      auto p = default_process(summary, true, "rego_test_cases", debug_path);
-      ok = p.build(ast, pass_range);
-      summary << "---------" << std::endl;
-    }
-    if (!ok)
-    {
-      logging::Error() << "Error with input file " << path;
+      logging::Error() << "No 'cases' block found in test file: "
+                       << path.string();
       return {};
     }
 
-    std::vector<TestCase> test_cases;
+    Node cases = defs[0] / json::Value;
+    assert(cases == json::Array);
 
-    Node case_seq = ast->front()->front()->back();
-    for (Node entry : *case_seq)
+    for (Node entry : *cases)
     {
-      auto maybe_testcase = TestCase::create_from_node(path, entry->front());
+      auto maybe_testcase = TestCase::create_from_node(path, entry);
       if (maybe_testcase.has_value())
       {
         test_cases.push_back(maybe_testcase.value());
@@ -348,12 +471,13 @@ namespace rego_test
   }
 
   std::optional<TestCase> TestCase::create_from_node(
-    const std::filesystem::path& filename, const Node& test_case_map)
+    const std::filesystem::path& filename, const Node& test_case_obj)
   {
+    assert(test_case_obj == json::Object);
     try
     {
       TestCase test_case;
-      auto note = maybe_get_string(test_case_map, "note");
+      auto note = maybe_get_string(test_case_obj, "note");
       if (note.has_value())
       {
         test_case = test_case.note(*note);
@@ -363,7 +487,7 @@ namespace rego_test
         throw std::runtime_error("Note is required");
       }
 
-      auto query = maybe_get_string(test_case_map, "query");
+      auto query = maybe_get_string(test_case_obj, "query");
       if (query.has_value())
       {
         test_case = test_case.query(*query);
@@ -373,27 +497,27 @@ namespace rego_test
         throw std::runtime_error("Query is required");
       }
 
-      auto data = maybe_get_file(test_case_map, "data");
-      if (data.has_value())
+      auto data = maybe_get_object(test_case_obj, "data");
+      if (data.has_value() && (*data)->front() == json::Object)
       {
         test_case = test_case.data(*data);
       }
 
-      auto input = maybe_get_file(test_case_map, "input");
+      auto input = maybe_get_object(test_case_obj, "input");
       if (input.has_value())
       {
         test_case = test_case.input(*input);
       }
 
       test_case.filename(filename)
-        .modules(get_modules(filename.parent_path(), test_case_map))
-        .input_term(get_string(test_case_map, "input_term"))
-        .want_defined(get_bool(test_case_map, "want_defined"))
-        .want_result(get_node(test_case_map, "want_result"))
-        .want_error_code(get_string(test_case_map, "want_error_code"))
-        .want_error(get_string(test_case_map, "want_error"))
-        .sort_bindings(get_bool(test_case_map, "sort_bindings"))
-        .strict_error(get_bool(test_case_map, "strict_error"));
+        .modules(get_modules(filename.parent_path(), test_case_obj))
+        .input_term(get_string(test_case_obj, "input_term"))
+        .want_defined(get_bool(test_case_obj, "want_defined"))
+        .want_result(get_node(test_case_obj, "want_result"))
+        .want_error_code(get_string(test_case_obj, "want_error_code"))
+        .want_error(get_string(test_case_obj, "want_error"))
+        .sort_bindings(get_bool(test_case_obj, "sort_bindings"))
+        .strict_error(get_bool(test_case_obj, "strict_error"));
 
       // --- Special Cases --- //
       // these test cases require some modification due to differences between
@@ -404,7 +528,7 @@ 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(WantResult);
+        Node want_result = NodeDef::create(yaml::Sequence);
         Node result = NodeDef::create(rego::Binding);
         result << (rego::Var ^ "x");
         result << (rego::Term << rego::version());
@@ -418,12 +542,31 @@ namespace rego_test
         test_case.want_error("");
       }
 
+      if (test_case.note() == "jsonbuiltins/yaml unmarshal error")
+      {
+        // the wanted error is implementation and instance specific.
+        // We output quite a lot of error information directly to the error log
+        // but return a clear message, which we check here.
+        test_case.want_error("failed to parse YAML");
+      }
+
+      // TODO
+      // the following test cases are broken, and should be checked with each
+      // new release to see if they have been fixed, as they are essentially
+      // being skipped at present.
+
+      if (test_case.note() == "reachable_paths/cycle_1022_3")
+      {
+        // the test is wrong, the actual result is correct
+        test_case.broken(true);
+      }
+
       return test_case;
     }
     catch (const std::exception& e)
     {
       std::cerr << "Error: " << e.what() << std::endl;
-      std::cerr << test_case_map->location().str() << std::endl;
+      std::cerr << test_case_obj->location().str() << std::endl;
       return std::nullopt;
     }
   }
@@ -432,25 +575,27 @@ namespace rego_test
     const std::filesystem::path& debug_path, bool wf_checks) const
   {
     rego::Interpreter interpreter;
-    interpreter.well_formed_checks_enabled(wf_checks);
-    interpreter.builtins().strict_errors(m_strict_error);
-    if (!debug_path.empty())
-    {
-      interpreter.debug_enabled(true);
-      interpreter.debug_path(debug_path);
-    }
+    interpreter.builtins()->strict_errors(m_strict_error);
+    interpreter.wf_check_enabled(wf_checks)
+      .debug_enabled(!debug_path.empty())
+      .debug_path(debug_path);
 
     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]);
     }
-    interpreter.add_data(m_data);
+
+    if (m_data != nullptr)
+    {
+      interpreter.add_data(m_data);
+    }
+
     if (m_input_term.size() > 0)
     {
-      interpreter.set_input_json(m_input_term);
+      interpreter.set_input_term(m_input_term);
     }
-    else
+    else if (m_input != nullptr)
     {
       interpreter.set_input(m_input);
     }
@@ -467,6 +612,11 @@ namespace rego_test
       return {false, e.what()};
     }
 
+    if (m_broken)
+    {
+      return {true, ""};
+    }
+
     if (actual->type() == ErrorSeq)
     {
       if (actual->size() > 1)
@@ -485,7 +635,7 @@ namespace rego_test
       }
     }
 
-    wf::push_back(interpreter.output_wf());
+    WFContext context(rego::wf_result);
 
     if (m_want_error.length() > 0)
     {
@@ -572,14 +722,12 @@ namespace rego_test
       }
       else
       {
-        std::cout << actual << std::endl;
+        logging::Error() << actual << std::endl;
         pass = false;
         error << "wanted an undefined result, but was defined";
       }
     }
 
-    wf::pop_front();
-
     return {pass, error.str()};
   }
 
@@ -740,4 +888,15 @@ namespace rego_test
     m_strict_error = strict_error;
     return *this;
   }
+
+  bool TestCase::broken() const
+  {
+    return m_broken;
+  }
+
+  TestCase& TestCase::broken(bool broken)
+  {
+    m_broken = broken;
+    return *this;
+  }
 }
\ No newline at end of file
diff --git a/tests/test_case.h b/tests/test_case.h
index 072f4c70..7cc83bfa 100644
--- a/tests/test_case.h
+++ b/tests/test_case.h
@@ -22,6 +22,8 @@ namespace rego_test
       const std::filesystem::path& path,
       const std::filesystem::path& debug_path = "");
 
+    TestCase();
+
     Result run(const std::filesystem::path& debug_path, bool wf_checks) const;
 
     /** name of the test case category. */
@@ -79,6 +81,10 @@ namespace rego_test
     bool strict_error() const;
     TestCase& strict_error(bool strict_error);
 
+    /** indicates that the test is broken and should be skipped */
+    bool broken() const;
+    TestCase& broken(bool broken);
+
   private:
     BindingMap to_binding_map(const Node& node) const;
     bool compare(
@@ -88,7 +94,7 @@ namespace rego_test
       const std::string& wanted,
       std::ostream& os) const;
 
-    static std::optional<Node> maybe_get_file(
+    static std::optional<Node> maybe_get_object(
       const Node& mapping, const std::string& name);
     static std::optional<std::string> maybe_get_string(
       const Node& mapping, const std::string& name);
@@ -116,6 +122,7 @@ namespace rego_test
     std::string m_want_error;
     bool m_sort_bindings;
     bool m_strict_error;
+    bool m_broken;
   };
 
 }