tur/trail
backtrackable state: mutable cells whose writes undo on demand.
Since: SX1 (graduated from `--enable=backtrackable-state` 2026-08-29)
Bt
(Bt :nil)
capability tag for mutating the backtracking trail.
nil (never performed; used only as a #fx{Bt} row annotation)
(defn bind-var [c : BtCell v : int] #fx{Bt} : bool (bt-set! c v))
Since: SX1 (checked since 2026-09-05)
Mark
(Mark)
an opaque handle to a trail level.
BtCell
(BtCell)
a mutable cell whose writes are trailed and undo on backtrack.
GCell
(GCell)
a mutable cell that is NEVER trailed.
bt-cell-new
(bt-cell-new [init : int] :)
allocate a trailed cell holding `init`.
| init | the cell's starting value |
A BtCell. Release with bt-cell-free.
(bt-get (bt-cell-new 0)) ; => 0
Since: SX1
bt-lvar-new
(bt-lvar-new [unbound : int] :)
allocate a WRITE-ONCE trailed cell, initially unbound.
| unbound | the value `bt-get` reads while the cell is unbound |
A BtCell in the unbound state.
(bt-bound? (bt-lvar-new 0)) ; => false
Since: SX1
bt-cell-free
(bt-cell-free [c : BtCell] :)
release a cell.
| c | the cell |
(bt-cell-free c)
Since: SX1
bt-get
(bt-get [c : BtCell] :)
read a trailed cell.
| c | the cell |
Its current value.
(bt-get c) ; => 0
Since: SX1
bt-bound?
(bt-bound? [c : BtCell] :)
has this write-once cell been bound?
| c | the cell |
true once bound. Always true for a value cell.
(bt-bound? v) ; => false
Since: SX1
bt-set!
(bt-set! [c : BtCell v : int])
write a trailed cell.
| c | the cell | |
| v | the new value |
true on success; false when `c` is a write-once cell that is already bound -- binding twice is a caller bug, not a silent overwrite.
(bt-set! c 42) ; => true
Since: SX1
g-cell-new
(g-cell-new [init : int] :)
allocate a cell that is never trailed.
| init | the starting value |
A GCell. Release with g-cell-free.
(g-get (g-cell-new 7)) ; => 7
Since: SX1
g-cell-free
(g-cell-free [g : GCell] :)
release a never-trailed cell.
| g | the cell |
(g-cell-free g)
Since: SX1
g-get
(g-get [g : GCell] :)
read a never-trailed cell.
| g | the cell |
Its current value.
(g-get g) ; => 7
Since: SX1
g-set!
(g-set! [g : GCell v : int])
write a never-trailed cell. The write survives every undo.
| g | the cell | |
| v | the new value |
(g-set! g 7)
Since: SX1
bt-mark
(bt-mark)
push a trail level and return a handle to it.
A Mark. Pass it to bt-undo-to! to restore, or bt-commit-to! to keep.
(let [m (bt-mark)] (bt-set! c 1) (bt-undo-to! m))
Since: SX1
bt-undo-to!
(bt-undo-to! [m : Mark])
restore every trailed write made since `m`.
| m | a mark from bt-mark |
true on success; false when `m` is STALE -- already undone, already committed, or from a level index that has since been reused. A stale undo is refused rather than performed: it is what a re-entered continuation produces, and quietly "succeeding" would restore words belonging to some other level's cells.
(bt-undo-to! m) ; => true
Since: SX1
bt-commit-to!
(bt-commit-to! [m : Mark])
drop the level but KEEP its writes.
| m | a mark from bt-mark |
true on success; false when `m` is stale.
(bt-commit-to! m) ; => true
Since: SX1
bt-level
(bt-level :)
the current trail level, 0 when no mark is outstanding.
The level depth.
(bt-level) ; => 0
Since: SX1
bt-depth
(bt-depth :)
how many entries are on the trail right now.
The entry count.
(bt-depth) ; => 1
Since: SX1
untrailed-begin
(untrailed-begin)
suspend trailing until the matching untrailed-end.
(untrailed-begin) (bt-set! counter 1) (untrailed-end)
Since: SX1
untrailed-end
(untrailed-end)
resume trailing.
(untrailed-end)
Since: SX1
bt-scope
(bt-scope [A])
run `body` in a fresh trail level and undo its writes after.
| body | a thunk. Taken ^fat, so a capturing lambda works -- which is the | |
| only interesting case, since the body has to reach the cells. |
Whatever `body` returned. The VALUE survives; the trailed WRITES do not. Returning something that points into state the scope just undid is the caller's problem -- this is a search primitive, not a memory-safe region.
(bt-scope (fn [] (do (bt-set! c 99) (bt-get c)))) ; => 99, c restored
TWO THINGS THAT COMPOSE SURPRISINGLY:
1. `bt-commit-to!` inside the body escapes this scope ENTIRELY, not just
one level. Commit means "promote to level 0" -- it discards the undo
information rather than merging it into the enclosing level -- so a
committed write survives every enclosing `bt-scope` too. That is
deliberate (it is how a learned clause outlives a backjump), but it is
the opposite of what "commit into the parent" suggests to a Prolog ear.
2. A panic inside `body` skips the undo, leaving the trail at the inner
level. A later `bt-undo-to!` on an OUTER mark still unwinds past it,
but anything reading cells in between sees the body's writes. There is
no unwind protection to hang the restore on.
The `bt-undo-to!` result is discarded: it reports only that the mark was
already spent, which inside this bracket means the body undid or committed
its own enclosing level -- covered by note 1.
Carries `#fx{Bt}` itself (it pushes and unwinds a level); `body` is
deliberately left without a declared row, so a body that also prints or
performs its own effects is not rejected at the bracket.
Since: SX2
with-untrailed
(with-untrailed [A])
run `body` with trailing PAUSED, then resume.
| body | a thunk, taken ^fat so it can capture the cells it writes. |
Whatever `body` returned.
(let [m (bt-mark)]
(with-untrailed (fn [] (bt-set! c 7)))
(bt-undo-to! m)
(bt-get c)) ; => 7, not restored
Same panic caveat as `bt-scope`, and worse: a panic inside `body` leaves
trailing paused for everything after it, so later writes silently stop
being undoable. Do not put fallible work here that a `bt-scope` could hold.
Since: SX2
trail-reset!
(trail-reset!)
drop every level and entry WITHOUT running undo.
(trail-reset!)
Since: SX1
Internal definitions
trail/autolink-hint-- internal marker that triggers trail.c compilation in tur build/run.