5.9 KiB
The takeaway
Consumed data must block recursion. Control data must not drive recursion. Branches with work must be lazy. Top-level fixed points must be hidden behind wrappers. Fixed-format data should be destructured finitely, not sliced recursively.
Rules for normalization-safe tricu
A top-level definition must normalize when its runtime inputs are still abstract. Therefore, avoid any shape where known control data can unfold recursion before the consumed data is available.
1. Put consumed data first
Recursive workers should take the structure they consume before counters, indexes, limits, accumulators, or other control state.
Avoid:
worker index records state
Prefer:
worker records index state
The worker’s first real operation should usually be a case split on the consumed value:
worker_ = (self records state :
lazyList
nilCase
consCase
records)
2. Do not use generic recursive consumers on abstract fixed-format data
Avoid applying helpers like these to abstract values in top-level-normalized definitions:
take n xs
drop n xs
nth n xs
length xs
startsWith? prefix xs
bytesTake n bytes
bytesDrop n bytes
These can be driven by known counters, indexes, lengths, or prefixes while xs is still abstract.
For fixed-format data, use finite destructuring helpers instead:
withNodePayloadForkIndices payload shortK indicesK
hashShard hash
This keeps the recursion bounded by syntax, not by a runtime counter.
3. Use lazy eliminators when a branch contains work
If a branch contains recursion, IO construction, parsing, lookup, response construction, or anything that may recurse internally, do not pass it as an ordinary branch value.
Avoid:
matchBool
resultNow
(self rest state)
cond
Prefer:
lazyBool
(_ : resultNow)
(_ : self rest state)
cond
Same rule for result, maybe, and list elimination:
lazyBool
lazyResult
lazyMaybe
lazyList
Strict eliminators are safe only when both branches are already cheap normal forms.
4. Do not expose top-level fixed points directly
Avoid top-level definitions like:
foo_ = y (self input state : ...)
Prefer the library-style split:
foo_ = (self input state : ...)
foo = (input state :
y foo_ input state)
This prevents each independently-normalized top-level definition from trying to normalize the fixed point itself.
5. Keep recursive self-application small and structurally progressing
Prefer recursive calls shaped like:
self rest nextState
over wide calls like:
self rest index i limit acc flags
Pack non-consumed state into a record/pair if needed.
The consumed argument should visibly progress:
self rest nextState
not restart from the original structure:
self originalRecords newIndex newState
Restarting from the original input inside recursive branches can create residual trees with no obvious structural progress.
6. Recursive state updates must be non-recursive
Do not call a recursive helper while constructing the next recursive state.
Avoid:
self rest (listSnoc acc value)
because listSnoc is itself recursive.
Prefer constant-time constructors:
self rest (pair value acc)
If order matters, reverse later only when the input is concrete, or store explicit indexes in an association list.
7. Do not rebuild from the whole input when a prefix invariant exists
If validation guarantees child references point backward, use that invariant.
Avoid:
buildTree allRecords childIndex
inside the build of each node.
Prefer:
lookup childIndex builtPrefix
For Arboricx nodes, this meant scanning records once left-to-right and resolving children from builtTrees.
8. Make route/path helpers consumed-data-driven
For request paths, hashes, and byte strings, avoid counter/prefix-driven recursive operations over abstract request data.
Avoid:
take 3 hash
drop 23 target
startsWith? prefix target
Prefer:
hashShard hash
stripPrefix prefix target
where the helper case-analyzes the consumed runtime data before recurring.
For fixed small slices like the first three hash bytes, use finite destructuring rather than take.
9. Treat top-level normalization as stricter than runtime evaluation
A function can be semantically correct at runtime and still fail import normalization.
Ask this for every top-level definition:
Can this normalize while all of its arguments are unknown?
If the answer depends on “the branch will not be taken” or “the input will be concrete by then,” the definition is probably not normalization-safe.
10. When a definition hangs alphabetically, inspect reachable dependencies
The alphabetically first hanging definition is not necessarily the root cause. It may simply be the first definition that reaches a later problematic helper.
Debug by replacing reachable branches with constants:
foo = (... : pure notFoundResponse)
Then add back one dependency at a time. If a constant version normalizes, the issue is in reachable branch work, not the wrapper itself.
Compact checklist
Before adding or exporting a definition, check:
1. Does every recursive worker consume unknown data first?
2. Is every recursive branch thunked with lazy eliminators?
3. Is `y` applied inside the public wrapper, not exposed as a top-level worker value?
4. Are recursive self-calls visibly progressing on consumed data?
5. Are recursive state updates constant-time?
6. Are `take`, `drop`, `nth`, `length`, `startsWith?`, or byte slicing used on abstract data?
7. Could a known counter, index, prefix, or length drive recursion?
8. Are fixed-format fields parsed with finite destructuring helpers?
9. Does any branch construct dynamic paths/responses from abstract data using recursive list helpers?
10. Can the definition normalize with all runtime arguments still unknown?