Expand description
Stack headroom for the compiler’s recursive AST passes.
Vize’s middle end walks the template AST with the machine stack: the
transform lane recurses through traverse_node, static hoisting recurses
through its subtree predicates and hoist_static_inner, and codegen recurses
through generate_node. Nesting depth in the template is therefore depth on
the call stack.
That matters because a Rust stack overflow is not a recoverable error. The
guard page faults, the runtime prints fatal runtime error: stack overflow,
and the process is aborted with SIGABRT — taking down a vize CLI run, an
LSP session, or a test binary with it. There is no catch_unwind for it and
no way to turn it into a diagnostic after the fact. A depth limit chosen to
stay under the stack is not a fix either: it only moves the abort to a
different input, and it caps the templates Vize accepts far below what
@vue/compiler-dom accepts (measured: ~1092 levels of <div> on a default
Node stack, after which Vue raises a catchable RangeError).
ensure_sufficient_stack closes that gap. Called at the point where a
recursive pass steps from a node to its children, it checks the remaining
stack and, when the headroom drops below [RED_ZONE], moves the rest of the
recursion onto a freshly allocated [SEGMENT_SIZE] stack. Depth then costs
heap, which is recoverable and bounded by the parser’s nesting limit, instead
of stack, which is not.
§Why not an explicit work stack
An iterative traversal with a heap-allocated worklist is the cleaner shape
and remains the long-term direction for the transform lane, whose recursion
is confined to one function pair. It is not viable for the whole middle end
today: codegen interleaves emission with recursion at ~20 call sites across
ten modules (generate_element emits an opening call, recurses into
children, then emits the closing arguments), so making it iterative means a
continuation-passing rewrite of every one of those sites. Static hoisting
adds four more mutually recursive walks. Guarding the recursion covers all
three passes with one mechanism and no behavioural risk, and it is the same
answer rustc reached for the same problem
(rustc_data_structures::stack::ensure_sufficient_stack).
§Cost
On the shallow templates that dominate real projects the guard never grows
anything: it is one thread-local read plus a pointer comparison per guarded
call, and no segment is ever allocated. Growth only happens once per
[SEGMENT_SIZE] of consumed stack, so a pathologically deep template pays a
handful of stack-segment allocations in total.
Functions§
- ensure_
sufficient_ stack - Run
f, first moving to a fresh stack segment if the current stack is close to exhausted.