Both executors give a sketch block's body its own environment whose parent is the current environment, via a snapshot: the recursive executor in Node<SketchBlock>::get_result (exec_ast.rs, snapshot + push_new_env_for_call), the machine in sketch_block_body_setup (machine.rs). snapshot() sets might_be_refed on the parent environment, the flag is never cleared, and Environment::compact() skips any env with the flag set. So the frame enclosing a sketch block is retained with all its bindings for the life of the program, even when nothing escaped the block.
Impact today is low: sketch blocks almost always appear at a module's top level, where the parent is the root/module environment, which is retained anyway. It becomes a real per-call leak if sketch blocks inside functions become common (each call's parameters and locals would be retained).
Fix: replace the snapshot() + push_new_env_for_call(snapshot) pattern in both executors' sketch-block setup (Node<SketchBlock>::get_result in exec_ast.rs, sketch_block_body_setup in machine.rs) with push_new_env_for_block, which defers marking the parent until the block's environment is itself referenced (added for KCL 3.0 if-arm scoping in #13535). Note this applies to both environments the sketch block pushes — the sketch-alias env created via prep_mem's snapshot and the body env. Both are popped through the arena's common pop path, so the deferred pin applies automatically once the pushes are swapped. Verify sketch-mode re-execution still works: cached environment refs held across runs (SketchModeState) must mark the envs referenced for the deferred pin to keep their parent chains alive. A regression test should assert, via the arena's test-only envs_with_bindings, that repeated calls to a function containing a sketch block with no escaping references don't accumulate retained frames.
Both executors give a sketch block's body its own environment whose parent is the current environment, via a snapshot: the recursive executor in
Node<SketchBlock>::get_result(exec_ast.rs, snapshot +push_new_env_for_call), the machine insketch_block_body_setup(machine.rs).snapshot()setsmight_be_refedon the parent environment, the flag is never cleared, andEnvironment::compact()skips any env with the flag set. So the frame enclosing a sketch block is retained with all its bindings for the life of the program, even when nothing escaped the block.Impact today is low: sketch blocks almost always appear at a module's top level, where the parent is the root/module environment, which is retained anyway. It becomes a real per-call leak if sketch blocks inside functions become common (each call's parameters and locals would be retained).
Fix: replace the
snapshot()+push_new_env_for_call(snapshot)pattern in both executors' sketch-block setup (Node<SketchBlock>::get_resultinexec_ast.rs,sketch_block_body_setupinmachine.rs) withpush_new_env_for_block, which defers marking the parent until the block's environment is itself referenced (added for KCL 3.0 if-arm scoping in #13535). Note this applies to both environments the sketch block pushes — the sketch-alias env created viaprep_mem's snapshot and the body env. Both are popped through the arena's common pop path, so the deferred pin applies automatically once the pushes are swapped. Verify sketch-mode re-execution still works: cached environment refs held across runs (SketchModeState) must mark the envs referenced for the deferred pin to keep their parent chains alive. A regression test should assert, via the arena's test-onlyenvs_with_bindings, that repeated calls to a function containing a sketch block with no escaping references don't accumulate retained frames.