Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
36c571c
Key a writer decision's write set by the variable written, not by pos…
dpmills Aug 22, 2026
d4aad97
Add a control port that diffs a running program against a new version…
dpmills Aug 21, 2026
4a9f0c2
Say phase, not stage, for a pipeline position in the differ
dpmills Aug 27, 2026
bc92cd3
Retire a route when a version is installed, not when one is compiled
dpmills Aug 27, 2026
8476c25
Hang a resuming store's position off each variable, not off the id th…
dpmills Aug 27, 2026
ca8b66a
Identify a mutable variable by its declaration, not by the store that…
dpmills Aug 27, 2026
9f530e1
Read a variable's sequencing domain off the node that declares it
dpmills Aug 28, 2026
f7d7dde
Carry a variable's value into a recurrence that reads something else
dpmills Aug 28, 2026
ad83679
Start a store where its source will next offer, not at zero
dpmills Aug 28, 2026
773d112
Say what the tests pin now that a store id no longer names anything
dpmills Aug 28, 2026
2f9f5ab
Cover the case where a variable moves to a fixed collection
dpmills Aug 28, 2026
4ea0926
Hand a fan-out's agreed release to the subscriber that replaces one
dpmills Aug 28, 2026
77c21c4
Name what a recurrence counts its positions in
dpmills Aug 28, 2026
dd2da47
Keep every source's producer releases the same way
dpmills Aug 28, 2026
d289a32
Pull a new version's sinks once it has them
dpmills Aug 28, 2026
affab82
Say that a variable may move between loops, since it may
dpmills Aug 28, 2026
ee7ae5c
Say what a reused operator hands on, and what the guard refuses
dpmills Aug 28, 2026
5130e01
Resume the driver's cursors, not the window's base
dpmills Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/shared-state/check_shared_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
("src/interpreter/mod.rs", "shared cell of `C`"),
("src/interpreter/tile_operators/cycle_slot.rs", "shared cell of `Option<Box<T>>`"),
("src/interpreter/tile_operators/fanout.rs", "cell of `bool`"),
("src/interpreter/tile_operators/fanout.rs", "shared cell of `usize`"),
("src/interpreter/tile_operators/fanout.rs", "shared cell of `Box<dyn TileOperator>`"),
("src/interpreter/tile_operators/fanout.rs", "shared cell of `Box<dyn TileOperator>`"),
("src/interpreter/tile_operators/mod.rs", "ambient mutable state"),
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ cargo run -- --inspect tests/programs/inner_join/program.cambra

The inspector defaults to port 8080 (`--inspect=9090` to change it). After the program finishes, the process stays alive so you can browse `http://localhost:<port>`; Ctrl+C to exit.

### Control port

Pass `--control` to let a running program be diffed against, and replaced by, a new version of its source. Both endpoints take the new source as the query string or the request body:

```bash
cargo run -- --control tests/programs/http_greeter/program.cambra

# in another terminal, with the edited program in v2.cambra:
curl --data-binary @v2.cambra localhost:8081/diff
curl --data-binary @v2.cambra localhost:8081/update
```

`/diff` reports how the two versions differ and changes nothing. `/update` replaces the program in place: sockets stay open, every binding whose computation is unchanged keeps running, and a variable whose logic you did edit resumes from the value it was holding. Add `phase=<name>&` before the source to diff somewhere other than the default (`lowered`, `inferred`, `inlined`, `channelized`, `lambda-elim`, `planned`).

An update may change the program's logic freely, add endpoints, and stop serving ones it no longer wants (their addresses then answer 404). A variable may even move to another loop or into a transaction: it keeps its value and starts counting positions in whatever it now iterates. The one thing an update may not do is break continuity of state: a variable the running program is holding a value for must be one the new version declares, at the same type, or the update is refused and the running program is left serving. See [live-update.md](src/ccl/design/live-update.md).

The control port defaults to 8081 (`--control=9090` to change it).

## License

Apache 2.0 — see [LICENSE](LICENSE).
2 changes: 2 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ The connections between the layers above and the capabilities Cambra claims:
- **Incremental views by construction.** Monotone tilings mean live aggregates are maintained, not recomputed. The materializable time-pinned view is the decided form of this — the history substrate is implemented, the transaction-handle read it needs is not yet (the ledger's `txn_kv` pins it).
- **Verification [Sketched].** A small referentially-transparent core plus a refinement-typed checker means a semantic predicate established at one point composes across the whole program. The machinery exists today; whole-application contracts on top of it are the driving direction.
- **Validation and program branching [Open].** Referential transparency makes program versions *syntactically comparable with well-defined semantics*, and temporal functional mutation makes state a value over time domains — *branchable and pinnable by construction*. Together they are the substrate for branching a running application — logic and state — exercising the branch under a realistic workload, and diffing behaviour.
- **Live update [Partial].** A running program can be replaced by a new version of its source over the control port (`--control`): `/diff` reports how the two versions differ, at a pipeline phase the caller picks, and `/update` swaps the program. The new version inherits the running one's sources and sinks, may add to them, and retires a route it stops serving, and inherits the operator behind every `Let` binding whose computation is unchanged; a variable whose logic the edit did touch resumes from the value it held. A variable that moves to another loop, or into or out of a transaction, keeps its value and counts positions in whatever it now iterates. The one refused update is one that cannot take over the state — a variable the new version no longer declares, or declares at a different type. See [live-update.md](/src/ccl/design/live-update.md). Running two versions at once is not implemented.
- **Observability.** The compilation pipeline preserves a legible chain from source to running state; the web inspector (`--inspect`) serves the CHL AST, the lowered CCL, the operator graph, and live per-producer runtime state for any running program.

## Feature status at a glance
Expand All @@ -221,6 +222,7 @@ The connections between the layers above and the capabilities Cambra claims:
| `rec` fixpoint bindings | **[Decided]** |
| Collections-as-functions model | Organizing idea decided; encodings **[Sketched]** |
| `match` / `case` | Tag dispatch implemented; deeper patterns **[Tentative]** |
| Live update of a running program (`--control`) | Implemented for logic, endpoint changes, and state resume; running two versions at once is **[Open]** |
| `while`, floats, imports, classes, exceptions | Absent (see the spec) |

The [spec](chl-spec.md) carries the authoritative per-construct markers; [demo-programs.md](demo-programs.md) maps them to runnable programs and their blockers.
Expand Down
49 changes: 49 additions & 0 deletions src/ccl/ccl_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,55 @@ pub fn free_names_in_value(expr: &Expr) -> HashSet<Name> {
out
}

/// Every name free in `expr`, in its term structure **and** inside the
/// refinement predicates riding its type slots — the set counterpart of
/// [`is_free`], as [`free_names_in_value`] is of [`is_free_in_value`].
///
/// Answers "which bindings does this term read?" for a caller that must decide
/// something for all of them at once and would otherwise walk the term once per
/// candidate name. Predicates count because a term can depend on a binding
/// through one: a refinement is a term in a type position, and a domain
/// restriction built from it reaches the operator graph.
///
/// Self-referential predicates terminate on the same `visited` discipline as
/// [`count_free`], and the refinement element binder is excluded in type
/// position for the reason given on [`count_free_in_type_with_visited`].
pub fn free_names(expr: &Expr) -> HashSet<Name> {
fn go(
e: &Expr,
bound: &mut Vec<Name>,
visited: &mut HashSet<PredicateId>,
out: &mut HashSet<Name>,
) {
e.walk_type_slots(|ty| {
walk_refined_predicates(ty, visited, &mut |pred, vis| {
// A binder's declared type sits in the enclosing scope, so the
// predicate is walked under the binders in force *here*.
let mut inner = HashSet::new();
go(pred, bound, vis, &mut inner);
out.extend(inner.into_iter().filter(|n| !n.is_elem()));
});
});
for_each_scoped_item(e, &mut |item| match item {
ScopedItem::VarRef(n) => {
if !bound.contains(n) {
out.insert(n.clone());
}
}
ScopedItem::KeyRef(_) => {}
ScopedItem::Child { expr, binders } => {
let depth = bound.len();
bound.extend(binders.iter().map(|b| b.name.clone()));
go(expr, bound, visited, out);
bound.truncate(depth);
}
});
}
let mut out = HashSet::new();
go(expr, &mut Vec::new(), &mut HashSet::new(), &mut out);
out
}

/// Value-only worker for [`is_free_in_value`]: the same fold over
/// [`crate::ccl::scope::for_each_scoped_item`] as [`count_free`], minus the type
/// slots (so a refinement on a `Lambda` param — which lives in the type — is
Expand Down
Loading