Lazy Temporal: Deferred Timeline Evolution in Nic
Temporal Zippers already give Nic a powerful model of state: pure, replayable, debuggable, branchable, and zero-cost in the linear case.
Explicit Laziness gives Nic predictable, memoized, deferred computation.
But what if these two ideas could merge?
What if a timeline itself could advance lazily?
This is the idea behind Lazy Temporal.
Lazy Temporal combines:
- time as a first-class concept
- lazy evaluation as a mechanism
- non-deterministic exploration
- memoizable branching
- timeline-based debugging
Into a single, uniform abstraction.
Motivation
Some computations:
- expand only a little at a time
- may never need their full history
- involve lots of branching
- or require backtracking
- or compute large “future states” that you only want on demand
Examples:
- backtracking parsers
- regex engines
- LALR and Pratt-style parsers
- constraint solvers
- search trees
- AI planning
- simulation engines
- interpreters with speculative execution
Lazy Temporal gives Nic a precise mechanism for this class of problems.
The Key Idea
In a regular timeline:
timeline x = 0 {
evolve x + 1;
evolve x * 2;
evolve x - 3;
return x;
}Every 'evolve' step moves time forward immediately.
With lazy temporal, an evolve is wrapped in laziness:
timeline x = 0 {
lazy_evolve x + 1;
lazy_evolve x * 2;
lazy_evolve x - 3;
// No evolution happened yet!
let first = force_next(); // executes first evolve
let second = force_next(); // executes next evolve
// ...
}A lazy temporal evolution:
- is queued in time, not executed
- becomes a lazy tail of the timeline
- behaves like a lazily-constructed list
- can be forced step-by-step
- can be partially explored
- can be memoized
It is essentially an infinite or partial timeline, constructed only as needed.
Syntax Overview
Proposed syntax:
lazy_evolve expression;Or the more explicit form:
evolve lazy { return expression; };And then forcing:
let value = force_next();Or forcing n steps:
force_n(10); // runs 10 lazy evolutionsOr forcing until a condition:
force_until(fn(x) -> x > 100);Why is this powerful?
Lazy Temporal lets Nic do things typically reserved for:
- logic languages
- lazy functional languages
- backtracking systems
- constraint engines
- coroutines
- actor systems
And it does so with deterministic cost, strictness, and zero hidden magic.
Lazy Temporal Example: Incremental Parser
Imagine a backtracking parser that consumes characters step-by-step:
fn parse(input: []char) -> Result[AST, string] {
timeline (i = 0, ast = Empty) {
for c in input {
lazy_evolve advance(c);
}
// force only enough to confirm the grammar
force_until(fn(t) -> grammar_is_satisfied(t.ast));
return finalize(ast);
}
}Instead of pushing parse results eagerly:
- each evolve is a lazy “possible future parse state”
- you only expand until the grammar stabilizes
- the rest of the timeline may never be computed
This massively reduces unnecessary computation.
Branching + Laziness = Insane Power
Regular 'fork()' creates a new branch immediately.
Lazy Temporal allows branch creation to be deferred:
timeline x = 0 {
lazy_evolve x + 1;
lazy_evolve x * 2;
if some_condition {
let branch = lazy_fork(); // branch created only when forced
}
let v = force_next(); // step forward lazily
}A 'lazy_fork()' behaves like:
- a branch that may or may not ever be explored
- a memoized future
- a cheap speculative path
This is perfect for:
- A* search
- proof search
- constraint satisfaction
- auto-layout engines
- code optimization passes
Lazy Temporal Streams
Temporal Zippers already support infinite streams via lazy tails.
Lazy Temporal pushes the idea further:
timeline x = 0 {
lazy_evolve x + 1; // next natural
lazy_evolve x + 1; // next
lazy_evolve x + 1; // next
// ...
}
let first_10 = force_n(10);This is cleaner than building a custom 'Stream' enum, because the timeline itself becomes the stream.
Lazy Temporal + Temporal Logic
Because lazy temporal builds a deferred timeline:
- invariants
- eventually conditions
- safety/liveness checks
can run as:
- compile-time for static parts
- incremental checks during forced steps
- deferred checks on demand
timeline counter = 0 {
lazy_evolve counter + 1;
invariant: counter >= 0;
eventually: counter == 100; // only checked when forcing
}Zero-cost when unused
Nic maintains its strong philosophy:
- Linear lazy temporal evolutions that are never forced → erased
- Lazy evolutions forced once → become direct SSA values
- Repeated forcing → memoized
There’s no interpreter, no thunk graph, no Haskell-like hidden cost structures.
Lazy Temporal is all opt-in.
When to use Lazy Temporal
Perfect for:
- incremental computations
- backtracking search
- lazy streams
- parsers
- symbolic execution
- state machines
- interpreters
- constrained simulations
Avoid for:
- simple deterministic loops
- cheap arithmetic
- places where strict evolution is clearer
Summary Table
| Concept | Description |
|---|---|
lazy_evolve | Adds a deferred evolution step |
force_next() | Forces one lazy evolution |
force_n(n) | Forces n steps |
force_until(f) | Forces until predicate satisfied |
| Lazy fork | Branch created only when forced |
| Memoization | Built-in, predictable |
| Zero-cost | Removed when unused |
Lazy Temporal turns Nic timelines into a programmable, incremental, partially evaluated time structure.
It feels like:
- a lazy list
- a coroutine
- a parser combinator
- a search tree
- a solver
- a controlled future
All at the same time — but with strict, explicit semantics and predictable performance.
This is where Nic really begins to feel like a new kind of systems language.
Next post:
The Nic Lazy Runtime: How Minimal Can It Be?