#pragma once #include "Api.h" #include "CursorManager.h" #include "Types.h" #include #include #include #include /** * @file VM.h * @brief Declares the logic execution boundary for relational and higher-order workloads. */ namespace nt { /** * @class VM * @brief Hosts the first-order logic (FOL) and second-order logic (SOL) cores. * * This partitioning exists to ensure retrieval terminates when execution is * constrained to first-order logic. * * - Tarski runtime (FOL): a relational engine that implements Tarski's * relational calculus. It uses a deterministic Volcano-iterator model to * perform relational algebra operations such as joins, projections, and * selects directly against the CursorManager and a prepared plan. * * - Karuta runtime (SOL): an isolated WAM-based environment for * higher-order logic programming. It handles recursion, choice points, and * other programming constructs that do not guarantee termination. * * @remark Lazy evaluation and paging strategy * * Both the Tarski first-order engine and the Karuta second-order engine are * strictly pull-based. Data is streamed lazily from the CursorManager to * preserve a constant memory footprint regardless of relation size. * * Pipeline dynamics: * - Demand-driven: the Karuta WAM requests a fact only when a goal needs * satisfaction. A request propagates down the algebra tree as a chain of * Next() calls. * * - Short-circuiting: if the Karuta engine finds a solution or reaches a * failure that invalidates a branch, the Tarski iterator is discarded or * reset without materializing the remaining tuples. * * - Resource stewardship: by maintaining laziness up to the highest logical * level, the runtime minimizes pressure on ObjectManager and keeps * snapshots pinned through LifecycleManager::Monitor only for the minimum * time required. */ /** * @brief One segment of a parameterized relation path in a SCAN node. * * Var segments are resolved at runtime from the outer tuple of a JOIN: * the JOIN writes the bound value into the cursor's args vector before * each probe. Const segments are ground literals fixed at plan construction. * * The resolved args vector is passed to the ephemeral relation's generator * (for EPHEMERAL_RELATION cursors) or ignored (for stored RELATION cursors, * which need no per-probe parameterization). */ struct PathArg { enum class Kind { Var, Const }; Kind kind; std::string name; /**< Attribute name to look up (Var), or literal value (Const). */ static PathArg Var(std::string attr) { return {Kind::Var, std::move(attr)}; } static PathArg Const(std::string val) { return {Kind::Const, std::move(val)}; } }; enum Operation { FOL_OPERATION_SCAN = 1, FOL_OPERATION_JOIN, FOL_OPERATION_TAKE, FOL_OPERATION_PROJECT, FOL_OPERATION_MATERIALIZE, FOL_OPERATION_RENAME, FOL_OPERATION_UNION }; /** * @brief A node in the Volcano operator tree executed by the Tarski (FOL) runtime. * * Each node represents one relational algebra operator. The tree is demand-driven: * VM::Next() propagates pull requests from root to leaves. * * Supported operators: * - SCAN : pulls tuples from a pre-opened cursor (stored or ephemeral relation). * - JOIN : nested-loop join. For each outer (left) tuple, resolves scan_args on * the inner (right) SCAN, resets its cursor, and probes it. Works for * both stored relations (full re-scan of inner) and ephemeral relations * (O(1) membership probe when all args are bound). * - TAKE : passes at most take_limit tuples through, then returns nullptr. Use * this to bound scans over AlephZero ephemeral relations. * - PROJECT: filters each input tuple to the named attributes in project_attrs. * - MATERIALIZE: on its first scan it pulls the whole child and keeps a * private copy of every tuple. On later scans it replays that copy * and never reads the child again. Sits on the inner side of a * nested-loop JOIN so the inner relation is read once instead of * once per outer tuple. This only works when the child does not * depend on the outer tuple, meaning it has no Var SCAN leaves that * the JOIN binds, because the same cached tuples are handed back for * every outer tuple. Rewind does not pass the outer tuple through * this node, so it is up to the planner to place it only where the * child is independent of the outer side. */ struct PlanNode { Operation op; PlanNode* left = nullptr; PlanNode* right = nullptr; /** * SCAN: pre-opened cursor. For ephemeral relations this cursor is * initially exhausted; the JOIN resets it with resolved args before * each probe. */ CursorManager::cursor* scan_cursor = nullptr; /** * SCAN: argument template for parameterized relations. * Var entries are resolved from the outer JOIN tuple; Const entries * are used as-is. The resolved values are written into scan_cursor->args * by the JOIN before it calls Next() on this node. */ std::vector scan_args; /** TAKE: maximum number of tuples to emit before returning nullptr. */ std::size_t take_limit = 0; std::size_t take_count = 0; /**< Runtime counter; mutable during execution. */ /** PROJECT: unordered set of attribute names to keep. */ std::unordered_set project_attrs; std::optional project_buffer; /** * JOIN runtime state. * * join_left holds the current outer tuple while probing the inner * (right) side. The pointer is stable for the lifetime of one outer * iteration because Next(left) is only called when join_left is reset * to nullptr, which happens only after the inner side is exhausted. * * join_buffer holds the merged output tuple and is overwritten on each * successful join. Callers must consume the returned pointer before * calling Next() again — same contract as a cursor page pointer. */ Tuple* join_left = nullptr; std::optional join_buffer; std::unordered_set join_attrs; /** * MATERIALIZE runtime state. * * mat_buffer holds owned copies of the child's tuples, filled on the * first scan. mat_done marks the cache complete (child exhausted). * mat_pos is the replay cursor, reset to 0 by Rewind on each later scan. */ std::vector mat_buffer; std::size_t mat_pos = 0; bool mat_done = false; /** RENAME runtime state. */ PlanNode* upstream = nullptr; std::unordered_map attrs; /** UNION runtime state. */ std::vector nodes; std::vector::iterator current_node; }; class VM { public: // TODO: CursorManager should be injected as a shared runtime instance, // matching the pattern flagged for HandlerManager and LifecycleManager. // // A single CursorManager instance supports multiple simultaneous cursors — // all iteration state lives in each cursor struct, not in the manager. // A JOIN plan, for example, holds two independent cursors (one per SCAN leaf) // and both are driven through the same CursorManager reference. explicit VM(CursorManager& cursors); /** * @brief Pulls the next tuple from the plan tree (Tarski/FOL core). * * Propagates a Next() call down the operator tree: * - SCAN delegates to CursorManager::Next. * - JOIN drives the nested-loop: for each outer tuple it resolves * scan_args on the inner SCAN, resets the inner cursor, and * probes until a match is found or the inner is exhausted. * - TAKE counts emitted tuples and returns nullptr once the limit * is reached. * - PROJECT filters an input tuple to the requested attribute names. * * @param node Root of the plan subtree to evaluate. * @return Next matching tuple, or nullptr when the plan is exhausted. */ Tuple* Next(PlanNode* node); private: CursorManager& cursors_; /** * Resolves a scan_args template against an outer tuple, producing the * concrete args vector to write into the inner cursor before probing. */ static std::vector ResolveArgs(const std::vector& tmpl, Tuple* from); /** Resets the inner cursor state and writes resolved args into it. */ static void ResetInner(CursorManager::cursor* c, std::vector args); /** * Restarts the inner (right) subtree of a JOIN for a new outer tuple. * Walks down to every SCAN leaf, rebinds its scan_args to the outer * tuple, and resets its cursor. On the way it also clears the operator * state it passes through, such as TAKE counters and nested JOIN buffers. */ void Rewind(PlanNode* node, Tuple* outer); /** Merges left and right tuple attributes into node->join_buffer. */ static Tuple* MergeInto(PlanNode* node, Tuple* left, Tuple* right); }; } // namespace nt