Skip to content

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:

nic
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:

nic
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:

nic
lazy_evolve expression;

Or the more explicit form:

nic
evolve lazy { return expression; };

And then forcing:

nic
let value = force_next();

Or forcing n steps:

nic
force_n(10); // runs 10 lazy evolutions

Or forcing until a condition:

nic
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:

nic
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:

nic
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:

nic
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
nic
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

ConceptDescription
lazy_evolveAdds a deferred evolution step
force_next()Forces one lazy evolution
force_n(n)Forces n steps
force_until(f)Forces until predicate satisfied
Lazy forkBranch created only when forced
MemoizationBuilt-in, predictable
Zero-costRemoved 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?

Released under the MIT License.