Follow-up to #33, which stopped deep input from aborting but did not make it parse.
Where things stand
Composition is recursive — compose_node → compose_node_from_event → compose_sequence/compose_mapping → compose_node — one stack frame per nesting level. #33 capped the reachable depth at MAX_SAFE_DEPTH (192) so the overflow became a clean DepthLimitExceeded instead of an uncatchable abort.
That is the safety half. The capability half is unaddressed: glaucus cannot parse a document nested deeper than 192, whatever max_depth says.
Why the ceiling is so low
The measurement has a 24x spread, because stack frame size scales with optimisation level:
| build |
stack |
overflows at |
opt-level = 1 |
8 MiB (main thread) |
~7,300 |
opt-level = 1 |
2 MiB (spawned thread) |
~1,850 |
opt-level = 0 |
2 MiB (spawned thread) |
~300 |
192 is sized against the last row — a consumer building glaucus in their own debug profile on a runtime whose workers get Rust's default 2 MiB stack. Any ceiling that keeps the recursion has to assume that case, so no amount of tuning gets this much higher. Only removing the recursion does.
What the rewrite involves
An explicit frame stack, the way the parser already works (Vec<State>, deliberately non-recursive). The delicate part is compose_mapping, which holds ~12 locals live across the recursive call:
entries, and the strict flag
seen_keys — a HashSet lazily promoted past DUP_LINEAR_THRESHOLD
merges — merge-key sources, with explicit-wins-then-earlier-source precedence
- a pending key awaiting its value
All of that has to move into a frame type. The subtle risks are duplicate-key error spans, merge precedence, node-count ordering, and anchor-registration order — none of which the type system protects.
Acceptance criteria
Split from #33 so the safety fix could ship without waiting on a rewrite of the composer's control flow.
Follow-up to #33, which stopped deep input from aborting but did not make it parse.
Where things stand
Composition is recursive —
compose_node→compose_node_from_event→compose_sequence/compose_mapping→compose_node— one stack frame per nesting level. #33 capped the reachable depth atMAX_SAFE_DEPTH(192) so the overflow became a cleanDepthLimitExceededinstead of an uncatchable abort.That is the safety half. The capability half is unaddressed: glaucus cannot parse a document nested deeper than 192, whatever
max_depthsays.Why the ceiling is so low
The measurement has a 24x spread, because stack frame size scales with optimisation level:
opt-level = 1opt-level = 1opt-level = 0192 is sized against the last row — a consumer building glaucus in their own debug profile on a runtime whose workers get Rust's default 2 MiB stack. Any ceiling that keeps the recursion has to assume that case, so no amount of tuning gets this much higher. Only removing the recursion does.
What the rewrite involves
An explicit frame stack, the way the parser already works (
Vec<State>, deliberately non-recursive). The delicate part iscompose_mapping, which holds ~12 locals live across the recursive call:entries, and thestrictflagseen_keys— aHashSetlazily promoted pastDUP_LINEAR_THRESHOLDmerges— merge-key sources, with explicit-wins-then-earlier-source precedenceAll of that has to move into a frame type. The subtle risks are duplicate-key error spans, merge precedence, node-count ordering, and anchor-registration order — none of which the type system protects.
Acceptance criteria
opt-level = 0on a 2 MiB stackMAX_SAFE_DEPTHremoved, or raised to a value justified by measurement rather than by stack budgetNode::clonerecursion) resolved too, or the ceiling stays needed for a different reasonSplit from #33 so the safety fix could ship without waiting on a rewrite of the composer's control flow.