""" Lexical environment for s-expression evaluation. Environments form a parent chain so inner scopes shadow outer ones while still allowing lookup of free variables. """ from __future__ import annotations from typing import Any class Env: """A lexical scope mapping names → values with an optional parent.""" __slots__ = ("_bindings", "_parent") def __init__( self, bindings: dict[str, Any] | None = None, parent: Env | None = None, ): self._bindings: dict[str, Any] = {} if bindings is None else bindings self._parent = parent # -- lookup ------------------------------------------------------------- def lookup(self, name: str) -> Any: """Resolve *name*, walking the parent chain. Raises ``KeyError`` if not found. """ if name in self._bindings: return self._bindings[name] if self._parent is not None: return self._parent.lookup(name) raise KeyError(name) def __contains__(self, name: str) -> bool: if name in self._bindings: return True if self._parent is not None: return name in self._parent return False def __getitem__(self, name: str) -> Any: return self.lookup(name) def __setitem__(self, name: str, value: Any) -> None: """Set *name* in the **current** scope (like ``define``).""" self._bindings[name] = value def get(self, name: str, default: Any = None) -> Any: try: return self.lookup(name) except KeyError: return default def update(self, other: dict[str, Any] | Env) -> None: """Merge *other*'s bindings into the **current** scope.""" if isinstance(other, Env): self._bindings.update(other._bindings) else: self._bindings.update(other) def keys(self): """All keys visible from this scope (current + parents).""" return self.to_dict().keys() def __iter__(self): return iter(self.to_dict()) # -- mutation ----------------------------------------------------------- def define(self, name: str, value: Any) -> None: """Bind *name* in the **current** scope.""" self._bindings[name] = value def set(self, name: str, value: Any) -> None: """Update *name* in the **nearest enclosing** scope that contains it. Raises ``KeyError`` if the name is not bound anywhere. """ if name in self._bindings: self._bindings[name] = value elif self._parent is not None: self._parent.set(name, value) else: raise KeyError(f"Cannot set! undefined variable: {name}") # -- construction ------------------------------------------------------- def extend(self, bindings: dict[str, Any] | None = None) -> Env: """Return a child environment.""" return Env({} if bindings is None else bindings, parent=self) # -- conversion --------------------------------------------------------- def to_dict(self) -> dict[str, Any]: """Flatten the full chain into a single dict (parent first).""" if self._parent is not None: d = self._parent.to_dict() else: d = {} d.update(self._bindings) return d def __repr__(self) -> str: keys = list(self._bindings.keys()) depth = 0 p = self._parent while p: depth += 1 p = p._parent return f"" class MergedEnv(Env): """Env with two parent chains: primary (closure) and secondary (caller). Reads walk: local bindings → primary chain → secondary chain. set! walks: local bindings → primary chain (skips secondary). This allows set! to modify variables in the defining scope (closure) without being confused by overlay copies from the calling scope. """ __slots__ = ("_secondary",) def __init__( self, bindings: dict[str, Any] | None = None, primary: Env | None = None, secondary: Env | None = None, ): super().__init__(bindings, parent=primary) self._secondary = secondary def lookup(self, name: str) -> Any: try: return super().lookup(name) except KeyError: if self._secondary is not None: return self._secondary.lookup(name) raise def __contains__(self, name: str) -> bool: if super().__contains__(name): return True if self._secondary is not None: return name in self._secondary return False def get(self, name: str, default: Any = None) -> Any: try: return self.lookup(name) except KeyError: return default def to_dict(self) -> dict[str, Any]: if self._secondary is not None: d = self._secondary.to_dict() else: d = {} if self._parent is not None: d.update(self._parent.to_dict()) d.update(self._bindings) return d def extend(self, bindings: dict[str, Any] | None = None) -> Env: return Env(bindings or {}, parent=self)