pydantic/monty · error

bubble-up captured '{name_str}' that is bound nowhere — scop

Error message

bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug

What it means

When a child scope's capture is bubbled up to its parent during scope finalization, the captured name must exist in the parent's cell vars or enclosing locals. If it is bound in neither, preparation panics with a message explicitly flagging a scope-analysis bug — the capture should never have been predicted.

Source

Thrown at crates/monty/src/prepare.rs:442

        }

        let PrepareState::Function(state) = &mut self.state else {
            return Ok(());
        };

        if state.cell_var_map.contains_key(&captured_name) || state.free_var_map.contains_key(&captured_name) {
            return Ok(());
        }

        if state.assigned_names.contains(&captured_name) || state.locals.contains(captured_name) {
            let slot = state.locals.ensure_slot(captured_name, position)?;
            state.cell_var_map.insert(captured_name, slot);
        } else if state.enclosing_locals.contains(&captured_name) {
            let slot = state.locals.ensure_slot(captured_name, position)?;
            state.free_var_map.insert(captured_name, slot);
        } else {
            let name_str = self.interner.get_str(captured_name);
            panic!("bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug");
        }
        Ok(())
    }

    /// Builds the parallel free-var slot vectors for a just-prepared child
    /// scope from its `free_var_map` (`name -> the child's own slot`).
    ///
    /// Returns `(free_var_slots, free_var_enclosing_slots)`: the first holds the
    /// child's own slots (where it installs each captured cell at call time);
    /// the second holds OUR slot it reads that cell from when the closure is
    /// built (via [`Self::lookup_captured_slot`]). Both are ordered by the
    /// child slot so they stay index-aligned.
    fn build_free_var_slots(
        &mut self,
        inner_free_var_map: AHashMap<StringId, NamespaceId>,
    ) -> (Vec<NamespaceId>, Vec<CaptureSource>) {
        let mut entries: Vec<_> = inner_free_var_map.into_iter().collect();
        entries.sort_by_key(|(_, inner_slot)| *inner_slot);

View on GitHub (pinned to adc986b362)

Solutions

  1. Fix capture prediction so only names actually bound in the chain are marked captured.
  2. Check that cell_var_map and enclosing_locals are merged from ancestor scopes before finalization.
  3. Reproduce with a minimal Python snippet and compare against CPython's closure behavior to find the missing binding site.

Example fix

// before
panic!("bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug");
// after
// skip the spurious capture and let name resolution fall back to globals
return Ok(()); // once prediction is fixed to not emit it
Defensive patterns

Strategy: validation

Validate before calling

// guard before bubbling up a capture
assert!(state.cell_var_map.contains_key(&captured_name) || state.enclosing_locals.contains(&captured_name));

Prevention

When it happens

Trigger: Finalizing a nested scope whose captured name the parent never bound: e.g. the analysis recorded a capture from a pass that ran before the parent's binding information was complete, or a name is bound only in a scope not on the capture chain.

Common situations: Contributors editing finalize_child_scope or the capture-prediction logic; test cases with deeply nested closures and class scopes where capture chains are hard to predict.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/ffb0e628c8b52640. Report an issue: GitHub.