nustack

Kinds

The atom roles Nu provides. Each kind is a point in the two-dimensional space of mutation and cardinality, plus the composition glue that ties mutating atoms into larger programs.

KindMutates ContextYieldsCardinalityRole
Refno (own)address / valueSCALARaddress atom; only path to Context
ScalarQuerynoone valueSCALARpure value producer
StreamQuerynomany valuesSTREAMpure stream producer
CommandyesnothingVOIDpure mutator
ScalarActionyesone valueSCALARmutates and yields one
StreamActionyesmany valuesSTREAMmutates and yields many
Flowno (own)nothingVOIDcomposes mutating children
SpantransparenttransparentTRANSPARENTcross-cutting wrapper

Deep dives per kind come later. This doc is the high-level breakdown and the composition rules between kinds.

Ref

A name for a location. The only atom that touches Context. Two roles in one:

  • a symbol naming an address;
  • a Nu atom that, in a value slot, yields the value at that address.

Examples: user_ref, config["theme"], counter, orders[i].

Refs are the seam. Every READ, WRITE in a Nu program happens at a Ref. No Refs in a subtree, no mutation in the subtree.

Query

An atom that yields a value and never mutates Context on its own (its subtree may still mutate via Action descendants). Pure expression-level work: arithmetic, comparison, projection, iteration that produces values.

Splits on cardinality: a ScalarQuery yields one value, a StreamQuery yields many.

Examples: Add(a, b), Eq(x, y) (ScalarQuery); ItemsOf(coll_ref), Map(items, f) (StreamQuery).

Command

An atom that mutates Context and yields nothing. The pure mutator: it writes through one or more Ref children and that is its only output.

Examples: Store(ref, value), Copy(src, dst), Append(list_ref, item), Delete(ref).

Action

An atom that mutates Context and yields a value. The deliberate hybrid: real-world operations that atomically write and return (pop, swap, INSERT...RETURNING, POST /resource → id).

Like Query, Action splits on cardinality: a ScalarAction mutates and yields one value, a StreamAction mutates and yields many (Drain a queue, DELETE ... RETURNING over a predicate).

Examples: Pop(list_ref), Swap(ref, new_value), Create(collection_ref, payload) (ScalarAction); Drain(queue_ref) (StreamAction).

Action exists because modelling these as Command-then-Query would lose atomicity. One syscall, one transaction, one network roundtrip becomes two unrelated steps.

Flow

An atom that composes mutating children into a larger mutation-shaped program. Yields nothing. Mutates nothing on its own. Its body slot doesn't need a value; it needs a mutator (Command, Action, or another Flow whose own leaves are mutators).

Examples: Sequential(c1, c2, ...), IfDo(cond, body), ForEachDo(items, body), Parallel(c1, c2).

Flow's child should contain a mutating atom, otherwise it is meaningless (yields nothing, no mutation downstream -> no observability).

Span

A transparent wrapper for cross-cutting concerns: snapshotting, transactions, retry, fallback. It wraps any Nu and forwards the body's yield in the same shape and protocol. The Span atom is always a Span; parents slot-fit it by looking through to the body's yield, not by re-typing.

Examples: Snapshot(body), Transaction(body), Retry(body, policy), TryCatch(body, handler).

Span doesn't change the composition matrix below: it inherits its body's slot fit.

Composition matrix

Rows are parents, columns are children. Cells: ✓ accepted, ✗ rejected.

parentRefQueryCommandActionFlow
Ref✓✓✗✓✗
Query✓✓✗✓✗
Command✓✓✗✓✗
Action✓✓✗✓✗
Flow(p)(p)✓✓✓

(p) = accepted only as a parameter slot (cond, items, count), never as a body peer.

One rule decides every cell

The cells are applications of mutation. Examples:

  • A yielding parent (Q / A) must ride a mutating ancestor chain - so its slots feed that chain, and a child that doesn't yield has nothing to feed.
  • A non-yielding parent (Flow) draws its observability from the subtree - so its body slot must hold a mutator.

On this page