Files
tricu/notes/tricu-normalization-rules.md

5.9 KiB
Raw Blame History

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 workers 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?