Repository: zefhub/zef (language: python)

Issue:
Api keys
Upping the zefdb protocol version to 6 to replace `ZEFHUB_AUTH_KEY` as an api key


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 4a9afc18..da8f60cc 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -23,7 +23,7 @@ jobs:
           MAYBE_DISPATCH: ${{ github.event.inputs.tag }}
           MAYBE_RELEASE: ${{ github.event.release.tag_name }}
         run: |
-          if [ -n $MAYBE_DISPATCH ] ; then
+          if [ -n "$MAYBE_DISPATCH" ] ; then
               echo ::set-output name=tag::${MAYBE_DISPATCH}
           else
               echo ::set-output name=tag::${MAYBE_RELEASE##pyzef-}
diff --git a/.github/workflows/pyzef-pypi.yml b/.github/workflows/pyzef-pypi.yml
index ed09c1bc..ba70789f 100644
--- a/.github/workflows/pyzef-pypi.yml
+++ b/.github/workflows/pyzef-pypi.yml
@@ -1,11 +1,12 @@
 name: PyPI releases
 
 on:
- push:
-   tags:
-     - "pyzef-*"
-     - "!pyzef-*a*"
-     - "!pyzef-*dev*"
+  push:
+    tags:
+      - "pyzef-*"
+      - "!pyzef-*a*"
+      - "!pyzef-*dev*"
+  workflow_dispatch:
 
 jobs:
   check-license:
@@ -98,4 +99,63 @@ jobs:
         run: |
           ls -R wheelhouse
           pip3 install --upgrade twine
-          python3 -m twine upload -u __token__ -p "${PYPI_TOKEN}" --skip-existing wheelhouse/*
\ No newline at end of file
+          python3 -m twine upload -u __token__ -p "${PYPI_TOKEN}" --skip-existing wheelhouse/*
+
+  push_to_docker_hub:
+    name: Push Docker image to Docker Hub
+    environment: Release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out the repo
+        uses: actions/checkout@v3
+
+      - name: Log in to Docker Hub
+        uses: docker/login-action@v2
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_HUB_TOKEN }}
+
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+        with:
+          images: zefhub/zef
+
+      - name: Build and push Docker image
+        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+        with:
+          context: .
+          push: true
+          file: dockerfiles/Dockerfile.base
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+
+  push_to_github_packages:
+    name: Push Docker image to Github Packages
+    environment: Release
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+
+      - name: Log in to the Container registry
+        uses: docker/login-action@v2
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.CR_PAT }}
+
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+        with:
+          images: ghcr.io/${{ github.repository }}
+
+      - name: Build and push Docker image
+        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+        with:
+          context: .
+          push: true
+          file: dockerfiles/Dockerfile.base
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03f2fea6..fc753cce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,39 @@
+pyzef-0.15.8
+============
+
+New features:
+-------------
+
+- Rich output in `zef.ui`, via `show`, `to_table` and `to_card`.
+- `splice`, `alias`, ... zefops.
+- Filegraphs and credentials are stored together in directory for ZefHub URL.
+  This may require a new login with your credentials.
+
+Breaking changes:
+-----------------
+
+- `ZEFHUB_AUTH_KEY` is repurposed for API keys for service jobs. API keys can be
+  created for your account at `console.zefhub.io`.
+
+Fixes:
+------
+
+- SimpleGQL graphs are kept alive by default.
+- Fixes in ordering of graph delta commands.
+- Zefops `ZefGenerator`s internally now for reliable iteration.
+
+Improvements:
+-------------
+
+- FlatGraphs:
+  - FlatGraphs can absorbed other FlatGraphs from FlatRefs
+- SimpleGQL:
+  - Faster queries/updates with explicit IDs.
+  - Queries work in consistent graph slice.
+  - `debug-level` for extra info
+- Core:
+  - GraphSlices now hold a reference to their corresponding Graph.
+
 pyzef-0.15.7
 ============
 
diff --git a/compile_for_local_dev.sh b/compile_for_local_dev.sh
index ec1f25f9..0ff99286 100755
--- a/compile_for_local_dev.sh
+++ b/compile_for_local_dev.sh
@@ -83,3 +83,5 @@ python3 -m pip install -qr python/requirements.txt || exit 1
 
 ln -fs $(realpath python/pyzef/build/pyzef.* --relative-to=python/zef) python/zef/
 ln -fs $(realpath core/auth.html --relative-to=python/zef) python/zef/
+# Generate the rel_ent_instances.pyi file inplace
+python3 core/scripts/run_cog_gen.py python/zef/core/internals/ python/zef/core/internals/ python/pyzef/build/zef-build/
\ No newline at end of file
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index cfb6301e..f6246d76 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -140,7 +140,6 @@ target_link_libraries(zef PUBLIC range-v3)
 target_link_libraries(zef PRIVATE doctest)
 target_link_libraries(zef PUBLIC websocketpp)
 # target_link_libraries(zef PRIVATE blake3)
-target_link_libraries(zef PUBLIC openssl)
 target_link_libraries(zef PRIVATE libcurl)
 target_link_libraries(zef PRIVATE jwt-cpp)
 # Until I fix up our header deps this will have to be public
diff --git a/core/include/butler/butler.h b/core/include/butler/butler.h
index 63e45dba..e30628e3 100644
--- a/core/include/butler/butler.h
+++ b/core/include/butler/butler.h
@@ -190,6 +190,7 @@ namespace zefDB {
             void handle_incoming_message(json & j, std::vector<std::string> & rest);
 
             void handle_incoming_terminate(json & j);
+            void handle_incoming_force_unsubscribe(json & j);
             void handle_incoming_graph_update(json & j, std::vector<std::string> & rest);
             void handle_incoming_update_tag_list(json & j);
             void handle_incoming_merge_request(json & j);
@@ -210,11 +211,13 @@ namespace zefDB {
             std::atomic_bool no_credentials_warning = false;
             // The authentication string. This is set on connection.
             std::string refresh_token = "";
+            std::string api_key = "";
             // The protocol version chosen for communication. This may have to be autodetected in earlier versions.
             std::atomic_int zefdb_protocol_version = -1;
             constexpr static int zefdb_protocol_version_min = 4;
-            constexpr static int zefdb_protocol_version_max = 5;
+            constexpr static int zefdb_protocol_version_max = 6;
             AtomicLockWrapper auth_locker;
+            std::string upstream_layout();
 
             int chunked_transfer_size_user = -1;
             int chunked_transfer_size = 10*1024;
@@ -271,7 +274,7 @@ namespace zefDB {
             bool want_upstream_connection();
             // wait_for_auth: will start_connection if not already connected
             bool wait_for_auth(std::chrono::duration<double> timeout=std::chrono::seconds(-1));
-            void determine_refresh_token();
+            void determine_login_token();
             std::string who_am_i();
             // ensure_auth_credentials: if no credentials will pop up browser
             void ensure_auth_credentials();
diff --git a/core/include/butler/messages.h b/core/include/butler/messages.h
index f345a770..f7ee49c8 100644
--- a/core/include/butler/messages.h
+++ b/core/include/butler/messages.h
@@ -164,6 +164,10 @@ namespace zefDB {
             UpdatePayload payload;
         };
 
+        struct ForceUnsubscribe {
+            std::string graph_uid;
+        };
+
         struct MergeRequest {
             struct PayloadGraphDelta {
                 // TODO: Currently this is got from python as a python object, but will later be a native C object.
@@ -248,6 +252,7 @@ namespace zefDB {
             NewTransactionCreated,
             Reconnected,
             Disconnected,
+            ForceUnsubscribe,
             NewGraph,
             LocalGraph,
             LoadGraph,
@@ -299,6 +304,7 @@ namespace zefDB {
         CREATE_NAME_STRING(TokenQuery)
         CREATE_NAME_STRING(Reconnected)
         CREATE_NAME_STRING(Disconnected)
+        CREATE_NAME_STRING(ForceUnsubscribe)
         CREATE_NAME_STRING(NewGraph)
         CREATE_NAME_STRING(LocalGraph)
         CREATE_NAME_STRING(LoadGraph)
diff --git a/core/include/verification.h b/core/include/verification.h
index 9ab431d7..162437f0 100644
--- a/core/include/verification.h
+++ b/core/include/verification.h
@@ -45,11 +45,11 @@ namespace zefDB {
         LIBZEF_DLL_EXPORTED void break_graph(Graph&g, blob_index index, int style);
 
         inline bool verify_graph(Graph&g) {
-            // This is to handle mixed kinds of return false/true or the exception throwing
             try {
-                return (verify_graph_double_linking(g)
-                        && verify_chronological_instantiation_order(g));
-            } catch(...) {
+            return (verify_graph_double_linking(g)
+                    && verify_chronological_instantiation_order(g));
+            } catch(const std::exception & e) {
+                std::cerr << "Verification failed with: " << e.what() << std::endl;
                 return false;
             }
         }
diff --git a/core/include/zwitch.h b/core/include/zwitch.h
index c9eba508..6160bff3 100644
--- a/core/include/zwitch.h
+++ b/core/include/zwitch.h
@@ -122,4 +122,15 @@ namespace zefDB {
         std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - time_start_of_process;
         std::cerr << elapsed.count() << " seconds: " << msg << std::endl;
     }
+
+    inline void developer_output(std::string s) {
+        if(!zwitch.developer_output())
+            return;
+
+        std::time_t t = std::time(nullptr);
+        std::tm tm = *std::localtime(&t);
+
+        std::cerr << "DEV-" << std::put_time(&tm, "%T") << ": ";
+        std::cerr << s << std::endl;
+    }
 }
diff --git a/core/src/butler/butler.cpp b/core/src/butler/butler.cpp
index 92a231dd..f8f27801 100644
--- a/core/src/butler/butler.cpp
+++ b/core/src/butler/butler.cpp
@@ -847,8 +847,8 @@ namespace zefDB {
                 return std::filesystem::path(std::string(env)) / upstream_name;
 
             std::filesystem::path path = zefdb_config_path();
-            path /= "graphs";
             path /= upstream_name;
+            path /= "graphs";
             return path;
         }
 
@@ -882,7 +882,17 @@ namespace zefDB {
 
         ////////////////////////////////////////////////////////
         // * External handlers
+        std::string Butler::upstream_layout() {
+            if(butler_is_master)
+                return data_layout_version;
 
+            if(zefdb_protocol_version == -1)
+                throw std::runtime_error("Shouldn't be asking for upstream layout when we haven't connected and done a handshake.");
+
+            if(zefdb_protocol_version <= 6)
+                return "0.2.0";
+            throw std::runtime_error("Don't know what the upstream layout should be for this protocol version");
+        }
 
         // ** Merge handler
         std::optional<std::function<merge_handler_t>> merge_handler;
diff --git a/core/src/butler/butler_connection.cpp b/core/src/butler/butler_connection.cpp
index 6535118d..fc0db952 100644
--- a/core/src/butler/butler_connection.cpp
+++ b/core/src/butler/butler_connection.cpp
@@ -74,15 +74,16 @@ bool Butler::wait_for_auth(std::chrono::duration<double> timeout) {
     return true;
 }
 
-void Butler::determine_refresh_token() {
+void Butler::determine_login_token() {
     std::optional<std::string> key_string = load_forced_zefhub_key();
     if(key_string) {
+        refresh_token = "";
         if(*key_string == constants::zefhub_guest_key) {
             std::cerr << "Connecting as guest user" << std::endl;
-            refresh_token = "";
             have_logged_in_as_guest = true;
-        } else 
-            refresh_token = get_firebase_refresh_token_email(*key_string);
+        } else  {
+            api_key = *key_string;
+        }
         return;
     } else if(!have_auth_credentials()) {
         no_credentials_warning = true;
@@ -108,9 +109,7 @@ std::string Butler::who_am_i() {
         if(*key_string == constants::zefhub_guest_key) {
             name = "GUEST via forced key";
         } else {
-            // This is  duplicated with get_firebase_refresh_token_email which is annoying
-            int colon = key_string->find(':');
-            name = key_string->substr(0,colon) + " via forced key";
+            name = "API key beginning with " + key_string->substr(0, 4) + "...";
         }
     } else if(!have_auth_credentials()) {
         name = "";
@@ -147,10 +146,14 @@ std::string Butler::who_am_i() {
 }
 
 Butler::header_list_t Butler::prepare_send_headers(void) {
-    determine_refresh_token();
+    determine_login_token();
     decltype(network)::header_list_t headers;
-    std::string auth_token = get_firebase_token_refresh_token(refresh_token);
-    headers.emplace_back("X-Auth-Token", auth_token);
+    if(api_key != "") {
+        headers.emplace_back("X-API-Key", api_key);
+    } else {
+        std::string auth_token = get_firebase_token_refresh_token(refresh_token);
+        headers.emplace_back("X-Auth-Token", auth_token);
+    }
     headers.emplace_back("X-Requested-Version", to_str(zefdb_protocol_version_max));
     return headers;
 }
@@ -293,27 +296,14 @@ std::optional<std::string> Butler::load_forced_zefhub_key() {
         return output;
     }
 
-    // Old location for fallback
-#ifdef ZEF_WIN32
-    env = std::getenv("LOCALAPPDATA");
-#else
-    env = std::getenv("HOME");
-#endif
-    std::filesystem::path path2(env);
-    path2 /= ".zefdb";
-    path2 /= "zefhub.key";
-    if (std::filesystem::exists(path2)) {
-        std::ifstream file(path2);
-        std::string output;
-        std::getline(file, output);
-        return output;
-    }
-
     return {};
 }
 
 bool Butler::have_auth_credentials() {
     // TODO: This should become more sophisticated in the future
+    if(api_key != "" || refresh_token != "")
+        return true;
+    
     if(load_forced_zefhub_key())
         return true;
 
@@ -358,7 +348,7 @@ void Butler::ensure_auth_credentials() {
         if(*forced_zefhub_key == constants::zefhub_guest_key)
             have_logged_in_as_guest = true;
         else
-            get_firebase_refresh_token_email(*forced_zefhub_key);
+            api_key = *forced_zefhub_key;
     } else {
         auto credentials_file = credentials_path();
         if(is_credentials_file_valid())
@@ -426,34 +416,11 @@ void Butler::user_login() {
 }
 
 void Butler::user_logout() {
-    // // Always remove any graphs that were being synced with ZefHub. Do
-    // // this first as the sync messages might require the right
-    // // credentials.
-    // std::vector<std::shared_ptr<Butler::GraphTrackingData>> saved_list;
-    // {
-    //     std::lock_guard lock(butler->graph_manager_list_mutex);
-    //     saved_list = butler->graph_manager_list;
-    // }
-    // for(auto data : butler->graph_manager_list) {
-    //     if(!data->gd->should_sync)
-    //         continue;
-
-    //     std::cerr << "Going to stop graph manager: " << uid(*(data->gd)) << std::endl;
-
-    //     try {
-    //         data->queue.set_closed(false);
-    //         long_wait_or_kill(*data->managing_thread, data->return_value, data->uid);
-    //     } catch(const std::exception & e) {
-    //         std::cerr << "Exception while trying to close queue for graph manager thread: " << e.what() << std::endl;
-    //     }
-    // }
-            
-
     auto butler = get_butler();
     butler->session_auth_key.reset();
 
     // Now remove credentials
-    if(load_forced_zefhub_key())
+    if(api_key != "" || load_forced_zefhub_key())
         throw std::runtime_error("Can't logout when an explicit key is given in ZEFHUB_AUTH_KEY or zefhub.key");
     if(have_auth_credentials()) {
         auto credentials_file = credentials_path();
diff --git a/core/src/butler/butler_handlers_graph_manager.cpp b/core/src/butler/butler_handlers_graph_manager.cpp
index 6d36bdbd..8d5b05ea 100644
--- a/core/src/butler/butler_handlers_graph_manager.cpp
+++ b/core/src/butler/butler_handlers_graph_manager.cpp
@@ -67,7 +67,6 @@ void do_reconnect(Butler & butler, Butler::GraphTrackingData & me) {
     auto response = butler.wait_on_zefhub_message<GenericZefHubResponse>(j);
     if(zwitch.graph_event_output())
         std::cerr << "Got response: " << response.j << std::endl;
-    int msg_version = response.j["msg_version"].get<int>();
 
     if(!response.generic.success) {
         me.gd->error_state = GraphData::ErrorState::UNSPECIFIED_ERROR;
@@ -80,6 +79,7 @@ void do_reconnect(Butler & butler, Butler::GraphTrackingData & me) {
         return;
     }
 
+    int msg_version = response.j["msg_version"].get<int>();
     bool hash_agreed;
     if(msg_version <= 2)
         hash_agreed = true;
@@ -461,8 +461,8 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, LoadGra
                 LockGraphData gd_lock{me.gd};
                 UpdateHeads heads = parse_message_update_heads(response.j);
                 apply_sync_heads(*me.gd, heads);
-                if(zwitch.developer_output())
-                    std::cerr << "Upstream head: " << me.gd->sync_head.load() << "/" << me.gd->read_head.load() << std::endl;
+                developer_output("Upstream head: " + to_str(me.gd->sync_head.load()) + "/" + to_str(me.gd->read_head.load()));
+
                 if(me.gd->sync_head < me.gd->read_head.load()) {
                     std::cerr << "WARNING:" << std::endl;
                     std::cerr << "WARNING:" << std::endl;
@@ -756,6 +756,10 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, NotifyS
             }
                 
             if(!me.gd->in_sync()) {
+                if(me.gd->error_state != GraphData::ErrorState::OK) {
+                    msg->promise.set_value(GenericResponse{false, "Graph is in invalid state"});
+                    return;
+                }
                 // The only reason we get here should be because another
                 // thread is writing to the graph and the read/write
                 // heads are out of sync.
@@ -782,6 +786,25 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, SetKeep
     msg->promise.set_value(GenericResponse{true});
 }
 
+template<>
+void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, ForceUnsubscribe & content, Butler::msg_ptr & msg) {
+    if(me.gd->error_state != GraphData::ErrorState::OK) {
+        msg->promise.set_value(GenericResponse{false, "Graph is in error state"});
+        return;
+    }
+
+    me.gd->currently_subscribed = false;
+
+    if(!me.gd->should_sync) {
+        std::cerr << "We didn't want to be subscribed anyway." << std::endl;
+        return;
+    }
+    
+    do_reconnect(*this, me);
+    
+    msg->promise.set_value(GenericResponse(true));
+}
+
 template<>
 void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, GraphUpdate & content, Butler::msg_ptr & msg) {
     if(me.gd->error_state != GraphData::ErrorState::OK) {
diff --git a/core/src/butler/butler_handlers_ws.cpp b/core/src/butler/butler_handlers_ws.cpp
index 9223360b..ee93983f 100644
--- a/core/src/butler/butler_handlers_ws.cpp
+++ b/core/src/butler/butler_handlers_ws.cpp
@@ -164,6 +164,9 @@ void Butler::handle_incoming_message(json & j, std::vector<std::string> & rest)
             if(msg_type == "terminate") {
                 handle_incoming_terminate(j);
                 return;
+            } else if(msg_type == "force_unsubscribe") {
+                handle_incoming_force_unsubscribe(j);
+                return;
             } else if(msg_type == "graph_update") {
                 handle_incoming_graph_update(j, rest);
                 return;
@@ -346,6 +349,20 @@ void handle_token_response(Butler & butler, json & j, Butler::task_promise_ptr &
 }
 
 
+void Butler::handle_incoming_force_unsubscribe(json & j) {
+    std::cerr << "Server requested we give up subscription for " << j["graph_uid"] << " forcibly: " << j["reason"].get<std::string>() << std::endl;
+    ForceUnsubscribe content;
+    content.graph_uid = j["graph_uid"].get<std::string>();
+
+    auto data = find_graph_manager(BaseUID::from_hex(content.graph_uid));
+    if(!data) {
+        std::cerr << "We weren't subscribed anyway!" << std::endl;
+    } else {
+        auto msg = std::make_shared<RequestWrapper>(content);
+        data->queue.push(std::move(msg), true);
+    }
+}
+
 
 
 void Butler::handle_incoming_terminate(json & j) {
diff --git a/core/src/butler/butler_tasks.cpp b/core/src/butler/butler_tasks.cpp
index a6419ec9..e7819056 100644
--- a/core/src/butler/butler_tasks.cpp
+++ b/core/src/butler/butler_tasks.cpp
@@ -124,7 +124,19 @@ Response Butler::wait_on_zefhub_message_any(json & j, const std::vector<std::str
 void Butler::fill_out_ZH_message(json & j) { //, bool add_hints) {
     j["protocol_type"] = "ZEFDB";
     j["protocol_version"] = zefdb_protocol_version.load();
-    j["who"] = get_firebase_token_refresh_token(refresh_token);
+    if(zefdb_protocol_version <= 5) {
+        if(api_key != "")
+            throw std::runtime_error("We got to a point of a using an API key but ZefHub is too old (" + to_str(zefdb_protocol_version) + ") to understand it.");
+        j["who"] = get_firebase_token_refresh_token(refresh_token);
+    } else {
+        if(api_key != "") {
+            j["who"] = json{{"api_key", api_key}};
+        } else {
+            j["who"] = json{
+                {"token", get_firebase_token_refresh_token(refresh_token)}
+            };
+        }
+    }
     // if(add_hints) {
     if(estimated_transfer_speed_accum_time > 0) {
         double speed = estimated_transfer_speed_accum_bytes / estimated_transfer_speed_accum_time;
diff --git a/core/src/butler/communication.cpp b/core/src/butler/communication.cpp
index 60ebd493..08a24d07 100644
--- a/core/src/butler/communication.cpp
+++ b/core/src/butler/communication.cpp
@@ -134,6 +134,16 @@ namespace zefDB {
                 if(response_code == 401) {
                     std::cerr << "Upstream rejected connection: " << response_code << " \"" << con->get_response_msg() << "\"." << std::endl;
                     std::cerr << "Please logout and login again." << std::endl;
+                    auto & response = con->get_response();
+                    if(response.get_header("Content-Length") != "") {
+                        int supposed_length = std::stoi(response.get_header("Content-Length"));
+                        while(!response.ready()) {}
+                        std::string raw;
+                        raw = response.get_body();
+                        if(raw.size() != supposed_length)
+                            std::cerr << "Response is not long enough" << std::endl;
+                        std::cerr << "Response was: " << response.raw() << std::endl;
+                    }
                     close();
                 } else {
                     if(zwitch.zefhub_communication_output())
diff --git a/core/src/mmap.cpp b/core/src/mmap.cpp
index b2cfb988..b4c6d27a 100644
--- a/core/src/mmap.cpp
+++ b/core/src/mmap.cpp
@@ -390,8 +390,7 @@ namespace zefDB {
                     }
                 }
             } else {
-                if(zwitch.developer_output())
-                    std::cerr << "Did not find existing file." << std::endl;
+                developer_output("Did not find existing file.");
             }
 
             // We get here if the file doesn't exist, or we fallback from a corrupted file.
diff --git a/python/pyzef/build_zefDB.cpp b/python/pyzef/build_zefDB.cpp
index 18c27e95..21c76cee 100644
--- a/python/pyzef/build_zefDB.cpp
+++ b/python/pyzef/build_zefDB.cpp
@@ -593,7 +593,7 @@ PYBIND11_MODULE(pyzef, toplevel_module) {
 	verification_submodule.def("verify_graph_double_linking", verification::verify_graph_double_linking, "Check that node/edge linking is consistent for the indexes.");
 	verification_submodule.def("verify_chronological_instantiation_order", verification::verify_chronological_instantiation_order, "Check that RAEs and delegates have a correct chronological instantiation order.");
 	verification_submodule.def("break_graph", verification::break_graph, "Internal use checks");
-	verification_submodule.def("verify_graph", verification::verify_graph, "Internal use checks");
+	verification_submodule.def("verify_graph", verification::verify_graph, "Internal use checks", py::call_guard<py::gil_scoped_release>());
 
 	admins_submodule.def("add_user", [](std::string username, std::string key) { zefDB::user_management("add_user", username, "", key);});
 	admins_submodule.def("reset_user_key", [](std::string username, std::string key) { zefDB::user_management("reset_user_key", username, "", key);});
diff --git a/python/zef/__init__.py b/python/zef/__init__.py
index 91961502..22288b7d 100644
--- a/python/zef/__init__.py
+++ b/python/zef/__init__.py
@@ -24,6 +24,7 @@
 from . import core
 from . import pyzef
 from . import ops
+from . import ui
 
 from .core import *
 
diff --git a/python/zef/core/VT/__init__.py b/python/zef/core/VT/__init__.py
index 3d816f04..970f2fa3 100644
--- a/python/zef/core/VT/__init__.py
+++ b/python/zef/core/VT/__init__.py
@@ -218,22 +218,3 @@ def used_for_ctor(x):
 OperatesOn     = ValueType_(type_name='OperatesOn',   constructor_func = operates_on_ctor,  get_item_func = operates_on_ctor)
 RelatedOps     = ValueType_(type_name='RelatedOps',   constructor_func = related_ops_ctor,  get_item_func = related_ops_ctor)
 UsedFor        = ValueType_(type_name='UsedFor',      constructor_func = used_for_ctor, get_item_func =used_for_ctor)
-
-
-#--------------------------ZefUI Types------------------
-def zef_ui_ctor(type_name, self, *args, **kwargs):
-    if len(args) != 0: 
-        raise ValueError(f'{type_name} constructor takes no positional arguments except self, got {args}')
-
-    ctor_dict = {"type_name":type_name, "constructor_func": partial(zef_ui_ctor, type_name), "pass_self": True}
-    if len(self.d['absorbed']) == 1:
-        return ValueType_( absorbed = ({**self.d['absorbed'][0], **kwargs},), **ctor_dict)
-
-    return ValueType_(absorbed = (kwargs,), **ctor_dict)
-
-Text  = ValueType_(type_name='Text', constructor_func = partial(zef_ui_ctor, 'Text'), pass_self = True)
-Code  = ValueType_(type_name='Code', constructor_func = partial(zef_ui_ctor, 'Code'), pass_self = True)
-Style = ValueType_(type_name='Style', constructor_func = partial(zef_ui_ctor, 'Style'), pass_self = True)
-Table = ValueType_(type_name='Table', constructor_func = partial(zef_ui_ctor, 'Table'), pass_self = True)
-Column = ValueType_(type_name='Column', constructor_func = partial(zef_ui_ctor, 'Column'), pass_self = True)
-Frame = ValueType_(type_name='Frame', constructor_func = partial(zef_ui_ctor, 'Frame'), pass_self = True)
\ No newline at end of file
diff --git a/python/zef/core/VT/value_type.py b/python/zef/core/VT/value_type.py
index 6a6fc5a2..007859bb 100644
--- a/python/zef/core/VT/value_type.py
+++ b/python/zef/core/VT/value_type.py
@@ -82,6 +82,7 @@ def __init__(self, type_name: str, absorbed=(), constructor_func=None, get_item_
             self.d = {
                 'type_name': type_name,
                 'absorbed': absorbed,
+                'alias': None,
             }
             if constructor_func is not None:
                 _value_type_constructor_funcs[type_name] = constructor_func
@@ -92,6 +93,8 @@ def __init__(self, type_name: str, absorbed=(), constructor_func=None, get_item_
             self.allowed_types = (ValueType_, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType, AtomicEntityType, EntityType, RelationType)
 
     def __repr__(self):
+        if self.d['alias'] != None:
+            return self.d['alias']
         return self.d['type_name'] + (
             '' if self.d['absorbed'] == () 
             else ''.join([ f"[{repr(el)}]" for el in self.d['absorbed'] ])
diff --git a/python/zef/core/__init__.py b/python/zef/core/__init__.py
index 98f1c4c2..de187353 100644
--- a/python/zef/core/__init__.py
+++ b/python/zef/core/__init__.py
@@ -148,7 +148,7 @@
 
 # Implementations come last, so that they can make use of everything else
 from . import op_implementations
-
+from .symbolic_expression import SV, SVs, unwrap_vars_hack
 
 pyzef.internals.finished_loading_python_core()
 
diff --git a/python/zef/core/_ops.py b/python/zef/core/_ops.py
index 2cd81c13..93768fdf 100644
--- a/python/zef/core/_ops.py
+++ b/python/zef/core/_ops.py
@@ -364,6 +364,8 @@ def make_zefop(rt):
 # TODO: implement
 on              = make_zefop(RT.On)         
 gather          = make_zefop(RT.Gather)
+alias           = make_zefop(RT.Alias)
+splice          = make_zefop(RT.Splice)
 
           
 # match
diff --git a/python/zef/core/flat_graph.py b/python/zef/core/flat_graph.py
index 8b859c25..87ac08e8 100644
--- a/python/zef/core/flat_graph.py
+++ b/python/zef/core/flat_graph.py
@@ -23,6 +23,19 @@ class Val:
     arg: VT.Any
 
 class FlatGraph_:
+    """
+    Internal data representation:
+    1) self.blobs: a tuple of blobs
+    2) self.key_dict: a dictionary mapping keys to indices in self.blobs
+
+    Each element of self.blobs is a tuple of the form 
+    (
+        index: Int, 
+        blob_type: e.g. ET.Foo / 'BT.ValueNode' / RT.Bar / AET.Int,
+        edge_list: a list of blob indexes (integers). Positive for outgoing, negative for incoming
+        origin_uid (optional)
+    )
+    """
     def __init__(self, *args):
         if args == ():
             self.key_dict = {}
@@ -43,6 +56,9 @@ def __repr__(self):
     def __or__(self, other):
         return LazyValue(self) | other
 
+    def __ror__(self, other):
+        return self | insert[other] 
+
     def __getitem__(self, key):
         return get(self, key)
 
diff --git a/python/zef/core/fx/__init__.py b/python/zef/core/fx/__init__.py
index db51de5a..5d1c0ba6 100644
--- a/python/zef/core/fx/__init__.py
+++ b/python/zef/core/fx/__init__.py
@@ -43,6 +43,7 @@
     graphql_stop_server_handler,
     graphql_start_playground_handler,
     graphql_stop_playground_handler,
+    graphql_generate_schema_string_handler,
 )
 
 
@@ -115,6 +116,7 @@
     FX.GraphQL.StopServer.d: graphql_stop_server_handler,
     FX.GraphQL.StartPlayground.d: graphql_start_playground_handler,
     FX.GraphQL.StopPlayground.d: graphql_stop_playground_handler,
+    FX.GraphQL.GenerateSchemaString.d: graphql_generate_schema_string_handler,
     
     FX.Stream.CreatePushableStream.d: stream_create_pushable_stream_handler,
     FX.Stream.Push.d: stream_push_handler,
diff --git a/python/zef/core/fx/fx_types.py b/python/zef/core/fx/fx_types.py
index 32b11abe..246f0292 100644
--- a/python/zef/core/fx/fx_types.py
+++ b/python/zef/core/fx/fx_types.py
@@ -84,6 +84,7 @@ class _GraphQL_Class():
     StopServer = FXElement(('GraphQL', 'StopServer'))
     StartPlayground = FXElement(('GraphQL', 'StartPlayground'))
     StopPlayground = FXElement(('GraphQL', 'StopPlayground'))
+    GenerateSchemaString = FXElement(('GraphQL', 'GenerateSchemaString'))
 
 
 class _LocalFile_Class():
diff --git a/python/zef/core/fx/graphql.py b/python/zef/core/fx/graphql.py
index 04d75190..7fd6a20e 100644
--- a/python/zef/core/fx/graphql.py
+++ b/python/zef/core/fx/graphql.py
@@ -133,3 +133,19 @@ def graphql_start_playground_handler(eff: dict):
 def graphql_stop_playground_handler(eff: dict):    
     return run({**eff, 'type': FX.HTTP.StopServer})
     
+
+def graphql_generate_schema_string_handler(eff: dict):   
+    """
+    Given a ZefRef to a ET.GQL_Schema node, this effect generates a
+    GraphQL string representation of the schema.
+
+    Usage:
+
+    Effect({
+        type: FX.GraphQL.GenerateSchemaString,
+        schema_root: zr_schema_root,
+    })
+    """
+    from zef.gql.schema_graph_to_file import generate_api_schema_string
+    schema_root = eff["schema_root"] 
+    return generate_api_schema_string(schema_root)
\ No newline at end of file
diff --git a/python/zef/core/graph_delta.py b/python/zef/core/graph_delta.py
index f44ad41a..a8d51694 100644
--- a/python/zef/core/graph_delta.py
+++ b/python/zef/core/graph_delta.py
@@ -1049,7 +1049,7 @@ def can_be_executed(cmd):
             return not isinstance(cmd['rae_type'], RelationType) or (cmd['source'] in ids and cmd['target'] in ids)
         if cmd['cmd'] == 'merge':
             # If the merge is of a relation, we need both source and target to exist already
-            return not isinstance(cmd['origin_rae'], Relation) or (cmd['origin_rae'].d["uids"][0] not in ids or cmd['origin_rae'].d["uids"][2] not in ids)
+            return not isinstance(cmd['origin_rae'], Relation) or (cmd['origin_rae'].d["uids"][0] in ids and cmd['origin_rae'].d["uids"][2] in ids)
         if cmd['cmd'] == 'terminate':
             # Don't terminate if there an upcoming operation that will create this item
             return all(get_id(cmd) not in get_ids(other) for other in input if other['cmd'] != 'terminate')
diff --git a/python/zef/core/graph_slice.py b/python/zef/core/graph_slice.py
index 00dd263b..476d9b05 100644
--- a/python/zef/core/graph_slice.py
+++ b/python/zef/core/graph_slice.py
@@ -35,6 +35,8 @@ def __init__(self, *args):
             if not (isinstance(z, ZefRef) or isinstance(z, EZefRef)):
                 raise TypeError(f'When calling the GraphSlice constructor with a single arguments, this has to be a ZefRef[TX] / EZefRef[TX]. Called with: args={args}')
             self.tx = to_ezefref(z)
+            # Hold a reference to the graph open
+            self._g = Graph(self.tx)
             return
 
         if len(args) == 2:
@@ -54,6 +56,8 @@ def __init__(self, *args):
                 if t >= time(last_tx) else
                 txs | filter[lambda x: time(x) >= t] | ops.first | ops.collect
             )
+            # Hold a reference to the graph open
+            self._g = Graph(self.tx)
             return
 
         raise TypeError(f'The GraphSlice constructor can only be called with one or two args. Called with args={args}')
diff --git a/python/zef/core/op_implementations/dispatch_dictionary.py b/python/zef/core/op_implementations/dispatch_dictionary.py
index e62fdd54..b1939249 100644
--- a/python/zef/core/op_implementations/dispatch_dictionary.py
+++ b/python/zef/core/op_implementations/dispatch_dictionary.py
@@ -311,4 +311,6 @@
         RT.UsedFor:            (used_for_imp, None),
 
         RT.Gather:             (gather_imp, None),
+        RT.Alias:              (alias_imp, None),
+        RT.Splice:             (splice_imp, None),
 }
diff --git a/python/zef/core/op_implementations/implementation_typing_functions.py b/python/zef/core/op_implementations/implementation_typing_functions.py
index 38b0fa51..8c446696 100644
--- a/python/zef/core/op_implementations/implementation_typing_functions.py
+++ b/python/zef/core/op_implementations/implementation_typing_functions.py
@@ -70,6 +70,40 @@ def verify_zef_list(z_list: ZefRef):
 
 
 
+
+class ZefGenerator:
+    """
+    A class that acts as a uniform interface instead of Python's
+    builtin generator. Defining a regular generator using yield
+    in Python does not always obey value semantics. It still keeps
+    state when iterated over.
+    
+    Example:
+    >>> def repeat(x, n=None):    
+    >>>  def make_wrapper():
+    >>>      if n is None:
+    >>>          while True:
+    >>>              yield x
+    >>>      else:
+    >>>             for _ in range(n):
+    >>>                 yield x
+    >>>  
+    >>>     return ZefGenerator(make_wrapper)
+
+    Returning `wrapper()` directly is equivalent to the class
+    where `__iter__` returns self, thus maintaining state from
+    previous iterations.
+    """
+    def __init__(self, generator_fct):
+        self.generator_fct = generator_fct
+
+    def __iter__(self):
+        return self.generator_fct()
+
+
+
+
+
 ####################################################
 # * TEMPLATE FOR DOCSTRINGS
 #--------------------------------------------------
@@ -400,11 +434,11 @@ def zip_imp(x, second=None, *args):
     if second is None:
         def wrapper1():
             yield from builtins.zip(*x)
-        return wrapper1()
+        return ZefGenerator(wrapper1)
         
     def wrapper2():
         yield from builtins.zip( *(x, second, *args) )    
-    return wrapper2()
+    return ZefGenerator(wrapper2)
 
 
 
@@ -460,12 +494,21 @@ def concat_implementation(v, first_curried_list_maybe=None, *args):
         else:
             return [v, first_curried_list_maybe, *args] | join[''] | collect
     else:
-        import more_itertools as mi
-        import itertools
         if first_curried_list_maybe is None:
-            return (el for sublist in v for el in sublist)
+            def wrapper():
+                it = iter(v)
+                try:
+                    while True:
+                        yield from next(it)
+                except StopIteration:
+                    return
+            
+            return ZefGenerator(wrapper)
         else:
-            return (el for sublist in (v, first_curried_list_maybe, *args)  for el in sublist)
+            def wrapper():
+                yield from (el for sublist in (v, first_curried_list_maybe, *args)  for el in sublist)
+            return ZefGenerator(wrapper)
+            
 
 
 
@@ -555,11 +598,11 @@ def prepend_imp(v, item, *additional_items):
         return (item, *v)
     elif isinstance(v, str):
         return item + v
-    elif isinstance(v, Generator) or isinstance(v, Iterator):
+    elif isinstance(v, Generator) or isinstance(v, Iterator) or isinstance(v, ZefGenerator):
         def generator_wrapper():
             yield item
             yield from v
-        return generator_wrapper()
+        return ZefGenerator(generator_wrapper)
     else:
         raise TypeError(f'prepend not implemented for type {type(v)}')
 
@@ -644,11 +687,11 @@ def append_imp(v, item, *additional_items):
 
     elif isinstance(v, str):
         return v + item
-    elif isinstance(v, Generator) or isinstance(v, Iterator):
+    elif isinstance(v, Generator) or isinstance(v, Iterator) or isinstance(v, ZefGenerator):
         def generator_wrapper():
             yield from v
             yield item
-        return generator_wrapper()
+        return ZefGenerator(generator_wrapper)
     else:
         raise TypeError(f'append not implemented for type {type(v)}')
 
@@ -830,8 +873,8 @@ def update_at_imp(v: list, n, f):
     assert isinstance(n, int)
     assert callable(f)
 
-    it = iter(v)
     def wrapper():
+        it = iter(v)
         if n>=0:
             try:
                 for _ in range(n):
@@ -841,7 +884,7 @@ def wrapper():
             except StopIteration:
                 return        
         else:
-            # we want to enable this lazily=, but only know which
+            # we want to enable this lazily, but only know which
             # element to apply the function to once the iterable completes.
             # This means we need to cache n values at any point in time.
             # Do this mutatingly here to optimize the tight loop.
@@ -865,7 +908,7 @@ def wrapper():
                 yield from cached[:offset]
                 return
 
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 
@@ -960,7 +1003,7 @@ def wrapper():
                 yield from cached[:offset]
                 return
 
-    return wrapper()    
+    return ZefGenerator(wrapper)    
 
 
 
@@ -1125,9 +1168,9 @@ def stride_imp(v, step: int):
     related zefop: sliding
     related zefop: slice
     """
-    it = iter(v)  
     # shield the yield for when we extend this.  
     def wrapper():
+        it = iter(v)  
         while True:
             try:
                 yield next(it)
@@ -1135,7 +1178,7 @@ def wrapper():
                     next(it)                
             except StopIteration:            
                 return
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 def stride_tp(v_tp, step_tp):
@@ -1182,31 +1225,36 @@ def chunk_imp(iterable, chunk_size: int):
     related zefop: sliding
     related zefop: slice
     """
-    it = iter(iterable)
     if isinstance(iterable, str):
-        c = 0
-        while c*chunk_size < len(iterable):
-            yield iterable[c*chunk_size:(c+1)*chunk_size]
-            c+=1
-        return
+        def wrapper():
+            c = 0        
+            while c*chunk_size < len(iterable):
+                yield iterable[c*chunk_size:(c+1)*chunk_size]
+                c+=1
+            return
+        return ZefGenerator(wrapper)
 
-    while True:
-        try:
-            # evaluate the entire chunk. Otherwise we have to share 
-            # state between the generators that are returned            
-            # this_chunk = [next(it) for _ in range(chunk_size)]
-            # we need to do this imperatively to treat the very last
-            # chunk correctly: the iterable may raise a StopIteration,
-            # but we want to have the list up to that point.
-            # A list comprehension doesn't do this easily.
-            this_chunk = []
-            for _ in range(chunk_size):
-                this_chunk.append(next(it))
-            yield this_chunk
-        except StopIteration:            
-            if this_chunk != []:
+    def wrapper():
+        it = iter(iterable)
+        while True:
+            try:
+                # evaluate the entire chunk. Otherwise we have to share 
+                # state between the generators that are returned            
+                # this_chunk = [next(it) for _ in range(chunk_size)]
+                # we need to do this imperatively to treat the very last
+                # chunk correctly: the iterable may raise a StopIteration,
+                # but we want to have the list up to that point.
+                # A list comprehension doesn't do this easily.
+                this_chunk = []
+                for _ in range(chunk_size):
+                    this_chunk.append(next(it))
                 yield this_chunk
-            return
+            except StopIteration:            
+                if this_chunk != []:
+                    yield this_chunk
+                return
+
+    return ZefGenerator(wrapper)
 
 
 def chunk_tp(v_tp, step_tp):
@@ -1236,28 +1284,30 @@ def sliding_imp(iterable, window_size: int, stride_step: int=1):
     - related zefop: slice
     - operates on: List
     """
-    it = iter(iterable)
-    try:
-        w = []
-        for c in range(window_size):
-            w.append(next(it))
-        yield tuple(w)
-    except StopIteration:
-        yield tuple(w)
-        return    
-    while True:
+    def wrapper():
+        it = iter(iterable)
         try:
-            new_vals = []
-            for c in range(stride_step):
-                new_vals.append(next(it))
-            w = (*w[stride_step:], *new_vals[-window_size:])
-            yield w
-        
+            w = []
+            for c in range(window_size):
+                w.append(next(it))
+            yield tuple(w)
         except StopIteration:
-            if new_vals == []: return
-            yield (*w[stride_step:], *new_vals)
-            return      #TODO!!!!!!!!!!!!!
+            yield tuple(w)
+            return    
+        while True:
+            try:
+                new_vals = []
+                for c in range(stride_step):
+                    new_vals.append(next(it))
+                w = (*w[stride_step:], *new_vals[-window_size:])
+                yield w
+            
+            except StopIteration:
+                if new_vals == []: return
+                yield (*w[stride_step:], *new_vals)
+                return      #TODO!!!!!!!!!!!!!
     
+    return ZefGenerator(wrapper)
         
 
 def sliding_tp(v_tp, step_tp):
@@ -1376,22 +1426,26 @@ def insert_into_imp(key_val_pair, x):
     # TODO: make this work: (10, 'a') | insert_into[range(10) | map[add[100]]] | take[5] | c
 
     """
+    from typing import Generator
     if not isinstance(key_val_pair, (list, tuple)):
         return Error(f'in "insert_into": key_val_pair must be a list or tuple. It was type(x)={type(x)}     x={x}')
     
     k, v = key_val_pair
     if isinstance(x, dict):
         return {**x, k:v}
-    if type(x) in {list, tuple, range}:
+    if type(x) in {list, tuple, range, Generator, ZefGenerator}:        
         assert isinstance(k, int)
         # So much laziness!
-        it = iter(x)
-        def tmp():
-            for c in range(k):
-                yield next(it)
-            yield v
-            yield from it
-        return tmp()
+        def wrapper():
+            it = iter(x)
+            try:
+                for c in range(k):
+                    yield next(it)
+                yield v
+                yield from it
+            except StopIteration:
+                return
+        return ZefGenerator(wrapper)
 
 
 
@@ -1462,7 +1516,7 @@ def get_imp(d, key, default=Error('Key not found in "get"')):
         return fg_get_imp(d, key)
     elif isinstance(d, dict):
         return d.get(key, default)
-    elif isinstance(d, list) or isinstance(d, tuple) or isinstance(d, Generator):
+    elif isinstance(d, list) or isinstance(d, tuple) or isinstance(d, Generator) or isinstance(d, ZefGenerator):
         return Error(f"get called on a list. Use 'nth' to get an element at a specified index.")
     elif isinstance(d, Graph) or isinstance(d, GraphSlice):
         try:
@@ -1620,7 +1674,7 @@ def reverse_imp(v):
     - operates on: List
     """
     from typing import Generator
-    if isinstance(v, Generator): return (tuple(v))[::-1]
+    if isinstance(v, (Generator, ZefGenerator)): return (tuple(v))[::-1]
     if isinstance(v, str): return v[::-1]
     if isinstance(v, list): return v[::-1]
     if isinstance(v, tuple): return v[::-1]
@@ -1651,21 +1705,23 @@ def cycle_imp(iterable, n=None):
     - used for: stream manipulation
     - operates on: List
     """
-    if n==0:
-        return
-    cached = []
-    for x in iterable:
-        cached.append(x)
-        yield x
-
-    rerun = 1
-    while True:
-        rerun += 1
-        if n is not None and rerun > n:
+    def wrapper():
+        cached = []
+        if n==0:
             return
-        for x in cached:
+
+        for x in iterable:
+            cached.append(x)
             yield x
 
+        rerun = 1
+        while True:
+            rerun += 1
+            if n is not None and rerun > n:
+                return
+            for x in cached:
+                yield x
+    return ZefGenerator(wrapper)
 
 
 def cycle_tp(iterable_tp, n_tp):
@@ -1674,7 +1730,7 @@ def cycle_tp(iterable_tp, n_tp):
 
 
 #---------------------------------------- repeat -----------------------------------------------
-def repeat_imp(iterable, n=None):
+def repeat_imp(x, n=None):
     """
     Repeat an element a given number of times 
     None (default) means infinite).
@@ -1687,9 +1743,18 @@ def repeat_imp(iterable, n=None):
     - related zefop: cycle
     - operates on: Any
     """
-    import itertools
-    if isinstance(iterable, Generator) or isinstance(iterable, Iterator): iterable = [i for i in iterable]
-    return itertools.repeat(iterable) if n is None else itertools.repeat(iterable, n)
+    def wrapper():
+        try:
+            if n is None:
+                while True:
+                    yield x
+            else:
+                for _ in range(n):
+                    yield x
+        except StopIteration:
+            return
+
+    return ZefGenerator(wrapper)
 
 
 def repeat_tp(iterable_tp, n_tp):
@@ -1726,7 +1791,7 @@ def contains_tp(x_tp, el_tp):
 
 #---------------------------------------- contained_in -----------------------------------------------
 def contained_in_imp(x, el):
-    if isinstance(x, Generator) or isinstance(x, Iterator): x = [i for i in x]
+    if isinstance(x, (Generator, ZefGenerator)) or isinstance(x, Iterator): x = [i for i in x]
     return x in el
 
 
@@ -1862,7 +1927,7 @@ def all_imp(*args):
     # once we're here, we interpret it like the Python "all"
     v = args[0]
     assert len(args) == 1
-    assert isinstance(v, list) or isinstance(v, tuple) or isinstance(v, Generator) or isinstance(v, Iterator)
+    assert isinstance(v, list) or isinstance(v, tuple) or isinstance(v, (Generator, ZefGenerator)) or isinstance(v, Iterator)
     # TODO: extend to awaitables
     return builtins.all(v)
             
@@ -1956,7 +2021,7 @@ def wrapper():
                 yield next(it)        
         except StopIteration:
             return
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 def trim_left_tp(v_tp, el_to_trim_tp):
@@ -2058,7 +2123,7 @@ def tap_imp(x, fct):
     - related zefop: apply
     """
     # As we can receive a generator, we need to collect it up first before continuing
-    if isinstance(x, Generator):
+    if isinstance(x, (Generator, ZefGenerator)):
         x = list(x)
     fct(x)
     return x
@@ -3039,12 +3104,12 @@ def skip_imp(v, n: int):
     # if isinstance(v, ZefRefs) or isinstance(v, EZefRefs):
     #     return pyzefops.drop(v, n)
     if n>=0:
-        it = iter(v)
-        for _ in range(n):
-            next(it)
-        def gen():
+        def wrapper():
+            it = iter(v)
+            for _ in range(n):
+                next(it)
             yield from it
-        return gen()    
+        return ZefGenerator(wrapper)    
     # n<0
     else:
         cached = tuple(v)
@@ -3121,8 +3186,18 @@ def iterate_implementation(x, f):
     ---- Signature ----
     (T, (T->T)) -> List[T]
     """
-    # TODO: assert that fct is an endomorphism on zef_type(x)
-    return itertools.accumulate(itertools.repeat(x), lambda fx, _: f(fx))
+    def wrapper():
+        current_val = x
+        try:
+            yield current_val
+            while True:
+                new_val = f(current_val)
+                current_val = new_val
+                yield new_val
+        except StopIteration:
+            return
+
+    return ZefGenerator(wrapper)
 
 
 def iterate_type_info(op, curr_type):
@@ -3146,19 +3221,49 @@ def take_implementation(v, n):
     negative n: take n last items. Must evaluate entire iterable, does not terminate 
     for infinite iterables.
 
+    ---- Examples ----
+    >>> ['a', 'b', 'c'] | take[2]       # => ['a', 'b']
+    >>> ['a', 'b', 'c'] | take[-2]      # => ['b', 'c']
+    >>> 'hello' | take[3]               # => 'hel'
+    >>> 'hello' | take[-3]              # => 'llo'
 
     ---- Signature ----
     (List[T], Int) -> List[T]
+    (String, Int)  -> String
+
+    ---- Tags ----
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: slice
+    - related zefop: reverse
+    - related zefop: first
+    - related zefop: last
+    - related zefop: nth    
     """
-    import itertools
     if isinstance(v, ZefRef) or isinstance(v, EZefRef):
         return take(v, n)
+    elif isinstance(v, str):
+        return v[:n] if n>=0 else v[n:]
     else:
-        if n >= 0:            
-            return (x for x in itertools.islice(v, n))
+        if n >= 0:
+            def wrapper():
+                it = iter(v)
+                try:
+                    for _ in range(n):
+                        yield next(it)
+                except StopIteration:
+                    return
+
+            return ZefGenerator(wrapper)
         else:
-            vv = list(v)
+            if isinstance(v, str): return v[n:]
+            vv = tuple(v)
             return vv[n:]
+
+
+
         
         
 def take_type_info(op, curr_type):
@@ -3167,9 +3272,34 @@ def take_type_info(op, curr_type):
 
 
 #---------------------------------------- take_while -----------------------------------------------
-def take_while_imp(it, predicate):
-    import itertools
-    return itertools.takewhile(predicate, it)
+def take_while_imp(v, predicate):
+    """ 
+    Similar to take_until, but with positive predicate and 
+    it does not include the bounding element.
+    
+    ---- Examples ----
+    >>> range(10) | take_while[lambda x: x<4]       # => [0, 1, 2, 3]
+
+    ---- Signature ----
+    (List[T], (T->Bool)) -> List[T]
+
+
+    ---- Tags ----
+    - operates on: List
+    - used for: list manipulation
+    - related zefop: take
+    - related zefop: take_until
+    - related zefop: skip
+    - related zefop: skip_while
+    """
+    def wrapper():
+        it = iter(v)
+        for el in it:
+            if predicate(el):
+                yield el                
+            else:
+                return
+    return ZefGenerator(wrapper)
 
 def take_while_tp(it_tp, pred_type):
     return it_tp
@@ -3178,24 +3308,27 @@ def take_while_tp(it_tp, pred_type):
 
 
 #---------------------------------------- take_until -----------------------------------------------
-def take_until_imp(it, predicate):
+def take_until_imp(v, predicate):
     """ 
     Similar to take_while, but with negated predicate and 
     it includes the bounding element. Useful in some cases,
     but take_while is more common.
     
     ---- Examples ----
-    >>> range(10) | take_until[lambda x: x>4] | collect       # => [0, 1, 2, 3, 4, 5]
+    >>> range(10) | take_until[lambda x: x>4]       # => [0, 1, 2, 3, 4, 5]
 
     ---- Signature ----
     (List[T], (T->Bool)) -> List[T]
     """
-    for el in it:
-        if not predicate(el):
-            yield el
-        else:
-            yield el
-            break
+    def wrapper():
+        it = iter(v)
+        for el in it:
+            if not predicate(el):
+                yield el
+            else:
+                yield el
+                break
+    return ZefGenerator(wrapper)
 
 def take_until_tp(it_tp, pred_type):
     return it_tp
@@ -3224,19 +3357,21 @@ class Sentinel:
         pass
     sentinel = Sentinel()       # don't reuse any value the user could have given
     _prev_value = sentinel
-    
-    it = iter(iterable)
-    val = next(it)
-    yield val
-    while True:
-        _prev_value = val
-        try:
-            val = next(it)
-        except StopIteration:
-            return
-        if not binary_predicate(_prev_value, val):
-            return
+    def wrapper():
+        it = iter(iterable)
+        val = next(it)
         yield val
+        while True:
+            _prev_value = val
+            try:
+                val = next(it)
+            except StopIteration:
+                return
+            if not binary_predicate(_prev_value, val):
+                return
+            yield val
+
+    return ZefGenerator(wrapper)
 
 
 
@@ -3730,7 +3865,8 @@ def wrapper():
         except StopIteration:
             return
 
-    return wrapper()
+    return ZefGenerator(wrapper)
+
 
 
 def distinct_tp(x):
@@ -3776,7 +3912,7 @@ def wrapper():
         except StopIteration:
             return
 
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 def distinct_by_tp(x):
@@ -3896,14 +4032,14 @@ def rep(el):
         raise TypeError('Dont use "replace", but "insert" to replace a key value pair in a dictionary')
   
     if isinstance(collection, Iterable):
-        it = iter(collection)
-        def my_gen():
+        def wrapper():
+            it = iter(collection)
             try:
                 while True:
                     yield rep(next(it))            
             except StopIteration:
                 return
-        return my_gen()
+        return ZefGenerator(wrapper)
     
     raise TypeError('not implemented')
 
@@ -3958,7 +4094,9 @@ def slice_imp(v, start_end: tuple):
 
     ---- Examples ----
     >>> ['a', 'b', 'c', 'd'] | slice[1:2]    # => ['b', 'c']
-    >>> 'abcdefgh' | slice_imp[1,6,2]        # => 'bdf'
+    >>> 'abcdefgh' | slice[1,6,2]            # => 'bdf'
+    >>> 'hello' | slice[1,3]                 # => 'el'
+    >>> 'hello' | slice[1,-1]                # => 'ello'
     
     ---- Tags ----
     - operates on: List
@@ -3979,20 +4117,20 @@ def slice_imp(v, start_end: tuple):
     
     if type(v) in {list, tuple, str}:
         if len(start_end) == 2:
-            return v[start_end[0]: start_end[1]]
+            end = start_end[1] if start_end[1]>=0 else start_end[1]+1
+            return v[start_end[0]: end] if start_end[1] !=-1 else v[start_end[0]:]
         if len(start_end) == 3:
             return v[start_end[0]: start_end[1] : start_end[2]]
-    elif isinstance(v, Generator):
+    elif isinstance(v, Generator) or isinstance(v, ZefGenerator):
         start, end = start_end
         # don't returns a custom slice object, but a generator to make it uniform.
         def wrapper():
             yield from itertools.islice(v, start, end)        
-        return wrapper()
+        return ZefGenerator(wrapper)
     else:
         raise TypeError(f"`slice` not implemented for type {type(v)}: {v}")
 
 
-
 def slice_tp(*args):
     return VT.Any
 
@@ -4038,7 +4176,7 @@ def wrapper():
         except StopIteration:
             yield s
             return
-    return wrapper()
+    return ZefGenerator(wrapper)
     
 
 def split_tp(*args):
@@ -4087,7 +4225,7 @@ def wrapper():
         except StopIteration:
             if s!=[]: yield s
             return
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 # ---------------------------------------- split_right -----------------------------------------------
@@ -4131,7 +4269,7 @@ def wrapper():
         except StopIteration:
             if s!=[]: yield s
             return
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 # ------------------------------------------ split_if ---------------------------------------------
@@ -4174,7 +4312,7 @@ def wrapper():
         except StopIteration:
             yield s
             return
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 
@@ -5047,7 +5185,7 @@ def apply_functions_imp(x, fns):
     from typing import Generator
     if not (isinstance(fns, list) or isinstance(fns, tuple)):
         raise TypeError(f"apply_functions must be given a tuple of functions.")
-    if not (isinstance(x, list) or isinstance(x, tuple) or isinstance(x, Generator)):
+    if not (isinstance(x, list) or isinstance(x, tuple) or isinstance(x, (Generator, ZefGenerator))):
         raise TypeError(f"apply_functions must be given a tuple of input args.")
     xx = tuple(x)
     if not len(fns) == len(xx):
@@ -5095,12 +5233,12 @@ def map_implementation(v, f):
             def wrapper_list():
                 for el in v:            
                     yield tuple(ff(el) for ff in f)
-            return wrapper_list()
+            return ZefGenerator(wrapper_list)
         else:
             def wrapper():
                 for el in v:
                     yield f(el)
-            return wrapper()       
+            return ZefGenerator(wrapper)
 
 
 
@@ -5194,7 +5332,7 @@ def wrapper():
                 yield tuple(current_list)
             return
 
-    return wrapper()
+    return ZefGenerator(wrapper)
 
 
 
@@ -5273,12 +5411,15 @@ def nth_implementation(iterable, n):
         
     if isinstance(iterable, list) or isinstance(iterable, tuple) or isinstance(iterable, str):
         return iterable[n]
+    
+    # it must be a generator or zef generator
+    if n<0: 
+        return tuple(iterable)[n]    # if we're taking from the back, it must be non-infinite in length
+    
     it = iter(iterable)
     for cc in range(n):
         next(it)
-    return next(it)
-    #   TODO: implementation for awaitables
-            
+    return next(it)    
 
 
 
@@ -5417,6 +5558,34 @@ def sort_implementation(v, key_fct=None, reverse=False):
 
 
 def to_delegate_implementation(first_arg, *curried_args):
+    """
+    Convert the Graph delegate representation to the Delegate representation,
+    which is not specific to any graph, or vice versa.
+    
+    Converting from a Graph EZefRef to a Delegate requires no additional curried
+    arguments.
+
+    Converting from a Delegate to a Graph EZefRef requires one argument of a
+    Graph and can take an additional argument specifying whether the EZefRef
+    should be created if it doesn't already exist. If this is not given, and the
+    delegate doesn't exist, then this function returns None.
+
+    ---- Examples ----
+    >>> z_entity_del | to_delegate      # A Delegate(...) of the entity
+    >>> delegate | to_delegate[g][True] # A EZefRef of the delegate
+    >>> delegate | to_delegate          # => delegate
+
+    ---- Signature ----
+    Delegate -> Delegate
+    (Delegate, Graph) -> EZefRef | Nil
+    (Delegate, Graph, True) -> EZefRef
+    EZefRef[Delegate] -> Delegate
+
+    ---- Tags ----
+    - related zefop: delegate_of
+
+    """
+
     if isinstance(first_arg, Delegate):
         if len(curried_args) == 0:
             return first_arg
@@ -5447,6 +5616,47 @@ def attempt_to_delegate(args):
         return Delegate(args)
 
 def delegate_of_implementation(x, arg1=None, arg2=None):
+    """
+    With no additional arguments, takes the input and produces an abstract
+    Delegate from it. The input could be a ZefRef on a graph, an
+    ET/RT/AET/RelationTriple type, or another delegate. The output will always
+    be one order of delegate higher than the input.
+    
+    With one additional argument, which must be a Graph, this will lookup the
+    ZefRef of the equivalent delegate on the graph, if it exists. A second
+    additional argument, set to True, forces the creation of the ZefRef on the
+    graph.
+    
+    This and `to_delegate` can be used interchangably in many ways.
+
+    ---- Examples ----
+    >>> z | delegate_of
+    >>> z | delegate_of | to_delegate == z | to_delegate | delegate_of
+    >>> ET.Machine | delegate_of == Delegate(ET.Machine) | delegate_of
+    >>> (ET.A, RT.B, ET.C) | delegate_of | source == ET.A | delegate_of
+    >>> z | delegate_of | all    # Get all instances that are the same type as z
+    
+    >>> ET.Machine | delegate_of[g] | now | all == g | now | all[ET.Machine]
+    >>> ET.Machine | delegate_of[g][True]   # Creates the delegate ZefRef on the graph g
+    
+    >>> z = ET.Machine | g | run
+    ... dz = z | delegate_of | collect
+    ... a,b,c = (dz, RT.Meta, "metadata") | g | run
+    ... b | delegate_of | discard_frame == (delegate_of(ET.Machine), RT.Meta, AET.String) | delegate_of[g]
+
+    ---- Signature ----
+    Delegate -> Delegate
+    AnyRAEType -> Delegate
+    ZefRef -> ZefRef | Nil
+    EZefRef -> EZefRef | Nil
+    (Delegate | AnyRAEType, Graph) -> EZefRef | Nil
+    (Delegate | AnyRAEType, Graph, Bool) -> EZefRef
+
+    where AnyRAEType = ET | AET | RT | RelationTriple
+
+    ---- Tags ----
+    - related zefop: to_delegate
+    """
     if isinstance(x, EZefRef) or isinstance(x, ZefRef):
         assert arg2 is None
         if arg1 is None:
@@ -6126,7 +6336,8 @@ class Sentinel: pass
             return True
         elif isinstance(x, list) or isinstance(x, tuple):
             for p_e, x_e in zip(p, x): # Creates tuples of pairwise elements from both lists
-                if not isinstance(p_e, ValueType_): raise ValueError(f"The pattern passed didn't have a ValueType but rather {v}")
+                if type(p_e) not in {ValueType_, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct}:
+                    raise ValueError(f"The pattern passed didn't have a ValueType but rather {p_e}")
                 if not is_a(x_e, p_e): return False  
             return True
         
@@ -6136,11 +6347,11 @@ class Sentinel: pass
     def list_matching(x, tp):
         import sys
         from typing import Generator
-        if not (isinstance(x, list) or isinstance(x, tuple) or isinstance(x, Generator)):
+        if not (isinstance(x, list) or isinstance(x, tuple) or isinstance(x, (Generator, ZefGenerator))):
             return False
         ab = tp.d['absorbed']
         if ab != ():
-            if isinstance(x, Generator):
+            if isinstance(x, (Generator, ZefGenerator)):
                 raise NotImplementedError()
             if len(ab)!=1:    # List takes only one Type argument
                 print('Something went wrong in `is_a[List[...]]`: multiple args curried into ', file=sys.stderr)
@@ -6832,7 +7043,7 @@ def merge_imp(a, second=None, *args):
     if is_a(a, FlatGraph) and is_a(second, FlatGraph):
         return fg_merge_imp(a, second)
     elif second is None:
-        assert isinstance(a, tuple) or isinstance(a, list) or isinstance(a, Generator)
+        assert isinstance(a, tuple) or isinstance(a, list) or isinstance(a, (Generator, ZefGenerator))
         return {k: v for d in a for k, v in d.items()}
     else:
         assert isinstance(a, dict)
@@ -7222,7 +7433,7 @@ def to_pipeline_imp(ops: list):
     - related zefop: bypass
     """
     from typing import Generator, Iterable, Iterator
-    if isinstance(ops, Generator) or isinstance(ops, Iterator): ops = [op for op in ops]
+    if isinstance(ops, (Generator, ZefGenerator)) or isinstance(ops, Iterator): ops = [op for op in ops]
     return identity if len(ops) == 0 else (ops[1:] | reduce[lambda v, el: v | el][ops[0]] | collect)
 
 
@@ -7548,7 +7759,7 @@ def replace_at_imp(str_or_list, index, new_el):
         if index >= len(s) or index < 0: return s
         if index == len(s) - 1: return s[:index] + char
         return s[:index] + char + s[index+1:] 
-    elif isinstance(str_or_list, list) or isinstance(str_or_list, tuple) or isinstance(str_or_list, Generator):
+    elif isinstance(str_or_list, list) or isinstance(str_or_list, tuple) or isinstance(str_or_list, Generator) or isinstance(str_or_list, ZefGenerator):
         def wrapper():
             it = iter(str_or_list)
             c = 0
@@ -7561,7 +7772,7 @@ def wrapper():
                 yield from it
             except StopIteration:
                 return
-        return wrapper()
+        return ZefGenerator(wrapper)
     else:
         return Error.TypeError(f"Expected an string or a list. Got {type(str_or_list)} instead.")
 
@@ -8293,8 +8504,12 @@ def common_logic(new_el):
             idx = new_key_dict[hash_vn]
 
         elif isinstance(new_el, FlatRef):
-            assert new_el.fg == fg, "The FlatRef's origin FlatGraph doesn't match current FlatGraph."
-            idx = new_el.idx
+            if new_el.fg == fg:
+                idx = new_el.idx
+            else:
+                # If the flatgraphs are different then merge the FlatGraph in and return the
+                # new index of the blob originally in the other FlatGraph
+                idx = fr_merge_and_retrieve_idx(new_blobs, new_key_dict,next_idx, new_el)
 
         elif type(new_el) in list(map_scalar_to_aet_type.keys()): 
             aet = map_scalar_to_aet_type[type(new_el)](new_el)
@@ -8394,6 +8609,53 @@ def _insert_single(new_el):
     new_fg.blobs = (*new_blobs,)
     return new_fg
 
+def fr_merge_and_retrieve_idx(blobs, k_dict, next_idx, fr):
+    fr_idx = fr.idx
+    fg2 = fr.fg
+
+    idx_key_2 = {i:k for k,i in fg2.key_dict.items()}
+    old_to_new = {}
+
+    def retrieve_or_insert_blob(new_b):
+        old_idx = new_b[0]
+        key = idx_key_2.get(new_b[0], None)
+
+        if old_idx in old_to_new:
+            idx = old_to_new[old_idx]
+            new_b = blobs[idx]
+        elif (new_b[1] == 'BT.ValueNode' or is_a(new_b[3], uid)) and key in k_dict:
+            new_b = blobs[k_dict[key]]
+            idx = new_b[0]
+        else:
+            idx = next_idx()
+            new_b = (idx, new_b[1], [], *new_b[3:])
+            blobs.append(new_b)
+        old_to_new[old_idx] = idx
+        return new_b
+
+    for b in fg2.blobs | filter[lambda b: isinstance(b[1], RelationType)] | sort[lambda b: -len(b[2])]:
+        src_b, trgt_b = fg2.blobs[b[4]], fg2.blobs[b[5]]
+        src_b  = retrieve_or_insert_blob(src_b)
+        trgt_b = retrieve_or_insert_blob(trgt_b)
+        
+        idx = next_idx()
+        rt_b = (idx, b[1], [], None, src_b[0], trgt_b[0])
+        src_b[2].append(idx)
+        trgt_b[2].append(-idx)
+        blobs.append(rt_b)
+        old_to_new[b[0]] = idx
+
+            
+    for b in fg2.blobs | filter[lambda b: not isinstance(b[1], RelationType)]:
+        if b[0] not in old_to_new: retrieve_or_insert_blob(b)
+
+    # If there is no overlab with the main k_dict, we insert
+    # the old key with the new index
+    for k,v in fg2.key_dict.items():
+        if k not in k_dict: k_dict[k] = old_to_new[v]
+
+    return old_to_new[fr_idx]
+
 def fg_get_imp(fg, key):
     kdict = fg.key_dict
     if type(key) in {ZefRef, EZefRef} and origin_uid(key) in kdict: return FlatRef(fg, kdict[origin_uid(key)])
@@ -8595,7 +8857,7 @@ def transact_imp(data, g, **kwargs):
         commands = flatgraph_to_commands(data)
     elif type(data) in {list, tuple}:
         commands = construct_commands(data)
-    elif isinstance(data, Generator):
+    elif isinstance(data, (Generator, ZefGenerator)):
         commands = construct_commands(tuple(data))
     else:
         raise ValueError(f"Expected FlatGraph or [] or () for transact, but got {data} instead.")
@@ -8667,11 +8929,6 @@ def retrieve_or_insert_blob(new_b):
     return new_fg
 
 
-
-
-
-
-
 #-----------------------------Range----------------------------------------
 def range_imp(*args):
     """
@@ -8740,7 +8997,7 @@ def generator_wrapper2():
     def generator_wrapper1():
         yield from range(lo, hi)
     # don't expose the yield directly, keep this a function
-    return generator_wrapper2() if hi==infinity else generator_wrapper1()
+    return ZefGenerator(generator_wrapper2 if hi==infinity else generator_wrapper1)
 
 
 def range_tp(op, curr_type):
@@ -9290,16 +9547,20 @@ def split_on_next_imp(s, el_to_split_on):
         ind = s.find(el_to_split_on)
         if ind == -1: return s, ''    # not found
         return s[:ind], s[ind+len(el_to_split_on):]
-    if isinstance(s, list) or isinstance(s, tuple) or isinstance(s, Generator):
-        it = iter(s)
-        part1 = []
-        while True:
-            val = next(it)
-            if val == el_to_split_on: break
-            part1.append(val)
-        def wrapper():      # keep split_on_next a function
-            yield from it
-        return part1, wrapper()
+    if isinstance(s, list) or isinstance(s, tuple) or isinstance(s, Generator) or isinstance(s, ZefGenerator):
+        def outer_wrapper():
+            it = iter(s)
+            part1 = []
+            while True:
+                val = next(it)
+                if val == el_to_split_on: break
+                part1.append(val)
+            yield part1
+            def wrapper():      # keep split_on_next a function
+                yield from it
+            yield wrapper()
+
+        return ZefGenerator(outer_wrapper)
 
     raise TypeError(f"expected a String or a List in `split_on_next`, got a {type(s)}")
 
@@ -9719,5 +9980,71 @@ def step(d: dict) -> dict:
 
 
 
+# ----------------------------- alias -----------------------------
+def alias_imp(vt, name: str):
+    """ 
+    Give a ValueType an alias: a name to show in the
+    repr's output to shorten and make the output more readable.
+
+    ---- Examples ----
+    >>> StringOrFloat = (String | Float) | alias('StringOrFloat')
+
+    ---- Signature ----
+    (ValueType, String)    -> ValueType
+
+    ---- Tags ----
+    - operates on: ValueType
+    - used for: readability
+    """
+    vt2 = ValueType_(type_name=vt.d['type_name'], absorbed=vt.d['absorbed'])
+    vt2.d['alias'] = name
+    return vt2
+
+
+
+
+
+# ----------------------------- splice -----------------------------
+def splice_imp(x, start_pos, els_to_replace, replacement):
+    """
+    "splice in" or insert a replacement sequence or string into
+    a sequence at a given position.
+
+    ---- Examples ----
+    >>> 'good morning world' | splice[5][7]['afternoon']         # 'good afternoon world'
+    >>> Range(0, 5) | splice[2][1][('a','b', 'c')]               # (0, 1, 'a', 'b', 'c', 3, 4)
+
+    ---- Signature ----
+    (Graph, str, bool) -> Effect
+    (ZefRef, str, bool) -> LazyValue
+
+    ---- Tags ----
+    - operates on: String
+    - operates on: List
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: replace_at
+    - related zefop: replace
+    """
+    if isinstance(x, str):
+        return x[:start_pos] + replacement + x[start_pos + els_to_replace:]
+    elif isinstance(x, list) or isinstance(x, tuple):
+        return (*x[:start_pos], *replacement, *x[start_pos + els_to_replace:])
+    
+    elif isinstance(x, ZefGenerator):        
+        # handle both the dataflow arg and the replacement as generators
+        def wrapper():
+            it1 = iter(x)
+            it2 = iter(replacement)
+            for _ in range(start_pos):
+                yield next(it1)
+            yield from it2
+            for _ in range(els_to_replace):
+                next(it1)     # throw these away
+            yield from it1
+        return ZefGenerator(wrapper)
+
+    else:
+        return Error(f'Unsupported type passed to "splice": {type(x)}')
 
 
diff --git a/python/zef/core/op_implementations/yo.py b/python/zef/core/op_implementations/yo.py
index 70e97df8..378bb7be 100644
--- a/python/zef/core/op_implementations/yo.py
+++ b/python/zef/core/op_implementations/yo.py
@@ -380,6 +380,10 @@ def graph_info(g) -> str:
         | filter[lambda x: x[0] in {BT.TX_EVENT_NODE, BT.ENTITY_NODE, BT.ATOMIC_ENTITY_NODE, BT.RELATION_EDGE}] 
         | collect
     )
+    grouped_by_bts[BT.TX_EVENT_NODE] = grouped_by_bts.get(BT.TX_EVENT_NODE, [])
+    grouped_by_bts[BT.ENTITY_NODE] = grouped_by_bts.get(BT.ENTITY_NODE, [])
+    grouped_by_bts[BT.ATOMIC_ENTITY_NODE] = grouped_by_bts.get(BT.ATOMIC_ENTITY_NODE, [])
+    grouped_by_bts[BT.RELATION_EDGE] = grouped_by_bts.get(BT.RELATION_EDGE, [])
 
     def simple_lengths(g) -> str:
         return (f"{g.graph_data.write_head} blobs, "
diff --git a/python/zef/core/op_structs.py b/python/zef/core/op_structs.py
index 20c7f042..8b291eef 100644
--- a/python/zef/core/op_structs.py
+++ b/python/zef/core/op_structs.py
@@ -916,6 +916,7 @@ def __bool__(self):
 
     def evaluate(self, unpack_generator = True):
         from .op_implementations.dispatch_dictionary import _op_to_functions
+        from .op_implementations.implementation_typing_functions import ZefGenerator
         curr_value = self.initial_val
         
         # TODO: Type info has been disabled due to typespecing of long lists/sets/dicts which consumes
@@ -956,7 +957,7 @@ def evaluate(self, unpack_generator = True):
             #     back_up_type_info.append(curr_type)
         # self.type_info = type_info if primary_type_info else back_up_type_info
         
-        if unpack_generator and (isinstance(curr_value, Iterator) or isinstance(curr_value, Generator)):
+        if unpack_generator and (isinstance(curr_value, Iterator) or isinstance(curr_value, Generator) or isinstance(curr_value, ZefGenerator)):
             return [i for i in curr_value]
         return curr_value      
 
diff --git a/python/zef/core/serialization.py b/python/zef/core/serialization.py
index fc082789..a9dd3ebb 100644
--- a/python/zef/core/serialization.py
+++ b/python/zef/core/serialization.py
@@ -28,6 +28,7 @@
 from .flat_graph import FlatGraph_, FlatRef, FlatRefs
 from ..pyzef import internals as pyinternals
 from .VT import ValueType_
+from .symbolic_expression import SymbolicExpression_
 
 ##############################
 # * Definition
@@ -286,6 +287,14 @@ def serialize_valuetype(vt):
     }
 
 
+def serialize_symbolicexpression(se):
+    return {
+        "_zeftype": "SymbolicExpression",
+        "name": se.name,
+        "root_node": serialize_internal(se.root_node),
+        "absorbed": serialize_internal(se.absorbed)
+    }
+
 
 def deserialize_tuple(json_d: dict) -> tuple:
     return tuple(deserialize_internal(el) for el in json_d["items"])
@@ -453,6 +462,13 @@ def deserialize_valuetype(d):
             return ValueType_(type_name=d["type_name"], absorbed=absorbed)
     raise Exception(f"Couldn't find a ValueType of type '{d['type_name']}'")
 
+def deserialize_symbolicexpression(d):
+    return SymbolicExpression_(
+        name = d["name"],
+        root_node= deserialize_internal(d["root_node"]),
+        absorbed = deserialize_internal(d["absorbed"]),
+    )
+
 serialization_mapping[ZefRef] = serialize_zeftypes
 # serialization_mapping[ZefRefs] = serialize_zeftypes
 serialization_mapping[EZefRef] = serialize_zeftypes
@@ -488,6 +504,7 @@ def deserialize_valuetype(d):
 serialization_mapping[pyinternals.DelegateRoot] = serialize_delegate
 serialization_mapping[pyinternals.DelegateRelationTriple] = serialize_delegate
 serialization_mapping[ValueType_] = serialize_valuetype
+serialization_mapping[SymbolicExpression_] = serialize_symbolicexpression
 
 
 deserialization_mapping["dict"] = deserialize_dict
@@ -526,3 +543,4 @@ def deserialize_valuetype(d):
 deserialization_mapping["pyinternals.DelegateRoot"] = deserialize_delegate
 deserialization_mapping["pyinternals.DelegateRelationTriple"] = deserialize_delegate
 deserialization_mapping["ValueType"] = deserialize_valuetype
+deserialization_mapping["SymbolicExpression"] = deserialize_symbolicexpression
diff --git a/python/zef/core/symbolic_expression.py b/python/zef/core/symbolic_expression.py
new file mode 100644
index 00000000..1abbab48
--- /dev/null
+++ b/python/zef/core/symbolic_expression.py
@@ -0,0 +1,343 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+SymbolicVariables are a special kind of SymbolicExpression. They are a primitive building block.
+Which variable they are is simply stored as a string, which makes them values without an identity
+other than this string.
+
+There are also other kinds of SymbolicExpressions: they can be composed to form composite expressions.
+Internally, the structure of the expression is stored as a FlatGraph (this is a more suitable
+data structure than a tree).
+
+Generally, SymbolicExpressions follow strict value semantics though. If a new expression is expressed
+in terms of another SymbolicExpression, the original expression is never mutated.
+
+It is generally up to the outer scope to decide what to do with a SymbolicExpression. They are just
+a value, potentially a complicated one.
+
+They are useful in Queries, expressing various interconnected constraints, truth maintenance systems,
+logic programs that are built up incrementally.
+
+A very similar structure is used for ReactiveVariables. These are a different type and have a different
+use case though.
+
+
+
+
+----------------------------- Basic Usage -----------------------------------
+>>> x = SV('x')
+>>> y = x*x + 42     # y is a composite SymbolicExpression
+
+Many operators are supported
+
+
+----------------------------- Use Cases -----------------------------------
+# z is a SE: serve as the body of a Zef Lambda expression.
+Lambda[z*z + 42]
+
+p, a = SVs('a, p')
+Query[
+    {
+        p: ET.Person,
+        a: ET.Dog | ET.Cat,
+    },
+    RP(p, RT.Owns, a),
+]
+
+
+Query[
+    z,
+    z[1]*z[1] + z[2]*z[2] == z[3]*z[3]
+]
+
+or
+Query[
+    (x1, x2, x3),               # do we want this? Not entirely consistent with the Dict notation.
+    x1*x1 + x2*x2 == x3*x3,
+]
+
+
+----------------------------- Sample Code -----------------------------------
+>>> from zef.core.symbolic_expression import SymbolicExpression_, SV, unwrap_vars
+>>> 
+>>> x1 = SV('x1')
+>>> x2 = SV('x2')
+>>> expr = (x1 + 5)**x2
+>>> expr.root_node.fg | unwrap_vars | graphviz | collect   # temporary hack: shows graphviz representation of the expression
+
+----------------------------- Open Questions -----------------------------------
+- What should "List[x2]" do? Simply Absorb the SymbolicExpression or should this itself be a SymbolicExpression?
+- What is the grammar for SEs interacting with ReactiveVariables?
+- if we expose `z`, should z['a'] be identical to SV('a')?
+
+----------------------------- TODOs -----------------------------------
+- add SymbolicExpression to VTs and implement in is_a
+- serialization
+- overload some unary operators: e.g. __pos__, __neg__, ...?
+- 41 | add[1] | x | multiply[2]          # advanced use case: should this also be a SE? SEs can themselves be used to do logic with ZefOps.
+
+"""
+
+
+from .VT import FlatGraph, Pattern, Any, SetOf
+from ._ops import match, collect, insert, split_if, get, filter, map, Z
+from .zef_functions import func
+from .internals import ET, RT, AET
+from .flat_graph import Val
+
+def merge_flatgraphs(g1, g2) -> FlatGraph:
+    """
+    Given two graphs, make a new FlatGraph that contains both.
+
+    """
+    def shift(el, d):
+        """
+        depending on the element type in the FlatGraph blobs, 
+        we may have to perform different types of shifts.
+        """
+        return el | match[
+         (Pattern[Any, AET], lambda el: (el[0]+d, el[1], [n+d if n>=0 else n-d for n in el[2]], el[3], el[4])),
+         (Pattern[Any, RT], lambda el: (el[0]+d, el[1], [n+d if n>=0 else n-d for n in el[2]], el[3], el[4]+d, el[5]+d)),
+         (Any, lambda el: (el[0]+d, el[1], [n+d if n>=0 else n-d for n in el[2]], el[3],)),
+        ] | collect
+    
+
+    g = FlatGraph()
+    delta_n = len(g1.blobs)    # how much to shift all indexes of g2
+    g.blobs = (
+        *g1.blobs,
+        *(shift(el, delta_n) for el in g2.blobs),
+    )
+    g.key_dict = {**g1.key_dict, **{k: v+delta_n for k,v in g2.key_dict.items()}}
+    return g
+
+
+
+
+class SymbolicExpression_:
+    """Symbolic Variable class"""
+    def __init__(self, name=None, root_node=None, absorbed=None):
+        self.name = name
+        self.root_node = root_node
+        self.absorbed = absorbed
+
+    def __repr__(self):
+        if self.name is not None:
+            # if the name is specified: it is a SV only
+            return f"SV('{self.name}')" if self.absorbed==None else f"SV('{self.name}')['{self.absorbed}']"
+            # return f"{self.name}" if self.absorbed==None else f"{self.name}['{self.absorbed}']"
+            
+        else:
+            return "SymbolicExpression('todo: give more details')"
+
+    def __getitem__(self, k):
+        if self.root_node is not None:
+            raise RuntimeError("a composite SymbolicExpression cannot absorb a value")
+        return SymbolicExpression_(name=self.name, absorbed=k)
+
+    def __add__(self, other):
+        return compose_se(ET.Add, self, other)
+
+    def __radd__(self, other):
+        return compose_se(ET.Add, other, self)
+
+    def __sub__(self, other):
+        return compose_se(ET.Subtract, self, other)
+
+    def __rsub__(self, other):
+        return compose_se(ET.Subtract, other, self)
+
+    def __mul__(self, other):
+        return compose_se(ET.Multiply, self, other)
+
+    def __rmul__(self, other):
+        return compose_se(ET.Multiply, other, self)
+
+    def __truediv__(self, other):
+        return compose_se(ET.Divide, self, other)
+
+    def __rtruediv__(self, other):
+        return compose_se(ET.Divide, other, self)
+
+    def __eq__(self, other):
+        return compose_se(ET.Equals, self, other)
+
+    def __req__(self, other):
+        return compose_se(ET.Equals, other, self)
+
+    def __ne__(self, other):
+        return compose_se(ET.NotEquals, self, other)
+
+    def __rne__(self, other):
+        return compose_se(ET.NotEquals, other, self)
+
+    def __gt__(self, other):
+        return compose_se(ET.GreaterThan, self, other)
+
+    def __rgt__(self, other):
+        return compose_se(ET.GreaterThan, other, self)
+
+    def __ge__(self, other):
+        return compose_se(ET.GreaterOrEqualThan, self, other)
+
+    def __rge__(self, other):
+        return compose_se(ET.GreaterOrEqualThan, other, self)
+
+    def __lt__(self, other):
+        return compose_se(ET.LessThan, self, other)
+
+    def __rlt__(self, other):
+        return compose_se(ET.LessThan, other, self)
+
+    def __le__(self, other):
+        return compose_se(ET.LessOrEqualThan, self, other)
+
+    def __rle__(self, other):
+        return compose_se(ET.LessOrEqualThan, other, self)
+
+    def __mod__(self, other):
+        return compose_se(ET.Modulus, self, other)
+
+    def __rmod__(self, other):
+        return compose_se(ET.Modulus, other, self)
+
+    def __or__(self, other):
+        return compose_se(ET.Or, self, other)
+
+    def __ror__(self, other):
+        return compose_se(ET.Or, other, self)
+
+    def __and__(self, other):
+        return compose_se(ET.And, self, other)
+
+    def __rand__(self, other):
+        return compose_se(ET.And, other, self)
+
+    def __xor__(self, other):
+        return compose_se(ET.Xor, self, other)
+
+    def __rxor__(self, other):
+        return compose_se(ET.Xor, other, self)
+
+    def __pow__(self, other):
+        return compose_se(ET.Power, self, other)
+
+    def __rpow__(self, other):
+        return compose_se(ET.Power, other, self)
+
+
+
+def SV(name: str):
+    """acts like a constructor. SymbolicVariable is the `primitive`, 
+    atomic building block  of any symbolic expression. The latter
+    can be a larger composed form."""
+    if not isinstance(name, str):
+        raise TypeError("name must be a string")    
+    return SymbolicExpression_(name=name)
+
+
+def SVs(names: str):
+    """
+    initialize multiple SVs together:
+    >>> x1, x2, *_ = SVs('x1, x2, x3, x4')
+    Space or comma indicates a new variable.
+    """
+    if not isinstance(names, str):
+        raise TypeError("name must be a string")    
+
+    return (names 
+        | split_if[lambda c: c==' ' or c==','] 
+        | filter[~SetOf['',' ', ',']] 
+        | map[SV]
+        | collect
+        )
+
+
+
+def compose_se(op_type, arg1, arg2):
+    """
+    either arg1 or arg2 will be a SymbolicExpression.
+    Otherwise the magic method would not forward to this function.
+    """
+    fg = FlatGraph()
+
+    is_composite_se = lambda x: isinstance(x, SymbolicExpression_) and x.name == None
+    arg1_composite_se = is_composite_se(arg1)
+    arg2_composite_se = is_composite_se(arg2)
+
+    # neither side is a composite SE: start with fresh graph
+    if (not arg1_composite_se) and (not arg2_composite_se):
+        res = (fg 
+            | insert[op_type['root'], RT.Arg1, Val(arg1)]
+            | insert[Z['root'], RT.Arg2, Val(arg2)]
+            | get['root']
+            | collect
+        )
+        return SymbolicExpression_(root_node=res)
+    
+    # both sides are composite SEs: merge
+    elif arg1_composite_se and arg2_composite_se:
+        g1 = arg1.root_node.fg
+        g2 = arg2.root_node.fg
+        g_merged = merge_flatgraphs(g1, g2)
+        # some manual hacking here. g2's blob list is appended to g1's blob list.
+        g_merged.key_dict['_arg1'] = arg1.root_node.fg.key_dict['root']
+        g_merged.key_dict['_arg2'] = arg2.root_node.fg.key_dict['root'] + len(arg1.root_node.fg.blobs)
+        res = (g_merged
+            | insert[op_type['root'], RT.Arg1, Z['_arg1']]
+            | insert[Z['root'], RT.Arg2, Z['_arg2']]
+            | get['root']
+            | collect
+        )
+        return SymbolicExpression_(root_node=res)
+        
+
+    elif arg1_composite_se:
+        res = (arg1.root_node.fg
+            | insert[op_type['root'], RT.Arg1, arg1.root_node]
+            | insert[Z['root'], RT.Arg2, Val(arg2)]
+            | get['root']
+            | collect
+        )
+        return SymbolicExpression_(root_node=res)
+        
+    elif arg2_composite_se:
+        res = (arg2.root_node.fg
+            | insert[op_type['root'], RT.Arg2, arg2.root_node]
+            | insert[Z['root'], RT.Arg1, Val(arg1)]
+            | get['root']
+            | collect
+        )
+        return SymbolicExpression_(root_node=res)
+        
+    raise RuntimeError('We should not be here')
+
+
+
+@func
+def unwrap_vars_hack(fg):
+    """
+    unwrap variables from a flatgraph.
+    """
+    def unwrap(x):
+        try:
+            return (*x[:3], x[3].name)
+        except:
+            return x
+    fg2 = FlatGraph()
+    fg2.key_dict = fg.key_dict
+    fg2.blobs = tuple((unwrap(x) for x in fg.blobs))
+    return fg2
+
diff --git a/python/zef/core/zef_rich.py b/python/zef/core/zef_rich.py
deleted file mode 100644
index 34aae2e3..00000000
--- a/python/zef/core/zef_rich.py
+++ /dev/null
@@ -1,265 +0,0 @@
-# Copyright 2022 Synchronous Technologies Pte Ltd
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from ._core import *
-from ._ops import *
-from .VT import *
-from .zef_functions import func
-
-@func
-def is_a_component(component, vt):
-    return isinstance(component, ValueType_) and without_absorbed(component) == vt
-
-#--------------------------TEXT--------------------------------------
-def dispatch_rich_text(component):
-    import rich.text as rt
-
-    def resolve_styles(d):
-        # TODO filter out non-rich styles if found
-        from rich.style import Style
-        if "background_color" in d:
-            d['bgcolor'] = d['background_color']
-            d.pop("background_color")
-        return Style(**d)
-
-    def resolve_nonstyle_attributes(d):
-        new_d = {}
-        if "justify" in d: new_d['justify'] = d.pop("justify")
-        if "overflow" in d: new_d['overflow'] = d.pop("overflow")
-        if "no_wrap" in d: new_d['no_wrap'] = d.pop("no_wrap")
-        if "tab_size" in d: new_d['tab_size'] = d.pop("tab_size")
-
-        return new_d
-    
-    def resolve_text_and_nested_data(t):
-        pairs_or_text = []
-        internals = t | absorbed | collect
-        assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Text should be of type dict!"
-        text = internals[0].get("text", "")
-        attributes = {**internals[0]}
-        attributes.pop("text", None)
-        nonstyle_attributes = resolve_nonstyle_attributes(attributes)
-
-        if "data" in attributes:
-            data = attributes["data"]
-            def dispatch_on_data_type(data):
-                # Just a str
-                if isinstance(data, str):
-                    pairs_or_text.append((data, ""))
-
-                # Nested Text component
-                elif is_a_component(data, Text) : 
-                    pairs_or_text.extend(dispatch_rich_text(data))
-                
-                # List of the above 2
-                elif isinstance(data, list):
-                    [dispatch_on_data_type(x) for x in data]
-
-                else:
-                    raise ValueError("Text's data field could contain only a str, Text component, or a list composed of the two.")
-
-            dispatch_on_data_type(data)
-            attributes.pop("data")
-        
-        if "style" in attributes:
-            style = dispatch_rich_style(attributes["style"])
-        else:
-            style = resolve_styles(attributes)
-
-        return [(text, style), *pairs_or_text], nonstyle_attributes
-
-    pairs_or_text, nonstyle_attributes = resolve_text_and_nested_data(component)
-    return rt.Text.assemble(*pairs_or_text, **nonstyle_attributes)
-
-
-
-#--------------------------Code--------------------------------------
-def dispatch_rich_syntax(component):
-    import rich.syntax as rs
-
-    def filter_attributes(d):
-        # TODO filter out non-rich styles if found
-        lexer = d.get("language", "python3")
-        d.pop("language", None)
-        d.pop("code", None)
-        return {**d, "lexer": lexer}
-    
-    internals = component | absorbed | collect
-    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Code should be of type dict!"
-    code = internals[0].get("code", "")
-    attributes = {**internals[0]}
-    attributes = filter_attributes(attributes)
-    return rs.Syntax(code, **attributes)
-
-
-#--------------------------Style--------------------------------------
-def dispatch_rich_style(component):
-    def resolve_styles(new_d):
-        from rich.style import Style
-        if "background_color" in new_d:
-            new_d['bgcolor'] = new_d['background_color']
-            new_d.pop("background_color")
-        return Style(**new_d)
-    
-    internals = component | absorbed | collect
-    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Style should be of type dict!"
-    attributes = {**internals[0]}
-    return resolve_styles(attributes)
-
-
-
-#--------------------------Table--------------------------------------
-def dispatch_rich_table(component):
-    import rich.table as rt
-    import itertools as itr
-
-    def handle_nested_components(maybe_component):
-        if isinstance(maybe_component, str):
-            return maybe_component
-        elif is_a_component(maybe_component, Text):
-            return dispatch_rich_text(maybe_component)
-        elif is_a_component(maybe_component, Column):
-            return dispatch_rich_column(maybe_component)
-        elif is_a_component(maybe_component, Style):
-            return dispatch_rich_style(maybe_component)
-        elif isinstance(maybe_component, tuple):
-            return tuple([handle_nested_components(el) for el in maybe_component])
-        else:
-            raise NotImplementedError(f"{maybe_component}:{type(maybe_component)}")
-
-    def resolve_attributes(d):
-        # row_styles: List of styles or strings
-        if "row_styles" in d:
-            d["row_styles"] = [handle_nested_components(x) for x in d["row_styles"]]
-        
-        # title: String or Text
-        if "title" in d:
-            d["title"] = handle_nested_components(d["title"])
-
-        # cols: string or text
-        if "cols" in d:
-            d["cols"] = [handle_nested_components(x) for x in d["cols"]]
-        
-        # rows: string or text
-        if "rows" in d:
-            d["rows"] = [handle_nested_components(x) for x in d["rows"]]
-
-        if "box" in d:
-            d["box"] = box_constants_mapping(d["box"])
-
-        return d
-    
-    internals = component | absorbed | collect
-    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Table should be of type dict!"
-    attributes = resolve_attributes({**internals[0]})
-    cols = attributes.pop('cols', [])
-    rows = attributes.pop('rows', [])
-    rich_table = rt.Table(*cols, **attributes)
-    # [rich_table.add_column(col, style = style) for (col, style) in itr.zip_longest(cols, col_styles)]
-    [rich_table.add_row(*row) for row in rows]
-    return rich_table
-
-#--------------------------Column--------------------------------------
-def dispatch_rich_column(component):
-    import rich.table as rt
-
-    def resolve_attributes(d):
-        allowed_keys = ["header_style", "footer_style", "style", "justify", "vertical", "width", "min_width", "max_width", "ratio", "no_wrap"]
-        attributes = select_keys(d, *allowed_keys)
-        # Resolve the non-string styles if found
-        for special_key in ["header_style", "footer_style", "style"]:
-            if special_key in attributes and is_a_component(attributes[special_key], Style):
-                attributes[special_key] = dispatch_rich_style(attributes[special_key])
-        
-        return attributes
-    
-    internals = component | absorbed | collect
-    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Column should be of type dict!"
-    
-    header = internals[0].get("text", "")
-    if is_a_component(header, Text): header = dispatch_rich_text(header)
-    attributes = resolve_attributes(internals[0])
-
-    return rt.Column(header, **attributes)
-
-
-#--------------------------Frame--------------------------------------
-def dispatch_rich_panel(component):
-    import rich.panel as rp
-
-    def filter_and_resolve_attributes(d):
-        d.pop("displayable", None)
-
-        if "title" in d and is_a_component(d["title"], Text): 
-            d["title"] = dispatch_rich_text(d["title"])
-        
-        if "subtitle" in d and is_a_component(d["title"], Text): 
-            d["subtitle"] = dispatch_rich_text(d["subtitle"])
-
-        if "box" in d:
-            d["box"] = box_constants_mapping(d["box"])
-
-        return d
-    
-    internals = component | absorbed | collect
-    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Code should be of type dict!"
-    displayable = internals[0].get("displayable")
-    displayable = match_and_dispatch(displayable)
-    if not displayable: raise ValueError("Frame's displayable field wasn't passed a component!")
-    
-    attributes = {**internals[0]}
-    attributes.pop("displayable", None)
-    attributes = filter_and_resolve_attributes(attributes)
-    return rp.Panel(displayable, **attributes)
-
-
-#-------------------Dispatch-------------------------------------
-def box_constants_mapping(box_style: str):
-    from rich import box
-    str_to_constant = {
-        'ascii':                   box.ASCII,
-        'square':                  box.SQUARE,                
-        'minimal':                 box.MINIMAL,  
-        'minimal_heavy_head':      box.MINIMAL_HEAVY_HEAD,     
-        'minimal_double_head':     box.MINIMAL_DOUBLE_HEAD,           
-        'simple':                  box.SIMPLE,    
-        'heavy':                   box.HEAVY,                 
-        'heavy_edge':              box.HEAVY_EDGE,             
-        'heavy_head':              box.HEAVY_HEAD,
-        'double':                  box.DOUBLE,               
-        'double_edge':             box.DOUBLE_EDGE,
-        'simple_heavy':            box.SIMPLE_HEAVY,            
-        'horizontals':             box.HORIZONTALS,              
-        'rounded':                 box.ROUNDED,
-    }
-    return str_to_constant.get(box_style, box.ROUNDED)
-
-def match_and_dispatch(component):
-    return component | match[
-        (Is[is_a_component[Text]], dispatch_rich_text),
-        (Is[is_a_component[Code]], dispatch_rich_syntax),
-        (Is[is_a_component[Style]], dispatch_rich_style),
-        (Is[is_a_component[Table]], dispatch_rich_table),
-        (Is[is_a_component[Column]], dispatch_rich_column),
-        (Is[is_a_component[Frame]], dispatch_rich_panel),
-        (Any, lambda x: print(f"Show not defined for {x}"))
-    ] | collect
-
-def print_rich(displayable):
-    import rich
-    console = rich.console.Console()
-    displayable = match_and_dispatch(displayable)
-    console.print(displayable)
-
-show = run[print_rich]
\ No newline at end of file
diff --git a/python/zef/gql/__init__.py b/python/zef/gql/__init__.py
index 84b3a2c4..f9951f52 100644
--- a/python/zef/gql/__init__.py
+++ b/python/zef/gql/__init__.py
@@ -16,6 +16,8 @@
 from ..ops import *
 from .auto import auto_generate_gql
 from ..core.logger import log
+from .generate_gql_api import generate_api_schema_file, generate_graph_from_file, schema_str_to_flatgraph
+from .resolvers_utils import find_field_with_name, connect_zef_function_resolvers, connect_delegate_resolvers, create_resolver_to_field
 
 def schema_node_checks(schema_node: ZefRef):
     if type(schema_node) != ZefRef:
diff --git a/python/zef/gql/ariadne_utils.py b/python/zef/gql/ariadne_utils.py
index 17c8728e..d2d23d5a 100644
--- a/python/zef/gql/ariadne_utils.py
+++ b/python/zef/gql/ariadne_utils.py
@@ -100,14 +100,14 @@ def resolve_with(rt, bt, ft):
         if is_list:
             return f'return (z | {dir}[RT.{d_rt_name}]) | map[value] | collect'
         else:
-            return f'return (z | {dir}[RT.{d_rt_name}]) | optional | maybe_value | collect'
+            return f'return (z | {dir}[RT.{d_rt_name}]) | single_or[None] | maybe_value | collect'
     else:
         if is_list:
             # return f'return (z >> L[RT.{d_rt_name}] | collect) , (z > L[RT.{d_rt_name}])| map[uid] | collect'
             return f'return (z | {dir}[RT.{d_rt_name}] | collect)'
         else:
             # return f'return (z >> RT.{d_rt_name} | collect) , (z > RT.{d_rt_name})|uid | collect'
-            return f'return (z | {dir}[RT.{d_rt_name}] | optional | collect)'
+            return f'return (z | {dir}[RT.{d_rt_name}] | single_or[None] | collect)'
 
 
 def resolve_with_script(rt):
diff --git a/python/zef/gql/generate_gql_api.py b/python/zef/gql/generate_gql_api.py
index 1410dba3..a16247d8 100644
--- a/python/zef/gql/generate_gql_api.py
+++ b/python/zef/gql/generate_gql_api.py
@@ -64,18 +64,45 @@ def generate_resolvers_fcts(schema_root, resolvers_destination):
         print(f'An exception was raised when processing file {exc}')
 
 
-def generate_api_schema_file(schema_root, destination):
+@func
+def generate_api_schema_file(schema_root, file_path):
     """
-    Creates a variable that holds the schema file which is represented by the passed schema graph.
+    Given a ZefRef to a schema_root, a file, defined in the file_path,
+    containing a 'generated_schema' string variable is generated.
+
+    If the requirement is to just produce a string holding the schema, this effect could be used:
+
+    Effect({
+        type: FX.GraphQL.GenerateSchemaString,
+        schema_root: schema_root,
+    })
     """
-    file_generator.generate_api_schema_file(schema_root, destination)
+    file_generator.generate_api_schema_file(schema_root, file_path)
 
 
-def generate_graph_from_file(schema_root, g):
+@func
+def generate_graph_from_file(schema_str, g):
+    """
+    The schema_str is parsed and then all required RAEs are instantiated on the passed graph.
     """
-    Creates a graph from a given schema.
+    return graph_generator.parse_schema(g, schema_str)
+
+
+@func
+def schema_str_to_flatgraph(schema_str):
     """
-    return graph_generator.parse_schema(g, schema_root)
+    Generate a Flatgraph containing all the RAEs defining the schema_str.
+    """
+    from zef.core.VT import FlatGraph
+    from zef import  BT
+    g = Graph()
+    generate_graph_from_file(schema_str, g)
+    all_raes = g | now | all | map[lambda zr: origin_rae(zr) if is_a(zr, BT.RELATION_EDGE) else zr] | collect
+    return FlatGraph(all_raes)
+
+
+    # return graph_generator.parse_schema(g, schema_str)
+
 
 
 def make_api(schema_root, schema_destination=None, resolvers_destination=None):
diff --git a/python/zef/gql/simplegql/__main__.py b/python/zef/gql/simplegql/__main__.py
index 10c66e00..5cbde5c7 100644
--- a/python/zef/gql/simplegql/__main__.py
+++ b/python/zef/gql/simplegql/__main__.py
@@ -37,6 +37,8 @@
                     help="If present, a python file containing the hooks to make available for the schema file")
 parser.add_argument("--init-hook", type=str, dest="init_hook", default=os.environ.get("SIMPLEGQL_INIT_HOOK", None),
                     help="If present, a function in the hooks file to run on initialisation of the data graph")
+parser.add_argument("--debug-level", type=int, dest="debug_level", default=os.environ.get("SIMPLEGQL_DEBUG_LEVEL", "0"),
+                    help="The amount of debug messages to output")
 args = parser.parse_args()
 
 
@@ -88,7 +90,10 @@
     log.info(f"Ran init hook: {args.init_hook}")
     
 
-server_uuid = start_server(root, g_data, args.port, args.bind)
+server_uuid = start_server(root, g_data,
+                           port=args.port,
+                           bind_address=args.bind,
+                           debug_level=args.debug_level)
 
 import time
 try:
diff --git a/python/zef/gql/simplegql/generate_api.py b/python/zef/gql/simplegql/generate_api.py
index ac297b49..6af0f1a5 100644
--- a/python/zef/gql/simplegql/generate_api.py
+++ b/python/zef/gql/simplegql/generate_api.py
@@ -17,6 +17,7 @@
 from ...ops import *
 from functools import partial as P
 from ...core.logger import log
+import functools
 
 from ariadne import ObjectType, QueryType, MutationType, EnumType, ScalarType
 
@@ -461,9 +462,7 @@ def resolve_get(_, info, *, type_node, **params):
     return find_existing_entity_by_id(info, type_node, params["id"])
 
 def resolve_query(_, info, *, type_node, **params):
-    g = info.context["g"]
-    ents = g | now | all[ET(type_node | Out[RT.GQL_Delegate] | collect)]
-    ents = ents | filter[pass_query_auth[type_node][info]]
+    ents = obtain_initial_list(type_node, params.get("filter", None), info)
 
     ents = handle_list_params(ents, type_node, params, info)
 
@@ -608,7 +607,7 @@ def resolve_upfetch(_, info, *, type_node, **params):
 
                 obj = find_existing_entity_by_field(info, type_node, upfetch_field, item[field_name])
                 if obj is None:
-                    obj_name,more_actions,more_post_checks  = add_new_entity(info, type_node, item, name_gen)
+                    obj_name,more_actions,more_post_checks = add_new_entity(info, type_node, item, name_gen)
                     actions += more_actions
                     post_checks += more_post_checks
                     new_obj_names += [obj_name]
@@ -668,7 +667,6 @@ def resolve_update(_, info, *, type_node, **params):
 def resolve_delete(_, info, *, type_node, **params):
     try:
         with mutation_lock:
-            g = info.context["g"]
             # Do the same thing as a resolve_query but delete the entities instead.
             if "filter" not in params:
                 raise ExtenalError("Not allowed to delete everything!")
@@ -706,8 +704,43 @@ def resolve_filter_response(obj, info, *, type_node, **params):
 # * Internal query parts
 #--------------------------------------------
 
+def obtain_initial_list(type_node, filter_opts, info):
+    gs = info.context["gs"]
+
+    type_et = ET(type_node | Out[RT.GQL_Delegate] | collect)
+    if filter_opts is not None and filter_opts.get("id", None) is not None:
+        # ids were provided, so the initial list starts with them
+
+        # Note: we make the decision here to not throw on a missing id, as that
+        # could potentially be exploited somehow by smoeone to learn about what
+        # items exist even if they don't have auth access. A missing id is hence
+        # just "not passing" the filter.
+
+        log.debug("In the explicit ID path")
+
+        ids = filter_opts["id"]
+        zs = []
+
+        zs = (ids
+              | map[lambda id: find_existing_entity_by_id(info, type_node, id)]
+              | filter[Not[equals[None]]])
+
+        if info.context["debug_level"] >= 3:
+            log.debug("DEBUG 3: built initial list from ids", length_ids=len(ids), length_list=length(zs))
+
+        return zs
+    else:
+        zs = gs | all[type_et] | filter[pass_query_auth[type_node][info]]
+        if info.context["debug_level"] >= 3:
+            log.debug("DEBUG 3: built initial list from type and auth", length_list=length(zs))
+        return zs
+
+    
+
 def handle_list_params(opts, z_node, params, info):
     opts = maybe_filter_result(opts, z_node, info, params.get("filter", None))
+    if info.context["debug_level"] >= 3:
+        log.debug("DEBUG 3: after filtering", length_list=length(opts))
     opts = maybe_sort_result(opts, z_node, info, params.get("order", None))
     opts = maybe_paginate_result(opts, params.get("first", None), params.get("offset", None))
     return opts
@@ -728,7 +761,15 @@ def maybe_filter_result(opts, z_node, info, fil=None):
         return opts
 
     temp_fil = build_filter_zefop(fil, z_node, info)
-    return opts | filter[build_filter_zefop(fil, z_node, info)]
+    if info.context["debug_level"] >= 4:
+        log.debug("DEBUG 4: filter is", fil=temp_fil)
+        temp_fil = (apply[identity, temp_fil]
+                    | tap[match[
+                        (Is[second], lambda x: log.debug("DEBUG 4: filter passed", item=first(x))),
+                        (Any, lambda x: log.debug("DEBUG 4: filter failed", item=first(x)))
+                        ]]
+                    | second)
+    return opts | filter[temp_fil]
 
 def build_filter_zefop(fil, z_node, info):
     field_resolver = field_resolver_by_name[z_node][info]
@@ -926,10 +967,10 @@ def find_existing_entity_by_id(info, type_node, id):
     if the_uid is None:
         raise Exception("An id of {id} cannot be converted to a uid.")
     
-    g = info.context["g"]
+    gs = info.context["gs"]
 
     et = ET(type_node | Out[RT.GQL_Delegate] | collect)
-    ent = g[uid(id)] | now | collect
+    ent = gs[uid(id)] | collect
     if not is_a(ent, et):
         return None
     if not ent | pass_query_auth[type_node][info] | collect:
@@ -941,7 +982,7 @@ def find_existing_entity_by_field(info, type_node, z_field, val):
     if val is None:
         raise Exception("Can't find an entity by a None field value")
 
-    g = info.context["g"]
+    gs = info.context["gs"]
     et = ET(type_node | Out[RT.GQL_Delegate] | collect)
 
     # Note: we filter out with query auth even though this may mean we return
@@ -950,7 +991,7 @@ def find_existing_entity_by_field(info, type_node, z_field, val):
     # because the field is also @unique then this will fail. Better to keep the
     # failure point at the same location so we can determine what kind of error
     # to return and whether this will leak sensitive information.
-    ent = (g | now | all[et] | filter[pass_query_auth[type_node][info]]
+    ent = (gs | all[et] | filter[pass_query_auth[type_node][info]]
            | filter[internal_resolve_field[info][z_field] | optional | equals[val]]
            | optional
            | collect)
@@ -992,7 +1033,7 @@ def add_new_entity(info, type_node, params, name_gen):
 
         if z_field | target | op_is_scalar | collect:
             if op_is_unique(z_field):
-                others = info.context["g"] | now | all[et] | filter[fvalue[rt][None] | equals[val]] | func[set] | collect
+                others = info.context["gs"] | all[et] | filter[fvalue[rt][None] | equals[val]] | func[set] | collect
                 if len(others) > 0:
                     log.error("Trying to add a new entity with unique field that conflicts with others", et=et, field=field_name, others=others)
                     raise ExternalError(f"Unique field '{field_name}' conflicts with existing items.")
@@ -1213,7 +1254,7 @@ def auth_helper_auth_field(field_name, auth, *, z, type_node, info):
 
 
 def commit_with_post_checks(actions, post_checks, info):
-    g = info.context["g"]
+    g = Graph(info.context["gs"])
     with Transaction(g):
         try:
             r = actions | transact[g] | run
diff --git a/python/zef/gql/simplegql/main.py b/python/zef/gql/simplegql/main.py
index 2dfac222..3f6ab837 100644
--- a/python/zef/gql/simplegql/main.py
+++ b/python/zef/gql/simplegql/main.py
@@ -25,7 +25,17 @@
 from .schema_file_parser import parse_partial_graphql, json_to_minimal_nodes
 
 def create_schema_graph(schema_string, hooks_string=None):
+    """Create a Zef representation of the GraphQL schema given in `schema_string`
+along with the zef functions given in hooks_string on a new blank graph `g`.
+
+    The graph `g` is marked keep alive. If the server is manually restarted in
+    the same python session multiple times, then this should be removed by the
+    calling function.
+    """
+
     g_schema = Graph()
+    set_keep_alive(g_schema, True)
+
     if hooks_string is not None:
         # Prep the schema graph with the hooks, so that the schema nodes can point directly at these
         prepare_hooks(g_schema, hooks_string)
diff --git a/python/zef/gql/simplegql/server.py b/python/zef/gql/simplegql/server.py
index ec01a3a0..111de61a 100644
--- a/python/zef/gql/simplegql/server.py
+++ b/python/zef/gql/simplegql/server.py
@@ -100,37 +100,52 @@ def query(request, context):
 
     q = json.loads(request["request_body"])
 
+    if context["debug_level"] >= 3:
+        log.debug("DEBUG 3: incoming query", q=q)
+
+    # We pass in the graph as a fixed slice, so that the queries can be done
+    # consistently.
     success,data = graphql_sync(
         context["ari_schema"],
         q,
-        context_value={"g": context["g_data"],
-                       "auth": auth_context},
+        context_value={"gs": now(context["g_data"]),
+                       "auth": auth_context,
+                       "debug_level": context["debug_level"]},
     )
     if not success or "errors" in data:
         log.error("Failure in GQL query.", data=data, q=q, auth_context=auth_context)
+
+    response = json.dumps(data)
+    if context["debug_level"] >= 3:
+        log.debug("DEBUG 3: response", response=response)
+
     return {
-        "response_body": json.dumps(data),
+        "response_body": response,
         **request
     }
 
 def start_server(z_gql_root,
                  g_data,
+                 *,
                  port=443,
                  bind_address="0.0.0.0",
                  logging=True,
+                 debug_level=0,
                  ):
 
     schema,objects = generate_resolvers_fcts(z_gql_root)
     ari_schema = make_executable_schema(schema, objects)
 
     from logging import getLogger
-    logger = getLogger("ariadne").disabled = True
-    log.debug("Disabled ariadne logging")
+    if debug_level < 4:
+        logger = getLogger("ariadne").disabled = True
+        log.debug("Disabled ariadne logging")
     
     context = {
         "z_gql_root": z_gql_root,
         "g_data": g_data,
         "ari_schema": ari_schema,
+        "debug_level": debug_level,
     }
 
     if z_gql_root | has_out[RT.AuthJWKURL] | collect:
@@ -144,8 +159,9 @@ def start_server(z_gql_root,
         'type': FX.HTTP.StartServer,
         'port': port,
         'pipe_into': (map[middleware_worker[permit_cors,
-                                           route["/gql"][P(query, context=context)],
-                                           fallback_not_found,
+                                            route["/"][insert_in[["response_body"]]["Healthy"]],
+                                            route["/gql"][P(query, context=context)],
+                                            fallback_not_found,
                                             send_response]]
                       | subscribe[run]),
         'logging': logging,
diff --git a/python/zef/gql/value_gql/generator_utils.py b/python/zef/gql/value_gql/generator_utils.py
new file mode 100644
index 00000000..49d2f2b9
--- /dev/null
+++ b/python/zef/gql/value_gql/generator_utils.py
@@ -0,0 +1,179 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ...core import *
+from ...ops import *
+from ariadne import ObjectType,MutationType,QueryType,InterfaceType,SubscriptionType
+
+
+#--------------------------Schema Generator-------------------------
+def generate_schema_str(schema_dict: dict) -> str:
+    def parse_scalars(scalars_list):
+        return (
+            scalars_list
+            | map[lambda x: f"scalar {x}"]
+            | join["\n"]
+            | collect
+        )
+
+    @func
+    def parse_interfaces_or_types(lst, prefix):
+        def parse_implements(interfaces):
+            if interfaces: return f"implements {''.join(interfaces)} "
+            return ""
+
+        def parse_interface_or_type(interface_or_type):
+            name, fields_d = list(interface_or_type.items())[0]
+            return (
+                f"{prefix} {name} {parse_implements(fields_d.get('_interfaces', []))}" + "{\n"
+                + parse_fields(fields_d)
+                + "\n}"
+            )
+        return (
+            lst
+            | map[parse_interface_or_type]
+            | join["\n\n"]
+            | collect
+        )
+    
+    
+    
+    def parse_args(args):
+        def parse_arg(arg):
+            def parse_default_arg():
+                if "default" in arg_dict: return f" = {arg_dict['default']}"
+                return ""
+            arg_name, arg_dict = list(arg.items())[0]
+            return f"{arg_name}: {arg_dict['type']}{parse_default_arg()}"
+
+        if args:
+            args = (args
+            | map[parse_arg]
+            | join[", "]
+            | collect
+            )
+            return f"({args})"
+        
+        return ""
+
+
+    def parse_fields(fields_d):
+        def parse_field(tup):
+            field_name, field_dict = tup
+            if field_name.startswith("_"): return ""
+            return f"  {field_name}{parse_args(field_dict.get('args', []))}: {field_dict['type']}" 
+
+        return (
+            fields_d
+            | items
+            | map[parse_field]
+            | join["\n"]
+            | collect
+        )
+
+
+    allowed_keys = ["_Interfaces", "_Subscriptions", "_Types", "_Scalars"]
+    schema_dict = select_keys(schema_dict, *allowed_keys)
+    dispatch = {
+        "_Interfaces":  parse_interfaces_or_types["interface"],
+        "_Types":       parse_interfaces_or_types["type"],
+        "_Scalars":     parse_scalars,
+    }
+
+    return (
+        schema_dict
+        | items
+        | map[lambda x: dispatch[x[0]](x[1])]
+        | join["\n"]
+        | collect
+    )
+
+
+
+
+
+#--------------------------Resolvers Generator-------------------------
+def initialize_object_type(object_type):
+    if "Mutation" == object_type:
+        return MutationType()
+    elif "Subscription" == object_type:
+        return SubscriptionType()
+    else:
+        return ObjectType(object_type)
+
+def resolve_args(args):
+    def handle_arg_dict(d):
+        arg, arg_d = list(d.items())[0]
+        d = {}
+        d[arg]  = arg_d.get('default', None)
+        return d
+
+    return (
+        args
+        | map[handle_arg_dict]
+        | merge
+        | collect
+    )
+
+def generate_fct(field_dict, g):
+    kwargs = resolve_args(field_dict.get('args', []))
+    resolver = field_dict["resolver"]
+
+    def resolve_field(obj, info, **kwargs):
+        args = [
+            obj,
+            g,
+        ]
+        kwargs_all = {
+            "context": info,
+            **kwargs,
+        }
+
+        if is_a(resolver, ZefOp):
+            if peel(resolver)[0][0] == RT.Function:
+                return resolver(*args, **kwargs_all)
+            else:
+                return resolver(obj)
+
+        return resolver(*args,**kwargs_all)
+
+    return resolve_field
+
+def assign_field_resolver(object_type, field_name, field_dict, g):
+    fct = generate_fct(field_dict, g)
+    object_type.field(field_name)(fct)
+
+def generate_resolvers(schema_dict, g):
+    skip_generation_list = schema_dict.get("skip_generation_list", [])
+    fallback_resolver = schema_dict.get("fallback_resolvers", [])
+
+    types = schema_dict.get("_Types", [])
+    interfaces = schema_dict.get("_Interfaces", [])
+    object_types = []
+
+    for t_dict in types:
+        for object_type, fields_dict in t_dict.items():
+        
+            # Don't generate resolvers for function in this list
+            if object_type in skip_generation_list: continue
+
+            object_type = initialize_object_type(object_type)
+            object_types.append(object_type)
+
+            for field_name, field_dict in fields_dict.items():
+                if field_name.startswith("_"): continue
+                assign_field_resolver(object_type, field_name, field_dict, g)
+
+
+    return object_types
\ No newline at end of file
diff --git a/python/zef/gql/value_gql/parser.py b/python/zef/gql/value_gql/parser.py
new file mode 100644
index 00000000..4097b231
--- /dev/null
+++ b/python/zef/gql/value_gql/parser.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2022 Synchronous Technologies Pte Ltd
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+# the Software, and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from ...core import *
+from ...ops import *
+from graphql import parse
+from graphql.language.ast import ScalarTypeDefinitionNode, ObjectTypeDefinitionNode, NamedTypeNode, ListTypeNode, NonNullTypeNode, InterfaceTypeDefinitionNode
+
+def schema_str_to_dict(schema_str):
+
+    def resolve_interfaces(interfaces):
+        if interfaces: return {'_interfaces': interfaces | map[lambda i: i.name.value] | collect}
+        return {}
+
+    def resolve_args(args):
+        def resolve_arg(arg):
+            def resolve_default_value(arg):
+                if arg.default_value:
+                    return {"default": arg.default_value.value}
+                return {}
+
+            return {
+                arg.name.value: {
+                    "type": resolve_type(arg.type),
+                    **resolve_default_value(arg),
+                }
+            }
+        
+        if args: return {'args': args | map[resolve_arg] | collect}
+        return {}
+
+    def resolve_type(type):
+        return LazyValue(type) | match[
+            (Is[lambda t: isinstance(t, NamedTypeNode)], lambda t: t.name.value),
+            (Is[lambda t: isinstance(t, ListTypeNode)], lambda t: f"[{resolve_type(t.type)}]"),
+            (Is[lambda t: isinstance(t, NonNullTypeNode)], lambda t: f"{resolve_type(t.type)}!"),
+        ] | collect
+
+    def resolve_field(field):
+        return {
+            field.name.value: {
+                'type': resolve_type(field.type),
+                'resolver': None,
+                **resolve_args(list(field.arguments)),
+            }
+        }
+
+    def dispatch_on_type(type_node):
+        return {
+            type_node.name.value : {
+                **(list(type_node.fields) | map[resolve_field] | merge | collect),
+                **resolve_interfaces(list(type_node.interfaces))
+            }
+        }
+
+    def dispatch_on_scalar(type_node):
+        return type_node.name.value 
+
+    dispatch = match[
+        (Is[lambda t: t[0] == ObjectTypeDefinitionNode], lambda g: {"_Types": map(g[1], dispatch_on_type)}),
+        (Is[lambda t: t[0] == ScalarTypeDefinitionNode], lambda g: {"_Scalars": map(g[1], dispatch_on_scalar)}),
+        (Is[lambda t: t[0] == InterfaceTypeDefinitionNode], lambda g: {"_Interfaces": map(g[1], dispatch_on_type)})
+    ]
+
+    document = parse(schema_str, no_location=True)
+    return (
+        list(document.definitions)
+        | group_by[type]
+        | map[dispatch]
+        | merge
+        | insert["skip_generation_list"][[]]
+        | collect
+    )
\ No newline at end of file
diff --git a/python/zef/ops/__init__.py b/python/zef/ops/__init__.py
index f5b1284a..0960540f 100644
--- a/python/zef/ops/__init__.py
+++ b/python/zef/ops/__init__.py
@@ -26,6 +26,7 @@
 # from ..tools.ops import _ 
 
 from .transactor_role import *
+from ..ui import show
 
 __all__ = [
     # "_",
@@ -34,4 +35,5 @@
     "value_or",
     "maybe_value",
     "config",
+    "show",
 ] + privileges.__all__ + lazy_all + transactor_role.__all__
\ No newline at end of file
diff --git a/python/zef/ui/__init__.py b/python/zef/ui/__init__.py
new file mode 100644
index 00000000..3303735b
--- /dev/null
+++ b/python/zef/ui/__init__.py
@@ -0,0 +1,35 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .components import *
+from .zef_rich import show
+from .visualizations import to_table, to_card
+
+__all__ = [
+    "show",
+    "Text",
+    "Code",
+    "Style",
+    "Table",
+    "Column",
+    "Frame",
+    "HStack",
+    "VStack",
+    "BulletList",
+    "NumberedList",
+    "Paragraph",
+    "to_table",
+    "to_card",
+]
\ No newline at end of file
diff --git a/python/zef/ui/components.py b/python/zef/ui/components.py
new file mode 100644
index 00000000..cfc4e61d
--- /dev/null
+++ b/python/zef/ui/components.py
@@ -0,0 +1,49 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from ..core.VT import ValueType_
+from functools import partial
+
+
+def zef_ui_ctor(type_name, self, *args, **kwargs):
+    data = None
+    if len(args) == 1: 
+        data = args[0]
+    elif len(args) > 1:
+        raise ValueError(f'{type_name} constructor takes at most one positional argument, got {args}')
+
+    if data:
+        if "data" in kwargs: raise ValueError(f'{type_name} was passed both a data positional arg and a data key in kwargs which isn\'t allowed')
+        else: kwargs = {**kwargs, "data": data}
+    
+    ctor_dict = {"type_name":type_name, "constructor_func": partial(zef_ui_ctor, type_name), "pass_self": True}
+    if len(self.d['absorbed']) == 1:
+        return ValueType_( absorbed = ({**self.d['absorbed'][0], **kwargs},), **ctor_dict)
+
+    return ValueType_(absorbed = (kwargs,), **ctor_dict)
+
+
+
+Text  = ValueType_(type_name='Text', constructor_func = partial(zef_ui_ctor, 'Text'), pass_self = True)
+Code  = ValueType_(type_name='Code', constructor_func = partial(zef_ui_ctor, 'Code'), pass_self = True)
+Style = ValueType_(type_name='Style', constructor_func = partial(zef_ui_ctor, 'Style'), pass_self = True)
+Table = ValueType_(type_name='Table', constructor_func = partial(zef_ui_ctor, 'Table'), pass_self = True)
+Column = ValueType_(type_name='Column', constructor_func = partial(zef_ui_ctor, 'Column'), pass_self = True)
+Frame = ValueType_(type_name='Frame', constructor_func = partial(zef_ui_ctor, 'Frame'), pass_self = True)
+HStack = ValueType_(type_name='HStack', constructor_func = partial(zef_ui_ctor, 'HStack'), pass_self = True)
+VStack = ValueType_(type_name='VStack', constructor_func = partial(zef_ui_ctor, 'VStack'), pass_self = True)
+BulletList   = ValueType_(type_name='BulletList', constructor_func = partial(zef_ui_ctor, 'BulletList'), pass_self = True)
+NumberedList = ValueType_(type_name='NumberedList', constructor_func = partial(zef_ui_ctor, 'NumberedList'), pass_self = True)
+Paragraph = ValueType_(type_name='Paragraph', constructor_func = partial(zef_ui_ctor, 'Paragraph'), pass_self = True)
\ No newline at end of file
diff --git a/python/zef/ui/visualizations.py b/python/zef/ui/visualizations.py
new file mode 100644
index 00000000..a1058c7c
--- /dev/null
+++ b/python/zef/ui/visualizations.py
@@ -0,0 +1,170 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import *
+from ..core import *
+from ..ops import *
+
+# -----------------------Table View------------------------------------------
+def generate_mapping_for_et(et, g):
+    z = delegate_of(et, g)
+    connected_rels = z | out_rels[RT] | collect
+    mapping =  connected_rels | map[rae_type] | collect
+    return mapping
+
+@func
+def value_or_type(node):
+    if is_a(node, AET):
+        val =  node | value | collect
+        if isinstance(val, bool):
+            return ["❌","✅"][val]
+        return val
+    else:
+        default = f"({repr(rae_type(node))})"
+        if len(Outs(node, RT.Name)) == 1:
+            return f"{node | Out[RT.Name]  | value | collect}{default}"
+        
+        return default
+
+@func
+def traverse_rt(rt, et, traverse_op = Outs):
+    outs_or_ins = et | traverse_op[rt] | collect
+    if len(outs_or_ins) == 1:
+        return outs_or_ins[0] | value_or_type | func[str] | collect
+    elif len(outs_or_ins) > 1:
+        return outs_or_ins | map[value_or_type | func[str]] | join[','] | collect
+    else:
+        return "-" 
+
+def generate_rows(nodes, mapping, et_type = None):
+    if et_type: return nodes | map[lambda et: mapping | map[traverse_rt[et]] | prepend[et_type] | func[tuple] | collect] | collect
+    return nodes | map[lambda et: mapping | map[traverse_rt[et]] | func[tuple] | collect] | collect
+
+
+@func
+def generate_table_for_single_type_zrs(selected_et, zrs, compact, limit):
+    g = Graph(zrs[0])
+    mapping = generate_mapping_for_et(selected_et, g)
+    mapping_values = [str(m) for m in mapping]
+
+    zrs = zrs | take[limit] | collect
+    padding = [(1,1,1,1),(0,0,0,0)][compact]
+
+    colors = ["#ff7e74","#ffe596","#81d76d","#57acf9","#baaee1"]
+    row_styles  = ["", "dim"]
+
+    rows = generate_rows(zrs, mapping)
+    columns = [ Column(Text(c,justify = "center"), header_style = Style(background_color="#2a3240", color = colors[i%len(colors)]), style = Style(color = colors[i%len(colors)]))
+                for i,c in enumerate(mapping_values)]
+
+    title = Frame(Text(f" A List of {len(zrs)} {repr(selected_et)}", bold= True, justify="center"), box ="simple_head")
+    table = Table(
+        show_edge=False,
+        expand=True,     
+        padding = padding,
+        show_header=True,
+        box = 'simple_head',        
+        rows = rows,
+        cols = columns,
+        row_styles=row_styles,
+    )  
+
+    return VStack([
+        title,
+        table
+    ])
+
+@func
+def generate_table_for_zrs(groups, compact, limit):
+    if len(groups) == 0: return Frame()
+    if len(groups) == 1: return generate_table_for_single_type_zrs(*groups[0], compact, limit)
+
+
+    g = Graph(groups[0][1][0])
+    mapping = groups | map[lambda tup: generate_mapping_for_et(tup[0], g)] | concat  | collect
+    
+    padding = [(1,1,1,1),(0,0,0,0)][compact]
+
+    colors = ["#ff7e74","#ffe596","#81d76d","#57acf9","#baaee1"]
+    row_styles  = ["", "dim"]
+
+    rows =  groups | map[lambda tup: generate_rows(tup[1], mapping, str(tup[0]))] | concat | take[limit] | collect
+
+    mapping = ["ET"] + mapping
+    mapping_values = [str(m) for m in mapping]
+    columns = [ Column(Text(c,justify = "center"), header_style = Style(background_color="#2a3240", color = colors[i%len(colors)]), style = Style(color = colors[i%len(colors)])) for i,c in enumerate(mapping_values)]
+
+    title = Frame(Text(f"Table of {','.join([str(g[0]) for g in groups])}", bold= True, justify="center"), box ="simple_head")
+    table = Table(
+        show_edge=False,
+        expand=True,     
+        padding = padding,
+        show_header=True,
+        box = 'simple_head',        
+        rows = rows,
+        cols = columns,
+        row_styles=row_styles,
+    )  
+
+    return VStack([
+        title,
+        table
+    ])
+
+@func
+def generate_table_from_query(query, compact = False, limit=10):
+    groups = query | filter[is_a[ET]] | group_by[rae_type] |  collect
+    table = generate_table_for_zrs(groups, compact, limit)
+    return VStack([table])
+
+
+#------------------------Card View------------------------------
+def generate_rows_for_node(node, is_out = True):
+    traverse_op, traverse_op_end = [(in_rels, Ins), (out_rels, Outs)][is_out]
+    return node | traverse_op[RT] | map[rae_type] | func[set] | map[lambda rt: (str(rt), traverse_rt(rt, node, traverse_op_end))] | collect
+
+def generate_rts_table(zr, is_out, compact):
+    colors = ["#ff7e74","#ffe596","#81d76d","#57acf9","#baaee1"]
+    padding = [(1,1,1,1),(0,0,0,0)][compact]
+
+    rows = generate_rows_for_node(zr, is_out)
+    if not rows: return ""
+    row_styles  = [colors[i%len(colors)] for i in range(len(rows))]
+
+    column_headers = ["", [" Ins 👈🏻", " Outs 👉🏻"][is_out]]
+    columns = [Column(Text(c,justify = "left", style = Style(color = colors[1]))) for i,c in enumerate(column_headers)]
+
+    return Table(
+        expand=True,     
+        padding= padding,
+        box = 'simple_head',        
+        rows = rows,
+        cols = columns,
+        row_styles=row_styles,
+    )  
+
+@func
+def generate_card(zr, compact = False):
+    title = Text(f"{(repr(rae_type(zr)))}", bold= True)
+    subtitle = Text(f"({uid(zr)})", bold= True)
+
+    outs_table = generate_rts_table(zr, True, compact)
+    ins_table = generate_rts_table(zr, False, compact)
+
+    return Frame(HStack([outs_table, ins_table], expand= True), title = title, subtitle=subtitle,  box="horizontals") 
+
+
+#--------------------------Ops--------------------------------------
+to_table =  generate_table_from_query
+to_card  =  generate_card
\ No newline at end of file
diff --git a/python/zef/ui/zef_rich.py b/python/zef/ui/zef_rich.py
new file mode 100644
index 00000000..9ef998e7
--- /dev/null
+++ b/python/zef/ui/zef_rich.py
@@ -0,0 +1,326 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import *
+from ..core import *
+from ..ops import *
+from ..core.zef_functions import func
+
+@func
+def is_a_component(component, vt):
+    return isinstance(component, ValueType_) and without_absorbed(component) == vt
+
+#--------------------------TEXT--------------------------------------
+def dispatch_rich_text(component):
+    import rich.text as rt
+
+    def resolve_style(d):
+        if "style" in d: return dispatch_rich_style(d["style"])
+        return dispatch_rich_style(Style(**d))
+
+    def resolve_attributes(d):
+        allowed_keys = ["justify", "overflow", "no_wrap", "tab_size"]
+        attributes = select_keys(d, *allowed_keys)
+        return attributes
+
+    def dispatch_on_data_type(data, style):
+        # Just a str
+        if isinstance(data, str):
+            pairs_or_text.append((data, style))
+        # Nested Text component
+        elif is_a_component(data, Text) : 
+            pairs_or_text.extend(dispatch_rich_text(data))
+        # List of the above 2
+        elif isinstance(data, list):
+            [dispatch_on_data_type(x, style) for x in data]
+        else:
+            raise ValueError("Text's data field could contain only a str, Text component, or a list composed of the two.")
+    
+    pairs_or_text = []
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Text should be of type dict!"
+    data = internals[0].get("data", "")
+    attributes = resolve_attributes(internals[0])
+    style      = resolve_style(internals[0])
+    dispatch_on_data_type(data, style)
+
+    return rt.Text.assemble(*pairs_or_text, **attributes)
+
+
+
+#--------------------------Code--------------------------------------
+def dispatch_rich_syntax(component):
+    import rich.syntax as rs
+
+    def resolve_attributes(d):
+        allowed_keys = ["line_numbers", "theme", "tab_size", "indent_guides"]
+        attributes = select_keys(d, *allowed_keys)
+        attributes["lexer"] = d.get("language", "python")
+        return attributes
+    
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Code should be of type dict!"
+    data = internals[0].get("data", "")
+    attributes = resolve_attributes(internals[0])
+    return rs.Syntax(data, **attributes)
+
+
+#--------------------------Style--------------------------------------
+def dispatch_rich_style(component):
+    def resolve_styles(d):
+        from rich.style import Style
+        allowed_keys = ["bold", "italic", "strike", "underline", "overline", "color"]
+        styles = select_keys(d, *allowed_keys)
+        styles["bgcolor"] = d.get("background_color", None)
+        return Style(**styles)
+    
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Style should be of type dict!"
+    return resolve_styles(internals[0])
+
+
+
+#--------------------------Table--------------------------------------
+def dispatch_rich_table(component):
+    import rich.table as rt
+
+    def handle_nested_components(maybe_component):
+        if isinstance(maybe_component, str):
+            return maybe_component
+        elif is_a_component(maybe_component, Text):
+            return dispatch_rich_text(maybe_component)
+        elif is_a_component(maybe_component, Column):
+            return dispatch_rich_column(maybe_component)
+        elif is_a_component(maybe_component, Style):
+            return dispatch_rich_style(maybe_component)
+        elif isinstance(maybe_component, tuple):
+            return tuple([handle_nested_components(el) for el in maybe_component])
+        else:
+            raise NotImplementedError(f"{maybe_component}:{type(maybe_component)}")
+
+    def resolve_attributes(d):
+        allowed_keys = ["row_styles", "title", "box", "expand", "show_header", "padding", "show_footer", "show_edge", "show_lines", "width"]
+        attributes       = select_keys(d, *allowed_keys)
+        # row_styles: List of attributes or strings
+        if "row_styles" in attributes:
+            attributes["row_styles"] = [handle_nested_components(x) for x in attributes["row_styles"]]
+        # title: String or Text
+        if "title" in attributes:
+            attributes["title"] = handle_nested_components(attributes["title"])
+        # box: str -> box.Attribute
+        if "box" in attributes:
+            attributes["box"] = box_constants_mapping(attributes["box"])
+        return attributes
+    
+    def resolve_row_and_cols(rows, cols):
+        return [handle_nested_components(x) for x in rows], [handle_nested_components(x) for x in cols]
+
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Table should be of type dict!"
+    rows, cols = resolve_row_and_cols(internals[0].get('rows', []), internals[0].get('cols', []))
+    attributes = resolve_attributes(internals[0])
+
+    rich_table = rt.Table(*cols, **attributes)
+    [rich_table.add_row(*row) for row in rows]
+    return rich_table
+
+#--------------------------Column--------------------------------------
+def dispatch_rich_column(component):
+    import rich.table as rt
+
+    def resolve_attributes(d):
+        allowed_keys = ["header_style", "footer_style", "style", "justify", "vertical", "width", "min_width", "max_width", "ratio", "no_wrap"]
+        attributes = select_keys(d, *allowed_keys)
+        # Resolve the non-string styles if found
+        for special_key in ["header_style", "footer_style", "style"]:
+            if special_key in attributes and is_a_component(attributes[special_key], Style):
+                attributes[special_key] = dispatch_rich_style(attributes[special_key])
+        
+        return attributes
+    
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Column should be of type dict!"
+    
+    data = internals[0].get("data", "")
+    if is_a_component(data, Text): data = dispatch_rich_text(data)
+    attributes = resolve_attributes(internals[0])
+
+    return rt.Column(data, **attributes)
+
+
+#--------------------------Frame--------------------------------------
+def dispatch_rich_panel(component):
+    import rich.panel as rp
+
+    def resolve_attributes(d):
+        allowed_keys = ["title", "subtitle", "box"]
+        attributes = select_keys(d, *allowed_keys)
+        if "title" in attributes and is_a_component(attributes["title"], Text): 
+            attributes["title"] = dispatch_rich_text(attributes["title"])
+        
+        if "subtitle" in attributes and is_a_component(attributes["subtitle"], Text): 
+            attributes["subtitle"] = dispatch_rich_text(attributes["subtitle"])
+
+        if "box" in attributes:
+            attributes["box"] = box_constants_mapping(attributes["box"])
+
+        return attributes
+    
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Code should be of type dict!"
+    if "data" not in internals[0]: raise ValueError("Can't render Frame without any data to display!")
+    data = internals[0].get("data")
+    data = match_and_dispatch(data)
+    attributes = resolve_attributes(internals[0])
+    return rp.Panel(data, **attributes)
+
+
+#--------------------------HStack,VStack--------------------------------------
+def dispatch_rich_stack(component):
+    import rich.table as rt
+
+    def handle_nested_components(maybe_component):
+        if isinstance(maybe_component, str):
+            return maybe_component
+        else:
+            return match_and_dispatch(maybe_component)
+
+    def resolve_attributes(d):
+        allowed_keys = ["expand", "padding", "pad_edge"]
+        attributes = select_keys(d, *allowed_keys)
+        return attributes
+    
+    stack_type = str(component | without_absorbed | collect)
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Table should be of type dict!"
+    data = internals[0].get('data', [])
+    assert isinstance(data, list), f"Data for ZefUI Stack should be of type list! {data} was passed"
+    cols = internals[0].get('cols', [])
+    assert isinstance(cols, list), f"Data for ZefUI Stack should be of type list! {cols} was passed"
+    data = [handle_nested_components(c) for c in data]
+    cols = [handle_nested_components(c) for c in cols]
+    attributes = resolve_attributes(internals[0])
+
+    rich_grid = rt.Table.grid(*cols, **attributes)
+    if stack_type == "HStack":
+        rich_grid.add_row(*data)
+    elif stack_type == "VStack":
+        [rich_grid.add_row(row) for row in data]
+    return rich_grid
+
+
+#--------------------------Markdown--------------------------------------
+def dispatch_rich_markdown(component):
+    import rich.markdown as rm
+
+    def resolve_style(d):
+        if "style" in d: return dispatch_rich_style(d["style"])
+        return dispatch_rich_style(Style(**d))
+    
+    internals = component | absorbed | collect
+    assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Table should be of type dict!"
+    style = resolve_style(internals[0])
+
+    return rm.Markdown(component_to_markdown_string(component), style = style)
+
+
+def component_to_markdown_string(component):
+    
+    def dispatch_bullet_or_numbered_list(component):
+        list_type = str(component | without_absorbed | collect)
+        if list_type == 'BulletList':
+            dispatch_type = lambda i: '- ' 
+        else:
+            dispatch_type = lambda i: str(i + 1) +'. '
+        nl = '\n'
+        
+        internals = component | absorbed | collect
+        assert isinstance(internals[0], dict), f"First absorbed argument for ZefUI {list_type} should be of type dict!"
+        data  = internals[0].get('data', [])
+        heading = internals[0].get('heading', "")
+        heading_level = internals[0].get('heading_level', 3)
+        
+        if heading: heading = f"{'#' * heading_level} {heading}"
+        
+        return f"""
+{heading}
+{nl.join([f'{dispatch_type(i)}{e}' for i,e in enumerate(data)])}
+    """
+
+    def dispatch_paragraph(component):
+        internals = component | absorbed | collect
+        assert isinstance(internals[0], dict), "First absorbed argument for ZefUI Paragraph should be of type dict!"
+        data  = internals[0].get('data', "")
+        heading = internals[0].get('heading', "")
+        heading_level = internals[0].get('heading_level', 2)
+        if heading: heading = f"{'#' * heading_level} {heading}"
+
+    
+        return f"""
+{heading}
+{data}
+    """
+
+    return component | match[
+        (Is[is_a_component[BulletList]], dispatch_bullet_or_numbered_list),
+        (Is[is_a_component[NumberedList]], dispatch_bullet_or_numbered_list),
+        (Is[is_a_component[Paragraph]], dispatch_paragraph),
+    ] | collect
+
+
+#-------------------Dispatch-------------------------------------
+def box_constants_mapping(box_style: str):
+    from rich import box
+    str_to_constant = {
+        'ascii':                   box.ASCII,
+        'square':                  box.SQUARE,                
+        'minimal':                 box.MINIMAL,  
+        'minimal_heavy_head':      box.MINIMAL_HEAVY_HEAD,     
+        'minimal_double_head':     box.MINIMAL_DOUBLE_HEAD,           
+        'simple':                  box.SIMPLE,    
+        'simple_head':             box.SIMPLE_HEAD,    
+        'heavy':                   box.HEAVY,                 
+        'heavy_edge':              box.HEAVY_EDGE,             
+        'heavy_head':              box.HEAVY_HEAD,
+        'double':                  box.DOUBLE,               
+        'double_edge':             box.DOUBLE_EDGE,
+        'simple_heavy':            box.SIMPLE_HEAVY,            
+        'horizontals':             box.HORIZONTALS,              
+        'rounded':                 box.ROUNDED,
+    }
+    return str_to_constant.get(box_style, box.ROUNDED)
+
+def match_and_dispatch(component):
+    return component | match[
+        (Is[is_a_component[Text]], dispatch_rich_text),
+        (Is[is_a_component[Code]], dispatch_rich_syntax),
+        (Is[is_a_component[Style]], dispatch_rich_style),
+        (Is[is_a_component[Table]], dispatch_rich_table),
+        (Is[is_a_component[Column]], dispatch_rich_column),
+        (Is[is_a_component[Frame]], dispatch_rich_panel),
+        (Is[is_a_component[HStack]], dispatch_rich_stack),
+        (Is[is_a_component[VStack]], dispatch_rich_stack),
+        (Is[is_a_component[BulletList]], dispatch_rich_markdown),
+        (Is[is_a_component[NumberedList]], dispatch_rich_markdown),
+        (Is[is_a_component[Paragraph]], dispatch_rich_markdown),
+    ] | collect
+
+def print_rich(displayable):
+    import rich
+    console = rich.console.Console()
+    displayable = match_and_dispatch(displayable)
+    console.print(displayable)
+
+show = run[print_rich]
+displayable = run[match_and_dispatch]
\ No newline at end of file


Test patch:
diff --git a/python/tests/test_simplegql.py b/python/tests/test_simplegql.py
index abf8099e..6e3eab1f 100644
--- a/python/tests/test_simplegql.py
+++ b/python/tests/test_simplegql.py
@@ -89,7 +89,10 @@ def dynamicHook(z, info):
         self.port = 4991
         root = create_schema_graph(schema_gql, hooks_string)
         g_data = Graph()
-        self.server_uuid = start_server(root, g_data, self.port, "localhost", logging=False)
+        self.server_uuid = start_server(root, g_data,
+                                        port=self.port,
+                                        bind_address="localhost",
+                                        logging=False)
 
     def tearDown(self):
         {