HQL-0005: Pipeline vocabulary and the logical/physical separation¶
Status: drafted Created: 2026-09-28 Source: typed pipeline operations
Abstract¶
An HQL pipeline is an ordered sequence of typed steps. A step applies an operation or a presentation to the value flowing through the pipeline. The logical pipeline does not prescribe a particular execution strategy. This record fixes six terms — pipeline, step, operation, presentation, operator and stage — and assigns each exactly one meaning. The first five describe the program; stage is reserved for the execution planner, where one or more steps are grouped into a physical unit of execution. An operation is a value of a reserved function type, so a user-defined function or lambda of that type is an operation on equal terms with the standard vocabulary (of hql reserved keywords). A stage is not a node of its own: the planner annotates each step with the stage it belongs to, and the annotated pipeline is the schedule it distributes. The knowledge base, the Rust host and the corpus comments adopt the vocabulary together.
Motivation¶
The words have drifted, and the drift is starting to encode a design decision nobody made.
- HQL-0004 defines the pipe as a "stage boundary" and
describes every step as a stage. That makes the logical program and its
execution plan the same object, which is exactly what a planner must be free
to change: fusing
filter | mapinto one traversal would, under that wording, delete a stage from the program. - map/reduce for queries and the
pipes card inherit that wording, and the
examples/mapreduce/comments say "three stages" for three steps. - The Rust registry entry is called
Step(src/extensions/mod.rs), but it is the declaration of what a step invokes, not an occurrence in a pipeline. Twofiltersteps in one pipeline share one registry entry. map,filterand the like are sometimes called operators, which collides with|,+,:and->.
The practical consequence is a language model that reads as one MapReduce cycle. Once pipelines may be long — streaming, batching, aggregating and presenting in one program — the planner has to be able to cut a single pipeline into several execution stages with different strategies, and the vocabulary has to leave it room to.
Goals¶
- Every term has one meaning, and the logical terms never name a physical unit.
- A long pipeline can be planned into several stages without the language saying anything about how.
- A user can write an operation, named or as a lambda, and a planner treats it
exactly as it treats
maporfilter. - A plan has one form: the pipeline's own step nodes, each annotated with its stage.
Non-goals¶
- No planning algorithm. This record fixes the unit a planner produces and the form its plan takes; how it chooses stage boundaries is left open.
- No syntax change. Named functions and lambdas are already written today; what changes is that their type can make them operations.
Specification¶
The six terms¶
The terms are normative across doc/, src/, std/ and examples/.
Pipeline ordered logical program: a maximal chain of steps joined by |
Step one occurrence, at one position, in a pipeline
Operation the semantics a step invokes: map, filter, collect, emit, group, reduce, ...
Presentation consumer-facing rendering a step may invoke instead: table, graph, markdown, ...
Operator syntax: | + : -> and the rest of the operator table
Stage physical, planned execution unit; one or more steps
Backend the mechanism that executes stages
Pipeline
|
+-- Step -> Operation
+-- Step -> Operation
+-- Step -> Presentation
| planner
v
Plan
+-- Stage (one or more steps) -> Backend
- A step MUST invoke exactly one operation or exactly one presentation. It
is identified by its position, so
filterwritten twice is two steps and one operation. map,filterand every other named thing after|MUST NOT be called an operator.|is the operator that forms a pipeline.- Stage MUST NOT be used as a synonym for step. A stage exists only in an execution plan; its boundaries are chosen by a planner and are not visible in HQL source.
- HQL syntax MUST NOT expose stage boundaries, backends, or any other physical choice. A program that could name one would be claiming its answer depends on it, and HQL-0004 already requires that it does not.
An operation is a value of a reserved function type¶
Operations are not a closed list. The language reserves a function type for them, and anything of that type is an operation:
// Reserved. One per shape; each is a function type from input to output.
Elementwise<I, O>
Partition<I, O>
Reduction<I, O>
Barrier<I, O>
// Their common supertype.
Operation<I, O>
Operation<I, O>and its four shape subtypes are reserved names. A program MUST NOT declare a type with one of them.- Each reserved type is a function type
I -> Othat additionally carries the HQL-0004 shape its name states and a purity. A value of the type is callable exactly as anI -> Ofunction is. - The standard vocabulary is declared through these types:
filterandmapareElementwise,groupisPartition,sizeisReduction,sortisBarrier, each as HQL-0004 tabulates it. They have no status a user operation cannot also have. - A user operation is a named function or a lambda whose type is one of the
reserved types. It MAY appear as a step wherever a standard operation may,
under the same typing rule:
x | f(a)is typed asf(x, a). - A function whose type is a plain
I -> Ois still callable from a step's arguments (map(b => summarize(b))) but MUST NOT itself be a step. A planner knows nothing about its shape and so could not place it. - The checker MUST verify a declared shape it can verify: a function declared
Elementwisewhose body reads anything but its one element is a type error. A shape the checker cannot verify, such as an associativeReduction, is the author's claim, and a planner relies on it as it relies on a host extension's declared shape. - Purity follows HQL-0001: a user operation is pure only if every call in its body is pure. An impure user operation is a barrier to reordering whatever its declared shape.
A user operation therefore enters the plan on the same information as a host one. The planner reads shape and purity off the step's operation type and never off its name.
A stage is an annotation on steps¶
Before a backend accepts a pipeline, the planner compiles a schedule: it partitions the pipeline's steps into stages and decides how those stages are distributed as jobs. It records that decision by annotating the steps, not by building a second tree.
step node annotation
events stage 0 streaming
| filter(e => e.active) stage 0 streaming
| collect(100) stage 1 batch boundary
| map(b => summarize(b)) stage 2 parallel, 8 jobs
| emit stage 3 streaming
| filter(r => r.accepted) stage 3 streaming
| table stage 4 presentation
- A plan is the checked pipeline with every step node carrying a stage annotation. The steps, their order and their operations are unchanged; only the annotations are added.
- Every step MUST carry exactly one stage annotation, and the steps of one stage MUST be contiguous in the pipeline.
- A stage annotation names the stage and the execution properties the backend needs to dispatch it: its strategy (streaming, batch, parallel, presentation) and its job units. Its exact fields are backend metadata and are not fixed here.
- A backend MUST refuse a pipeline that is not fully annotated. Annotation is the step at which the pipeline is accepted for execution; there is no unplanned execution path.
- Annotation is automatic. The planner derives every annotation from the operation type of each step (its shape and purity) and from what the target backend can execute; nothing in the program or from its author is required.
- The
localhostbackend's plan is the trivial one: the whole pipeline is a single stage, evaluated in process one step after another. - Annotations are not visible in HQL source and MUST NOT change the value the pipeline produces.
Plan IR¶
Placeholder; not yet designed.
The plan IR is the low-level form of an annotated plan, in the spirit of LLVM IR rather than a bytecode: the checked query itself, with each step carrying its annotations, stating what is scheduled where. It is the form a schedule is inspected and debugged in.
The IR is bound to a backend topology. It states the nodes a plan runs on and the resources each job requires, so that it says up front what running the query takes: on a Kubernetes cluster, for example, the resource claims that must be granted before the query can run at all.
What survives from HQL-0004¶
The execution contract of HQL-0004 stays; only its nouns change.
- HQL-0004's region is this record's pipeline. The two are the same
maximal chain of
|-joined steps; one name is kept. - The pipe still lowers to a node the executor sees and is never rewritten into a call before a backend receives it. That node is a step node.
- Shape (elementwise, partition, reduction, barrier) and purity remain properties of an operation. They are what a planner reads when it decides where one stage ends and the next begins.
- "Adjacent elementwise stages may be fused" becomes "adjacent elementwise steps may be fused into one stage".
A pipeline is not one MapReduce cycle¶
MapReduce is one strategy a planner may choose for a suitable run of steps; it is not the model of a pipeline.
logical pipeline one possible plan
events streaming stage
| filter(e => e.active) |
| collect(100) batch boundary
| map(b => summarize(b)) parallel stage
| emit streaming stage
| filter(r => r.accepted) |
| table presentation stage
- A single plan MAY contain streaming, batching, partitioned or parallel execution, graph or index access, materialization boundaries and presentation, each as its own stage.
- Every plan MUST produce the value the
localhostbackend produces, as HQL-0004 requires.
The Rust host follows the vocabulary¶
The registry names what it holds.
- The registry entry currently named
StepSHOULD be renamedOperation, andStepSpecSHOULD becomeOperationSpec. The entry declares an operation; a step is the execution-tree node that invokes it. - Presentations SHOULD be registered separately from operations, as HQL-0008 specifies, so that the host type says which one a step invokes.
hql builtinsSHOULD list operations and presentations under separate headings.- A host operation and a user operation SHOULD reach the step node through the same operation type, so that the planner has one code path for both.
- Stage annotations SHOULD be a field of the step node, empty until the planner fills it, rather than a separate plan structure.
Backwards Compatibility¶
No program changes meaning. A program that already declares a type named
Operation, Elementwise, Partition, Reduction or Barrier stops
checking; the corpus has none. The rename touches Rust identifiers that are
pub(crate) or are pub only within the hql crate's extension module; a
public re-export, if any, is kept as a deprecated alias for one release. The
--format json diagnostic field stage is a separate use of the word (the
processing phase that reported the diagnostic), and renaming it is a wire
change; see Open Questions.
Reference Implementation¶
src/extensions/mod.rs,src/extensions/spec.rs—SteptoOperation,StepSpectoOperationSpec, and every call sitesrc/builtins.rs— separate headings for operations and presentationsstd/core.hql— declares the reserved operation types- the checker — accepts a user function of an operation type as a step, rejects
a plain function type there, and verifies an
Elementwisebody - the step node — gains a stage annotation; the
localhostplanner puts every step in one stage, and the backend refuses an unannotated pipeline doc/wiki/hql/types/pipes.hmd— states the six terms and the sentence in the Abstract; stops calling a step a stagedoc/models/behavior/map-reduce.md,doc/proposals/HQL-0004/README.md— "stage" becomes "step" wherever it names an occurrence; "region" becomes "pipeline"doc/wiki/hql/presentation.hmd— "a presenter is a terminal stage" becomes "a terminal step"examples/mapreduce/queries/*.hql— comment wording only
Test Plan¶
Unit tests MUST include:
hql builtinsoutput lists operations and presentations under distinct headings, and every registered entry appears under exactly one- a named function and a lambda of type
Elementwise<T, U>each run as a step and produce the same value as the equivalentmap - a function of plain type
T -> Uused as a step is a type error - a function declared
Elementwisewhose body reads beyond its element is a type error - declaring a type named
Operationis a type error - the
localhostplan annotates every step with the same single stage, and the backend refuses a pipeline with a missing annotation
A documentation check SHOULD be added or run by hand:
grep -rn -i "stage" doc/wiki doc/models examples std src
Every remaining hit either names a planner/execution unit or is one of the separate uses listed under Open Questions.
Open Questions¶
- Does the diagnostic field
stagein--format json(reporting) becomephase? It names parse/check/evaluate, not a planner unit; keeping it leaves a third meaning of the word in a public format, and renaming it is a wire change. - Does vault assembly, which architecture describes as coordinated "stages", adopt "phases" as well, so that the reservation holds across the whole knowledge base?
- Is "presentation" the name of both the category and the registry entry, or
does the entry get a separate noun (
Presenter, as the presentation card currently says)? - Is purity part of the reserved operation type (
Elementwise<I, O>versus an impure variant), or always inferred from the body? - Do stream operations from HQL-0006 and
HQL-0007 (
collect,emit) need a shape beyond the four, or doesBarriercover a batch boundary? - Which fields of a stage annotation are fixed by the language, and which are left to each backend?
Changelog¶
- 2026-09-28: drafted
- 2026-10-17: operations as a reserved function type; stages as step annotations