Fully normalized top-level definitions

This commit is contained in:
2026-05-21 11:38:17 -05:00
parent bf30d5945e
commit 4bf2ce56dd
11 changed files with 612 additions and 544 deletions

View File

@@ -1,81 +0,0 @@
# Recursive Consumer Argument Order
## Rule
Put consumed data first in recursive workers in `tricu` code.
*AVOID* this shape:
```text
worker control state input
```
*USE* this shape:
```text
worker input control state
```
The consumed structure should block recursion when it is unknown. Counters, indexes, lengths, and accumulator state should not be able to drive recursion over abstract input.
## Bad shape
The original `readBytes_` worker put loop-control arguments before the byte stream:
```tricu
readBytes_ = y (self n i bs original acc :
matchBool
(ok (reverse acc) bs)
(matchResult
(code rest : err code original)
(actual rest :
self n (succ i) rest original (pair actual acc))
(readU8 bs))
(equal? i n))
readBytes = (n bs : readBytes_ n 0 bs bs t)
```
With a partial application like:
```tricu
readBytes 2
```
the evaluator knows `n = 2` and `i = 0`, but `bs` is still abstract. That lets the counter check drive recursive specialization before the byte stream is available, which can build a huge symbolic residual tree. This has been proven; do not reason about it further.
## Good shape
The corrected worker takes the byte stream first and immediately case-analyzes it:
```tricu
readBytes_ = y (self bs n i original acc :
matchList
(matchBool
(ok (reverse acc) bs)
(err errUnexpectedEof original)
(equal? i n))
(h r :
matchBool
(ok (reverse acc) bs)
(self r n (succ i) original (pair h acc))
(equal? i n))
bs)
readBytes = (n bs : readBytes_ bs n 0 bs t)
```
Now:
```tricu
readBytes 2
```
becomes a function waiting on `bs`. Since the worker immediately performs `matchList ... bs`, evaluation blocks on the missing input instead of unrolling the counter loop.
## Takeaway
```text
Let consumed data drive recursion.
Do not let counters unroll over abstract input.
```

View File

@@ -0,0 +1,248 @@
# 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:
```tricu
worker index records state
```
Prefer:
```tricu
worker records index state
```
The workers first real operation should usually be a case split on the consumed value:
```tricu
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:
```tricu
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:
```tricu
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:
```tricu
matchBool
resultNow
(self rest state)
cond
```
Prefer:
```tricu
lazyBool
(_ : resultNow)
(_ : self rest state)
cond
```
Same rule for result, maybe, and list elimination:
```tricu
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:
```tricu
foo_ = y (self input state : ...)
```
Prefer the library-style split:
```tricu
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:
```tricu
self rest nextState
```
over wide calls like:
```tricu
self rest index i limit acc flags
```
Pack non-consumed state into a record/pair if needed.
The consumed argument should visibly progress:
```tricu
self rest nextState
```
not restart from the original structure:
```tricu
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:
```tricu
self rest (listSnoc acc value)
```
because `listSnoc` is itself recursive.
Prefer constant-time constructors:
```tricu
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:
```tricu
buildTree allRecords childIndex
```
inside the build of each node.
Prefer:
```tricu
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:
```tricu
take 3 hash
drop 23 target
startsWith? prefix target
```
Prefer:
```tricu
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:
```text
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:
```tricu
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:
```text
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?
```