Module refinery.lib.scripts.js.analysis

Static-analysis substrate for JavaScript deobfuscation. Transforms query a shared, computed model of the program here instead of each re-deriving scope, binding, and dataflow facts on their own.

The foundation is refinery.lib.scripts.js.analysis.model, a flow-insensitive lexical model of scopes and resolved bindings. Later layers (control-flow graphs, effect summaries) attach behind the same representation-agnostic surface.

Expand source code Browse git
"""
Static-analysis substrate for JavaScript deobfuscation. Transforms query a shared, computed model of
the program here instead of each re-deriving scope, binding, and dataflow facts on their own.

The foundation is `model`, a flow-insensitive lexical model of scopes
and resolved bindings. Later layers (control-flow graphs, effect summaries) attach behind the same
representation-agnostic surface.
"""
from __future__ import annotations

from refinery.lib.scripts.js.analysis.cfg import (
    CfgNode,
    ControlFlowGraph,
    ControlFlowModel,
    build_cfg,
    build_control_flow,
)
from refinery.lib.scripts.js.analysis.effects import (
    EffectModel,
    EffectSummary,
    build_effects,
)
from refinery.lib.scripts.js.analysis.liveness import (
    LivenessModel,
    build_liveness,
)
from refinery.lib.scripts.js.analysis.model import (
    Binding,
    BindingKind,
    Role,
    Scope,
    ScopeKind,
    SemanticModel,
    build_semantic_model,
    is_use_position,
    pattern_identifiers,
    reference_role,
)

__all__ = [
    'Binding',
    'BindingKind',
    'CfgNode',
    'ControlFlowGraph',
    'ControlFlowModel',
    'EffectModel',
    'EffectSummary',
    'LivenessModel',
    'Role',
    'Scope',
    'ScopeKind',
    'SemanticModel',
    'build_cfg',
    'build_control_flow',
    'build_effects',
    'build_liveness',
    'build_semantic_model',
    'is_use_position',
    'pattern_identifiers',
    'reference_role',
]

Sub-modules

refinery.lib.scripts.js.analysis.assignment

Definite assignment for implicit globals: a forward must-analysis over the per-function control-flow graphs answering whether a write to a name the …

refinery.lib.scripts.js.analysis.cache

A per-run cache of the JavaScript analysis models. The deobfuscation pipeline builds one cache over the script being transformed and shares it across …

refinery.lib.scripts.js.analysis.cfg

JavaScript's contribution to the shared control-flow substrate: which node types are which control-flow shape, and where the parts of each are stored …

refinery.lib.scripts.js.analysis.conversions

Where a JavaScript program may give a function a conversion of its own from code that does not name the function. Converting a function (String(W), …

refinery.lib.scripts.js.analysis.dominance

Dominance over the per-function control-flow graphs of the SemanticModel. One node dominates another when …

refinery.lib.scripts.js.analysis.effects

Per-function effect summaries for JavaScript, computed over the SemanticModel's resolved bindings and call …

refinery.lib.scripts.js.analysis.environment

The host environment a script is assumed to run in, and the set of global names that environment guarantees a bare read resolves rather than throwing …

refinery.lib.scripts.js.analysis.evaluation

When each part of a JavaScript node is evaluated, relative to the node holding it …

refinery.lib.scripts.js.analysis.intrinsics

What a JavaScript program writes on the intrinsics, computed once per SemanticModel. build_intrinsic_writes …

refinery.lib.scripts.js.analysis.liveness

Flow-sensitive live-variable analysis for JavaScript, computed over the per-function control-flow graphs and the resolved bindings of the …

refinery.lib.scripts.js.analysis.localization

Whether a script-scope var behaves as a local of one function, so that its declaration can move into that function: obfuscators hoist a function's …

refinery.lib.scripts.js.analysis.model

A lexical semantic model for JavaScript: a tree of scopes with resolved bindings and def/use sets, computed once over an AST and then queried by …

refinery.lib.scripts.js.analysis.reaching

Reaching-value queries for JavaScript inlining, over the per-function control-flow graphs of the …

refinery.lib.scripts.js.analysis.tampering

The anchored tampering oracle: whether the built-ins a fold or an interpreted execution is about to trust still mean what the language says at one …

Functions

def build_cfg(owner)

Build the control-flow graph of owner, a JsScript or a function node, over its own body without descending into nested function bodies.

Expand source code Browse git
def build_cfg(owner: Node) -> ControlFlowGraph:
    """
    Build the control-flow graph of *owner*, a `refinery.lib.scripts.js.model.JsScript` or a
    function node, over its own body without descending into nested function bodies.
    """
    return _Builder(owner).build()
def build_control_flow(root)

Build one control-flow graph for the script and one for each run owner, a function or a field with an initializer (owns_a_run()), keyed by the owner node's identity.

Expand source code Browse git
def build_control_flow(root: JsScript) -> dict[int, ControlFlowGraph]:
    """
    Build one control-flow graph for the script and one for each run owner, a function or a field
    with an initializer (`refinery.lib.scripts.js.analysis.evaluation.owns_a_run`), keyed by the
    owner node's identity.
    """
    return _build_control_flow(root, _Builder, owns_a_run)
def build_effects(model, dominance=None, intrinsic_writes=None, settlement=None, refusals=None)

Build the EffectModel for a script's SemanticModel, sharing dominance when the caller holds one so the two models order the same tree through one control-flow layer. A None is not a refusal: the model builds its own the first time a summary must order a binding's temporal dead zone. intrinsic_writes is shared the same way, so the effect and ordering layers read one scan of what the program writes on the intrinsics; a None builds it here. settlement is the record of the deobfuscation run the model serves, which EffectModel.function_conversion_untouched() reads; without one that question refuses wherever it would wait for the run to settle. refusals is that run's record of the questions EffectModel.builtin_writes_followed() answered no.

Expand source code Browse git
def build_effects(
    model: SemanticModel,
    dominance: DominanceModel | None = None,
    intrinsic_writes: IntrinsicWrites | None = None,
    settlement: ConversionSettlement | None = None,
    refusals: UnfollowedWriteRefusals | None = None,
) -> EffectModel:
    """
    Build the `EffectModel` for a script's `refinery.lib.scripts.js.analysis.model.SemanticModel`,
    sharing *dominance* when the caller holds one so the two models order the same tree through one
    control-flow layer. A `None` is not a refusal: the model builds its own the first time a summary
    must order a binding's temporal dead zone. *intrinsic_writes* is shared the same way, so the
    effect and ordering layers read one scan of what the program writes on the intrinsics; a `None`
    builds it here. *settlement* is the record of the deobfuscation run the model serves, which
    `EffectModel.function_conversion_untouched` reads; without one that question refuses wherever
    it would wait for the run to settle. *refusals* is that run's record of the questions
    `EffectModel.builtin_writes_followed` answered no.
    """
    return EffectModel(model, dominance, intrinsic_writes, settlement, refusals)
def build_liveness(model, control_flow=None)

Build the LivenessModel for a script's SemanticModel, reusing control_flow when the caller has one to share, or building a fresh one when it is None.

Expand source code Browse git
def build_liveness(
    model: SemanticModel, control_flow: ControlFlowModel | None = None,
) -> LivenessModel:
    """
    Build the `LivenessModel` for a script's `refinery.lib.scripts.js.analysis.model.SemanticModel`,
    reusing *control_flow* when the caller has one to share, or building a fresh one when it is `None`.
    """
    return LivenessModel(model, control_flow)
def build_semantic_model(root, environment=HostEnvironment.universal, trust_eval=False, file_kind=FileKind.script, exports_read_outside=True, host_entrypoint=None)

Build the SemanticModel for a parsed script, resolving bare global reads against environment. The default universal environment asserts only GUARANTEED_GLOBALS, so the model answers read_may_throw exactly as an unpinned run; a pinned host recovers the reads that host guarantees. trust_eval selects the trusting model, which assumes code supplied as data is inert; see DeobfuscationOptions. file_kind is what the file is, which file_kind() decides for a run; text that runs as global code, such as the payload of a Function construction, is a script. exports_read_outside says whether code that loads the file may read what it exports; see SemanticModel.module_exports_unread(). host_entrypoint says whether the analyst declared a name one a host reaches; see SemanticModel.host_reaches_global(). The default declares none, and a host then reaches only the event handlers environment calls.

Expand source code Browse git
def build_semantic_model(
    root: JsScript,
    environment: HostEnvironment = HostEnvironment.universal,
    trust_eval: bool = False,
    file_kind: FileKind = FileKind.script,
    exports_read_outside: bool = True,
    host_entrypoint: Callable[[str], bool] | None = None,
) -> SemanticModel:
    """
    Build the `SemanticModel` for a parsed script, resolving bare global reads against *environment*.
    The default `universal` environment asserts only `GUARANTEED_GLOBALS`, so the model answers
    `read_may_throw` exactly as an unpinned run; a pinned host recovers the reads that host guarantees.
    *trust_eval* selects the trusting model, which assumes code supplied as data is inert; see
    `refinery.lib.scripts.js.options.DeobfuscationOptions`. *file_kind* is what the file is, which
    `refinery.lib.scripts.js.options.file_kind` decides for a run; text that runs as global code,
    such as the payload of a `Function` construction, is a script. *exports_read_outside* says
    whether code that loads the file may read what it exports; see
    `SemanticModel.module_exports_unread`. *host_entrypoint* says whether the analyst declared a
    name one a host reaches; see `SemanticModel.host_reaches_global`. The default declares none, and
    a host then reaches only the event handlers *environment* calls.
    """
    return SemanticModel(
        root,
        environment,
        trust_eval,
        file_kind,
        exports_read_outside,
        host_entrypoint,
    )
def is_use_position(node)

Whether an identifier occupies a position where it reads or writes a value, as opposed to naming a property, a key, a label, or something across a module boundary. names_a_property answers for every position that names what a value carries, the far side of a module boundary among them; what is added here is the two positions that name something else the program cannot refer to: a label, and the binding an import creates. The local half of an export list without a from clause reads the binding it names, which is why an engine refuses to link export { a }; where nothing declares a; where nothing renames, one node fills both halves of the specifier, and that node is the local half and reads. Binding sites are not excluded here; SemanticModel.is_reference() is the binding-aware predicate that also excludes them.

Expand source code Browse git
def is_use_position(node: JsIdentifier) -> bool:
    """
    Whether an identifier occupies a position where it reads or writes a value, as opposed to naming
    a property, a key, a label, or something across a module boundary. `names_a_property` answers
    for every position that names what a value carries, the far side of a module boundary among
    them; what is added here is the two positions that name something else the program cannot
    refer to: a label, and the binding an import creates. The local half of an export list without
    a `from` clause reads the binding it names, which is why an engine refuses to link
    `export { a };` where nothing declares `a`; where nothing renames, one node fills both halves
    of the specifier, and that node is the local half and reads. Binding sites are not excluded
    here; `SemanticModel.is_reference` is the binding-aware predicate that also excludes them.
    """
    p = node.parent
    if p is None:
        return False
    if names_a_property(node):
        return False
    if isinstance(p, (JsBreakStatement, JsContinueStatement, JsLabeledStatement)) and p.label is node:
        return False
    if isinstance(p, (
        JsImportSpecifier,
        JsImportDefaultSpecifier,
        JsImportNamespaceSpecifier,
    )):
        return False
    return True
def pattern_identifiers(target)

Yield every binding-site identifier introduced by a declaration target. A member-expression target ([a.b] = ...) introduces no binding and yields nothing, and neither does a binding position the parser could not read, which pattern_binds_unread_names is what reports.

Expand source code Browse git
def pattern_identifiers(target: Node | None) -> Iterator[JsIdentifier]:
    """
    Yield every binding-site identifier introduced by a declaration target. A member-expression
    target (`[a.b] = ...`) introduces no binding and yields nothing, and neither does a binding
    position the parser could not read, which `pattern_binds_unread_names` is what reports.
    """
    for node in pattern_targets(target):
        if isinstance(node, JsIdentifier):
            yield node
def reference_role(node)

Classify how a reference touches its binding: a plain read, a write-only target (the left of a simple =, including inside a destructuring pattern or a destructuring default, or a for-in/for-of head), or a read-and-write (compound assignment, ++/--, or a delete, each of which keeps the name live as a read rather than overwriting it outright). The shared _governing_target climb looks through destructuring containers, default patterns, and parentheses, so a target nested in a pattern or a grouping ([x = 9] = xs, (x)++, (o) = v) is still recognized as a write. The reference is usually an identifier, but the same rules classify the node an object aliasing the binding was reached through — a member access on a global-object alias (globalThis.g, globalThis.g = ...), and the global object itself where a call is handed it — so the def-use pass records each as the read or write it is.

Expand source code Browse git
def reference_role(node: ReferenceNode) -> Role:
    """
    Classify how a reference touches its binding: a plain read, a write-only target (the left of a
    simple `=`, including inside a destructuring pattern or a destructuring default, or a
    `for-in`/`for-of` head), or a read-and-write (compound assignment, `++`/`--`, or a `delete`, each
    of which keeps the name live as a read rather than overwriting it outright). The shared
    `_governing_target` climb looks through destructuring containers, default patterns, and
    parentheses, so a target nested in a pattern or a grouping (`[x = 9] = xs`, `(x)++`, `(o) = v`) is
    still recognized as a write. The reference is usually an identifier, but the same rules classify
    the node an object aliasing the binding was reached through — a member access on a global-object
    alias (`globalThis.g`, `globalThis.g = ...`), and the global object itself where a call is handed
    it — so the def-use pass records each as the read or write it is.
    """
    governor, target = _governing_target(node)
    if isinstance(governor, JsAssignmentExpression) and strip_parens(governor.left) is target:
        return Role.WRITE if governor.operator == '=' else Role.READWRITE
    if isinstance(governor, JsUpdateExpression) and strip_parens(governor.argument) is target:
        return Role.READWRITE
    if (
        isinstance(governor, JsUnaryExpression)
        and governor.operator == 'delete'
        and strip_parens(governor.operand) is target
    ):
        return Role.READWRITE
    if isinstance(governor, (JsForInStatement, JsForOfStatement)) and strip_parens(governor.left) is target:
        return Role.WRITE
    return Role.READ

Classes

class Binding (name, kind, scope, declarations=<factory>, reads=<factory>, writes=<factory>, dynamic_refs=<factory>, indefinite_writes=<factory>, captured=False, written_at_entry=False, reachable_through_a_handed_object=False, exported=False, copies_made=<factory>, copies_received=<factory>, supplied_by_host=False, may_be_absent=False)

A single declared name within one scope. declarations holds the binding-site identifier nodes that introduce the name; reads and writes hold the referencing identifiers that read and write it (a compound assignment or update appears in both). captured is set when the name is referenced from a function nested below the one that owns it. A read or write performed through an object that aliases the binding has no referencing identifier for the name it targets, so the JsMemberExpression stands in for that reference; every other reads/writes entry is an identifier. Two objects alias this way — a global-object alias (globalThis.g) reaching a global, and a mapped arguments reaching a parameter — and they are told apart by what the access is on, never by the entry being a member access at all. dynamic_refs holds referencing identifiers a dynamic scope resolves at runtime — a name inside a with body that could denote this binding — which reads/writes omit because such a name resolves to no binding statically; its target is uncertain, so it is kept apart from the definite references. It also holds the references to a binding that may be absent which the reading without that binding resolves to this one (Binding.may_be_absent): such a reference does resolve statically, to the other binding.

Expand source code Browse git
@dataclass(eq=False)
class Binding:
    """
    A single declared name within one scope. `declarations` holds the binding-site identifier nodes
    that introduce the name; `reads` and `writes` hold the referencing identifiers that read and write
    it (a compound assignment or update appears in both). `captured` is set when the name is referenced
    from a function nested below the one that owns it. A read or write performed through an object that
    aliases the binding has no referencing identifier for the name it targets, so the
    `JsMemberExpression` stands in for that reference; every other `reads`/`writes` entry is an
    identifier. Two objects alias this way — a global-object alias (`globalThis.g`) reaching a global,
    and a mapped `arguments` reaching a parameter — and they are told apart by what the access is on,
    never by the entry being a member access at all. `dynamic_refs` holds referencing identifiers a dynamic
    scope resolves at runtime — a name inside a `with` body that could denote this binding — which
    `reads`/`writes` omit because such a name resolves to no binding statically; its target is
    uncertain, so it is kept apart from the definite references. It also holds the references to a
    binding that may be absent which the reading without that binding resolves to this one
    (`Binding.may_be_absent`): such a reference does resolve statically, to the other binding.
    """
    name: str
    kind: BindingKind
    scope: Scope
    declarations: list[JsIdentifier] = field(default_factory=list)
    reads: list[ReferenceNode] = field(default_factory=list)
    writes: list[ReferenceNode] = field(default_factory=list)
    dynamic_refs: list[JsIdentifier] = field(default_factory=list)
    indefinite_writes: list[ReferenceNode] = field(default_factory=list)
    captured: bool = False
    #: Whether the call writes this binding before any statement of its scope runs. A `var` of a
    #: parameter's name is the one shape that does: the body's name starts out holding the argument,
    #: and only a declarator that runs later says anything about what it holds after that. There is
    #: no node for that write - the call makes it, not anything in the text - so it can be neither a
    #: `writes` nor an `indefinite_writes` entry, both of which every consumer orders by position.
    written_at_entry: bool = False
    #: Whether the program hands a call the object that carries this binding, so a body no reading of
    #: the text follows can name it. The references such a call may make are recorded like any other,
    #: and this says the one thing they cannot: that a walk which finds a name only where the text
    #: spells it is looking at less than the whole program. Only a global is ever carried this way.
    reachable_through_a_handed_object: bool = False
    #: Whether the binding is exported, so an importer observes its value across the module boundary
    #: after the module runs. Like the two flags above, it names an observer no reading of the text
    #: reaches: its declaration must be kept and never relocated out of module scope, and a write to
    #: it is never a dead store, because the final value is read from outside. `export var a`,
    #: `export function`/`class`, and the local half of a sourceless `export { a }` all set it; a
    #: `from`-clause list and a re-export name a binding of another module and set nothing here.
    exported: bool = False
    #: The copies Annex B makes of this block binding of a function declared in a block, one for
    #: each declaration of it B.3.3.1 does not stop, each into the same `var` outside the block.
    copies_made: list[AnnexBCopy] = field(default_factory=list)
    #: The copies Annex B makes into this `var` from the block bindings of functions declared in
    #: blocks of its function, script or static block.
    copies_received: list[AnnexBCopy] = field(default_factory=list)
    #: Whether the host supplies this binding: a parameter or the `arguments` of the function a
    #: CommonJS host wraps around the text of the file, which holds what the host passed from the
    #: moment the file starts to run. A `var` or function declaration of the file joins it, as one
    #: in the body of a function joins a parameter.
    supplied_by_host: bool = False
    #: Whether the binding exists in one way the file may run and not in another, so that a
    #: reference to it may find no binding at all: a binding the host supplies where the file may
    #: also be an ES module, which binds none of them, unless a declaration of the file joins it,
    #: and a `var` only conditional copies declare (`AnnexBCopy.conditional`).
    may_be_absent: bool = False

    def note_reference_from(self, scope: Scope | None) -> None:
        """
        Mark this binding captured where *scope* is on the far side of a closure boundary from the
        scope declaring it, which is what a reference made from a scope with a different variable
        scope is. A reference whose own scope is not known is counted as a capture, since nothing
        about it says that it is not one.

        Three walks record a reference and each of them asks this: the identifier walk, the one
        reading a binding through an alias of the global object, and the one reading a parameter
        through a mapped `arguments`. They have to agree, and one of them being written differently
        from the others is not a difference anything downstream could act on.
        """
        if scope is None or scope.closure_home is not self.scope.closure_home:
            self.captured = True

    def declared_within(self, node: Node) -> bool:
        """
        Whether the scope declaring this binding is introduced by *node* or by a node inside it, so
        that the binding belongs to *node*: a copy of *node* carries it along, and a name *node*
        reads that resolves elsewhere comes from outside.
        """
        owner = self.scope.node
        return owner is node or owner.is_descendant_of(node)

    @property
    def is_read(self) -> bool:
        """
        Whether the binding's value is ever read.
        """
        return bool(self.reads)

    @property
    def is_hoisted(self) -> bool:
        """
        Whether the binding is hoisted to the top of its variable scope — a `var` or a function
        declaration — and so is visible (as `undefined`, or the function) throughout that scope before
        its textual position, rather than sitting in a temporal dead zone.
        """
        return self.kind in (BindingKind.VAR, BindingKind.FUNCTION)

    @property
    def is_lexical(self) -> bool:
        """
        Whether the binding is block-scoped in a declarative environment — a `let`, `const`, or
        `class`. Defined positively: a parameter, catch binding, import, or implicit global is neither
        hoisted nor lexical in this sense.
        """
        return self.kind in (BindingKind.LET, BindingKind.CONST, BindingKind.CLASS)

    @property
    def is_dead(self) -> bool:
        """
        Whether no use observes the binding's value: it is read through no resolved reference, named
        inside no dynamic scope, and not exported. Definitions of a dead binding can be removed if they
        carry no other side effect (which the caller decides). A name a `with` body could read is not
        dead even though `reads` is empty — the dynamic reference may observe it at runtime — nor is an
        exported one, whose value an importer reads across the module boundary, so removers need not
        rely on a separate reflection gate to keep such a binding.
        """
        return not self.reads and not self.dynamic_refs and not self.exported

    @property
    def has_indefinite_write(self) -> bool:
        """
        Whether some access writes the binding at a point where what it stores, or whether it stores
        at all, is decided only at run time, so that its value stops holding there and no definition
        says what replaced it. Every write through a mapped `arguments` object is one. `arguments[k]
        = v` for a `k` no reading of the text computes writes exactly one parameter of the function
        and which one is not decidable, so it is a kill of each with a value for none; `arguments[0]
        = v` names its parameter but still lands only where the call supplied that argument
        (§10.2.11 maps an element onto a parameter only for a position `index < len`), so it is a
        kill of that one with a value for none. The object handed to a call or bound to a second
        name is another, and the entry is then the identifier the object escaped through rather than
        an access on it.

        It is kept apart from `writes` for the same reason `dynamic_refs` is kept apart: a `writes`
        entry is a definition, and a consumer reading one expects to find the value it stored. Recording
        this as a definition of every parameter would let a fold answer with a value only one of them
        can hold; recording it nowhere lets a fold carry a value across it that the write destroyed.

        The write a call makes on entry is one of these too, and it is the one with no node at all,
        so it is carried by `written_at_entry` and read here beside the rest.
        """
        return bool(self.indefinite_writes) or self.written_at_entry

    @property
    def has_global_member_write(self) -> bool:
        """
        Whether the binding is written through a member access on a global-object alias
        (`globalThis.x = ...`), recorded as a `JsMemberExpression` write site rather than a referencing
        identifier (see the class docstring). Only a global ever carries such a write, so the answer is
        always false for a lexical binding.

        The access is tested by what it is on and not by its being a member access, because a parameter
        of a sloppy function carries member-access writes too — through the `arguments` object that
        aliases it — and those reach one function's own parameter rather than the global object.
        """
        return any(_is_global_alias_access(write) for write in self.writes)

    @property
    def has_member_reference(self) -> bool:
        """
        Whether the binding is read or written through a member access on a global-object alias
        (`globalThis.x`), recorded as a `JsMemberExpression` reference rather than a referencing
        identifier (see the class docstring). Such a binding is reachable through the global object, so
        a caller must not treat it as an ordinary local — it cannot be relocated into a function.

        As with `has_global_member_write`, an access through a mapped `arguments` object is not one of
        these: it reaches a parameter, which no other function can name.
        """
        return any(_is_global_alias_access(ref) for ref in (*self.reads, *self.writes))

    @property
    def store_sites(self) -> list[ReferenceNode]:
        """
        Every node whose execution stores into the binding, whatever it stores: each recorded write,
        and each declaration that stores where it runs (`_where_it_stores`). A `let`, `const` or
        `class` declaration is one even without a value, since it ends the binding's temporal dead
        zone. A `var` has none, so a bare `var x;` stores nothing; its declarator stores where it
        carries a value, a destructuring pattern included, or stands in the head of a `for-in` or
        `for-of` loop, which stores on every iteration. A function declaration Annex B copies into
        the `var` stores where it runs, and is listed itself rather than its name, which lies inside
        the function it names. A store made before any statement of the binding's scope runs has no
        node and is not listed: a parameter's, or the function a declaration hoists. Ordering the
        sites against a point is the caller's.
        """
        sites: list[ReferenceNode] = list(self.writes)
        for declaration in self.declarations:
            if _where_it_stores(self, declaration) is not _DeclarationStore.WHERE_IT_RUNS:
                continue
            if isinstance(function := declaration.parent, JsFunctionDeclaration):
                sites.append(function)
            else:
                sites.append(declaration)
        return sites

Instance variables

var name

The type of the None singleton.

var kind

The type of the None singleton.

var scope

The type of the None singleton.

var declarations

The type of the None singleton.

var reads

The type of the None singleton.

var writes

The type of the None singleton.

var dynamic_refs

The type of the None singleton.

var indefinite_writes

The type of the None singleton.

var copies_made

The copies Annex B makes of this block binding of a function declared in a block, one for each declaration of it B.3.3.1 does not stop, each into the same var outside the block.

var copies_received

The copies Annex B makes into this var from the block bindings of functions declared in blocks of its function, script or static block.

var captured

The type of the None singleton.

var written_at_entry

Whether the call writes this binding before any statement of its scope runs. A var of a parameter's name is the one shape that does: the body's name starts out holding the argument, and only a declarator that runs later says anything about what it holds after that. There is no node for that write - the call makes it, not anything in the text - so it can be neither a writes nor an indefinite_writes entry, both of which every consumer orders by position.

var reachable_through_a_handed_object

Whether the program hands a call the object that carries this binding, so a body no reading of the text follows can name it. The references such a call may make are recorded like any other, and this says the one thing they cannot: that a walk which finds a name only where the text spells it is looking at less than the whole program. Only a global is ever carried this way.

var exported

Whether the binding is exported, so an importer observes its value across the module boundary after the module runs. Like the two flags above, it names an observer no reading of the text reaches: its declaration must be kept and never relocated out of module scope, and a write to it is never a dead store, because the final value is read from outside. export var a, export function/class, and the local half of a sourceless export { a } all set it; a from-clause list and a re-export name a binding of another module and set nothing here.

var supplied_by_host

Whether the host supplies this binding: a parameter or the arguments of the function a CommonJS host wraps around the text of the file, which holds what the host passed from the moment the file starts to run. A var or function declaration of the file joins it, as one in the body of a function joins a parameter.

var may_be_absent

Whether the binding exists in one way the file may run and not in another, so that a reference to it may find no binding at all: a binding the host supplies where the file may also be an ES module, which binds none of them, unless a declaration of the file joins it, and a var only conditional copies declare (AnnexBCopy.conditional).

var is_read

Whether the binding's value is ever read.

Expand source code Browse git
@property
def is_read(self) -> bool:
    """
    Whether the binding's value is ever read.
    """
    return bool(self.reads)
var is_hoisted

Whether the binding is hoisted to the top of its variable scope — a var or a function declaration — and so is visible (as undefined, or the function) throughout that scope before its textual position, rather than sitting in a temporal dead zone.

Expand source code Browse git
@property
def is_hoisted(self) -> bool:
    """
    Whether the binding is hoisted to the top of its variable scope — a `var` or a function
    declaration — and so is visible (as `undefined`, or the function) throughout that scope before
    its textual position, rather than sitting in a temporal dead zone.
    """
    return self.kind in (BindingKind.VAR, BindingKind.FUNCTION)
var is_lexical

Whether the binding is block-scoped in a declarative environment — a let, const, or class. Defined positively: a parameter, catch binding, import, or implicit global is neither hoisted nor lexical in this sense.

Expand source code Browse git
@property
def is_lexical(self) -> bool:
    """
    Whether the binding is block-scoped in a declarative environment — a `let`, `const`, or
    `class`. Defined positively: a parameter, catch binding, import, or implicit global is neither
    hoisted nor lexical in this sense.
    """
    return self.kind in (BindingKind.LET, BindingKind.CONST, BindingKind.CLASS)
var is_dead

Whether no use observes the binding's value: it is read through no resolved reference, named inside no dynamic scope, and not exported. Definitions of a dead binding can be removed if they carry no other side effect (which the caller decides). A name a with body could read is not dead even though reads is empty — the dynamic reference may observe it at runtime — nor is an exported one, whose value an importer reads across the module boundary, so removers need not rely on a separate reflection gate to keep such a binding.

Expand source code Browse git
@property
def is_dead(self) -> bool:
    """
    Whether no use observes the binding's value: it is read through no resolved reference, named
    inside no dynamic scope, and not exported. Definitions of a dead binding can be removed if they
    carry no other side effect (which the caller decides). A name a `with` body could read is not
    dead even though `reads` is empty — the dynamic reference may observe it at runtime — nor is an
    exported one, whose value an importer reads across the module boundary, so removers need not
    rely on a separate reflection gate to keep such a binding.
    """
    return not self.reads and not self.dynamic_refs and not self.exported
var has_indefinite_write

Whether some access writes the binding at a point where what it stores, or whether it stores at all, is decided only at run time, so that its value stops holding there and no definition says what replaced it. Every write through a mapped arguments object is one. arguments[k] = v<code> for a </code>k no reading of the text computes writes exactly one parameter of the function and which one is not decidable, so it is a kill of each with a value for none; arguments[0] = v names its parameter but still lands only where the call supplied that argument (§10.2.11 maps an element onto a parameter only for a position index < len), so it is a kill of that one with a value for none. The object handed to a call or bound to a second name is another, and the entry is then the identifier the object escaped through rather than an access on it.

It is kept apart from writes for the same reason dynamic_refs is kept apart: a writes entry is a definition, and a consumer reading one expects to find the value it stored. Recording this as a definition of every parameter would let a fold answer with a value only one of them can hold; recording it nowhere lets a fold carry a value across it that the write destroyed.

The write a call makes on entry is one of these too, and it is the one with no node at all, so it is carried by written_at_entry and read here beside the rest.

Expand source code Browse git
@property
def has_indefinite_write(self) -> bool:
    """
    Whether some access writes the binding at a point where what it stores, or whether it stores
    at all, is decided only at run time, so that its value stops holding there and no definition
    says what replaced it. Every write through a mapped `arguments` object is one. `arguments[k]
    = v` for a `k` no reading of the text computes writes exactly one parameter of the function
    and which one is not decidable, so it is a kill of each with a value for none; `arguments[0]
    = v` names its parameter but still lands only where the call supplied that argument
    (§10.2.11 maps an element onto a parameter only for a position `index < len`), so it is a
    kill of that one with a value for none. The object handed to a call or bound to a second
    name is another, and the entry is then the identifier the object escaped through rather than
    an access on it.

    It is kept apart from `writes` for the same reason `dynamic_refs` is kept apart: a `writes`
    entry is a definition, and a consumer reading one expects to find the value it stored. Recording
    this as a definition of every parameter would let a fold answer with a value only one of them
    can hold; recording it nowhere lets a fold carry a value across it that the write destroyed.

    The write a call makes on entry is one of these too, and it is the one with no node at all,
    so it is carried by `written_at_entry` and read here beside the rest.
    """
    return bool(self.indefinite_writes) or self.written_at_entry
var has_global_member_write

Whether the binding is written through a member access on a global-object alias (globalThis.x = ...), recorded as a JsMemberExpression write site rather than a referencing identifier (see the class docstring). Only a global ever carries such a write, so the answer is always false for a lexical binding.

The access is tested by what it is on and not by its being a member access, because a parameter of a sloppy function carries member-access writes too — through the arguments object that aliases it — and those reach one function's own parameter rather than the global object.

Expand source code Browse git
@property
def has_global_member_write(self) -> bool:
    """
    Whether the binding is written through a member access on a global-object alias
    (`globalThis.x = ...`), recorded as a `JsMemberExpression` write site rather than a referencing
    identifier (see the class docstring). Only a global ever carries such a write, so the answer is
    always false for a lexical binding.

    The access is tested by what it is on and not by its being a member access, because a parameter
    of a sloppy function carries member-access writes too — through the `arguments` object that
    aliases it — and those reach one function's own parameter rather than the global object.
    """
    return any(_is_global_alias_access(write) for write in self.writes)
var has_member_reference

Whether the binding is read or written through a member access on a global-object alias (globalThis.x), recorded as a JsMemberExpression reference rather than a referencing identifier (see the class docstring). Such a binding is reachable through the global object, so a caller must not treat it as an ordinary local — it cannot be relocated into a function.

As with has_global_member_write, an access through a mapped arguments object is not one of these: it reaches a parameter, which no other function can name.

Expand source code Browse git
@property
def has_member_reference(self) -> bool:
    """
    Whether the binding is read or written through a member access on a global-object alias
    (`globalThis.x`), recorded as a `JsMemberExpression` reference rather than a referencing
    identifier (see the class docstring). Such a binding is reachable through the global object, so
    a caller must not treat it as an ordinary local — it cannot be relocated into a function.

    As with `has_global_member_write`, an access through a mapped `arguments` object is not one of
    these: it reaches a parameter, which no other function can name.
    """
    return any(_is_global_alias_access(ref) for ref in (*self.reads, *self.writes))
var store_sites

Every node whose execution stores into the binding, whatever it stores: each recorded write, and each declaration that stores where it runs (_where_it_stores). A let, const or class declaration is one even without a value, since it ends the binding's temporal dead zone. A var has none, so a bare var x; stores nothing; its declarator stores where it carries a value, a destructuring pattern included, or stands in the head of a for-in or for-of loop, which stores on every iteration. A function declaration Annex B copies into the var stores where it runs, and is listed itself rather than its name, which lies inside the function it names. A store made before any statement of the binding's scope runs has no node and is not listed: a parameter's, or the function a declaration hoists. Ordering the sites against a point is the caller's.

Expand source code Browse git
@property
def store_sites(self) -> list[ReferenceNode]:
    """
    Every node whose execution stores into the binding, whatever it stores: each recorded write,
    and each declaration that stores where it runs (`_where_it_stores`). A `let`, `const` or
    `class` declaration is one even without a value, since it ends the binding's temporal dead
    zone. A `var` has none, so a bare `var x;` stores nothing; its declarator stores where it
    carries a value, a destructuring pattern included, or stands in the head of a `for-in` or
    `for-of` loop, which stores on every iteration. A function declaration Annex B copies into
    the `var` stores where it runs, and is listed itself rather than its name, which lies inside
    the function it names. A store made before any statement of the binding's scope runs has no
    node and is not listed: a parameter's, or the function a declaration hoists. Ordering the
    sites against a point is the caller's.
    """
    sites: list[ReferenceNode] = list(self.writes)
    for declaration in self.declarations:
        if _where_it_stores(self, declaration) is not _DeclarationStore.WHERE_IT_RUNS:
            continue
        if isinstance(function := declaration.parent, JsFunctionDeclaration):
            sites.append(function)
        else:
            sites.append(declaration)
    return sites

Methods

def note_reference_from(self, scope)

Mark this binding captured where scope is on the far side of a closure boundary from the scope declaring it, which is what a reference made from a scope with a different variable scope is. A reference whose own scope is not known is counted as a capture, since nothing about it says that it is not one.

Three walks record a reference and each of them asks this: the identifier walk, the one reading a binding through an alias of the global object, and the one reading a parameter through a mapped arguments. They have to agree, and one of them being written differently from the others is not a difference anything downstream could act on.

Expand source code Browse git
def note_reference_from(self, scope: Scope | None) -> None:
    """
    Mark this binding captured where *scope* is on the far side of a closure boundary from the
    scope declaring it, which is what a reference made from a scope with a different variable
    scope is. A reference whose own scope is not known is counted as a capture, since nothing
    about it says that it is not one.

    Three walks record a reference and each of them asks this: the identifier walk, the one
    reading a binding through an alias of the global object, and the one reading a parameter
    through a mapped `arguments`. They have to agree, and one of them being written differently
    from the others is not a difference anything downstream could act on.
    """
    if scope is None or scope.closure_home is not self.scope.closure_home:
        self.captured = True
def declared_within(self, node)

Whether the scope declaring this binding is introduced by node or by a node inside it, so that the binding belongs to node: a copy of node carries it along, and a name node reads that resolves elsewhere comes from outside.

Expand source code Browse git
def declared_within(self, node: Node) -> bool:
    """
    Whether the scope declaring this binding is introduced by *node* or by a node inside it, so
    that the binding belongs to *node*: a copy of *node* carries it along, and a name *node*
    reads that resolves elsewhere comes from outside.
    """
    owner = self.scope.node
    return owner is node or owner.is_descendant_of(node)
class BindingKind (*args, **kwds)

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

Color.RED

  • value lookup:

Color(1)

  • name lookup:

Color['RED']

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Expand source code Browse git
class BindingKind(enum.Enum):
    VAR             = 'var'              # noqa
    LET             = 'let'              # noqa
    CONST           = 'const'            # noqa
    PARAM           = 'param'            # noqa
    FUNCTION        = 'function'         # noqa
    CLASS           = 'class'            # noqa
    CATCH           = 'catch'            # noqa
    IMPORT          = 'import'           # noqa
    ARGUMENTS       = 'arguments'        # noqa
    FUNC_NAME       = 'func_name'        # noqa  the own name of a named function expression
    IMPLICIT_GLOBAL = 'implicit_global'  # noqa  a name assigned but never declared

Ancestors

  • enum.Enum

Class variables

var VAR

The type of the None singleton.

var LET

The type of the None singleton.

var CONST

The type of the None singleton.

var PARAM

The type of the None singleton.

var FUNCTION

The type of the None singleton.

var CLASS

The type of the None singleton.

var CATCH

The type of the None singleton.

var IMPORT

The type of the None singleton.

var ARGUMENTS

The type of the None singleton.

var FUNC_NAME

The type of the None singleton.

var IMPLICIT_GLOBAL

The type of the None singleton.

class CfgNode (graph, element, successors=<factory>, predecessors=<factory>, is_resumption_hub=False, is_hub_bound=False)

One vertex of a control-flow graph. element is the AST node it stands for — a statement, a loop-head expression whose reads and writes occur at this point, or a step a language gives a node of its own inside a statement — or None for the synthetic entry and exit. successors lists the nodes control may pass to next. graph is the body this vertex belongs to, which is what makes the questions below answerable of a node alone.

is_resumption_hub marks the synthetic fan-out standing for the over-approximate half of a resuming handler — see CfgEdge. A walk that wants only the paths going forward declines to enter one, and declines to leave one, which makes it a projection of the graph rather than a filter on its edges. It is a fact about the node and not about the edges into it, because an edge kind is keyed by the pair of nodes it joins: a plain edge drawn beside a hub edge makes the pair carry both, and a walk declining pairs that carry the hub kind would decline the plain path too. No pair of edges can disagree about what the node is.

is_hub_bound marks a node the precise, forward-only projection does not reach: it sits inside a handler body that resumes the block around it. That block's forward edges join each guarded statement to the one it resumes at; a statement of the handler itself is on neither end of one, and everything it may reach afterwards hangs off the hub. A forward edge it does carry belongs to a block further out — one whose own resuming set guards the statement this handler is written inside — and stands for that block's resumption, not this one, so it says nothing about where this handler carries on. A forward-only walk seeded here would stop dead where the real run carries on, which for a flood is the unsound direction; reachable_forward_from_any reads this and falls back to the hub for such a source.

Both are fields rather than questions asked of graph, and that is a matter of what a walk can afford as much as of where the fact lives. Every layer that had to ask them of a graph it was separately handed could be handed the wrong one — a node lifted from a nested body reads as neither against the graph around it, and the walk that asked takes the wrong branch without anything failing — and one depth-first sweep over a large script asks tens of millions of times. graph remains because a caller naming which body it means is a contract worth checking, and because ControlFlowGraph.hub_bound answers the same question for a caller holding ids.

eq=False because every map in every layer above is keyed by id(node), and two structurally equal statements are two distinct points in the program.

repr=False because a generated repr expands successors and predecessors, and the guard against recursion only covers the node currently being formatted, so a graph re-expands at every join: a chain of five hundred nodes — a script of five hundred statements — exhausts the interpreter's stack, and a handful of branches produces megabytes. A debugger, a failing assertion, or pytest --showlocals would print one.

Expand source code Browse git
@dataclass(eq=False, repr=False)
class CfgNode:
    """
    One vertex of a control-flow graph. `element` is the AST node it stands for — a statement, a
    loop-head expression whose reads and writes occur at this point, or a step a language gives a
    node of its own inside a statement — or `None` for the synthetic entry and exit. `successors`
    lists the nodes control may pass to next. `graph` is the body this vertex belongs to, which is
    what makes the questions below answerable of a node alone.

    `is_resumption_hub` marks the synthetic fan-out standing for the over-approximate half of a
    resuming handler — see `CfgEdge`. A walk that wants only the paths going forward declines to
    enter one, and declines to leave one, which makes it a *projection* of the graph rather than a
    filter on its edges. It is a fact about the node and not about the edges into it, because an
    edge kind is keyed by the pair of nodes it joins: a plain edge drawn beside a hub edge makes the
    pair carry both, and a walk declining pairs that carry the hub kind would decline the plain path
    too. No pair of edges can disagree about what the node is.

    `is_hub_bound` marks a node the precise, forward-only projection does not reach: it sits inside a
    handler body that resumes the block around it. That block's forward edges join each *guarded*
    statement to the one it resumes at; a statement of the handler itself is on neither end of one,
    and everything it may reach afterwards hangs off the hub. A forward edge it does carry belongs to
    a block further out — one whose own resuming set guards the statement this handler is written
    inside — and stands for that block's resumption, not this one, so it says nothing about where
    this handler carries on. A forward-only walk seeded here would stop dead where the real run
    carries on, which for a flood is the unsound direction; `reachable_forward_from_any` reads this
    and falls back to the hub for such a source.

    Both are fields rather than questions asked of *graph*, and that is a matter of what a walk can
    afford as much as of where the fact lives. Every layer that had to ask them of a graph it was
    separately handed could be handed the wrong one — a node lifted from a nested body reads as
    neither against the graph around it, and the walk that asked takes the wrong branch without
    anything failing — and one depth-first sweep over a large script asks tens of millions of times.
    `graph` remains because a caller naming which body it means is a contract worth checking, and
    because `ControlFlowGraph.hub_bound` answers the same question for a caller holding ids.

    `eq=False` because every map in every layer above is keyed by `id(node)`, and two structurally
    equal statements are two distinct points in the program.

    `repr=False` because a generated repr expands `successors` and `predecessors`, and the guard
    against recursion only covers the node currently being formatted, so a graph re-expands at every
    join: a chain of five hundred nodes — a script of five hundred statements — exhausts the
    interpreter's stack, and a handful of branches produces megabytes. A debugger, a failing
    assertion, or `pytest --showlocals` would print one.
    """
    graph: ControlFlowGraph
    element: Node | None
    successors: list[CfgNode] = field(default_factory=list)
    predecessors: list[CfgNode] = field(default_factory=list)
    is_resumption_hub: bool = False
    is_hub_bound: bool = False

Subclasses

Instance variables

var graph

The type of the None singleton.

var element

The type of the None singleton.

var successors

The type of the None singleton.

var predecessors

The type of the None singleton.

var is_resumption_hub

The type of the None singleton.

var is_hub_bound

The type of the None singleton.

class ControlFlowGraph (owner)

The control-flow graph of one function or script body. entry and exit are synthetic; every other node wraps an AST element reachable through node_of.

Expand source code Browse git
class ControlFlowGraph:
    """
    The control-flow graph of one function or script body. `entry` and `exit` are synthetic; every
    other node wraps an AST element reachable through `node_of`.
    """

    def __init__(self, owner: Node):
        self.owner = owner
        self._node_of: dict[int, CfgNode] = {}
        self._edge_kinds: dict[tuple[int, int], CfgEdge] = {}
        self._hub_bound: set[int] = set()
        self._resuming = False
        self._reached_after: dict[tuple[int, bool], frozenset[int]] = {}
        self._completions: dict[int, tuple[Node, list[CfgNode]]] = {}
        self._spans: dict[int, tuple[Node, range]] = {}
        self._positions: dict[int, int] | None = None
        self._condensations: dict[Projection, Condensation[CfgNode]] = {}
        self.entry = CfgNode(self, None)
        self.exit = CfgNode(self, None)
        self.nodes: list[CfgNode] = [self.entry, self.exit]

    def node_of(self, element: Node) -> CfgNode | None:
        """
        The graph node standing for *element*, or `None` if *element* is not part of this body, or is
        a node the graph does not represent on its own such as a plain expression inside a statement.
        """
        return self._node_of.get(id(element))

    def completion_of(self, statement: Node) -> list[CfgNode] | None:
        """
        The nodes from which normal control falls through past *statement*, a member of a statement
        sequence this graph was built from (`CfgBuilder.sequence`), or `None` for any other element.
        An empty list is a statement control never runs off the end of.
        """
        if (found := self._completions.get(id(statement))) is None or found[0] is not statement:
            return None
        return found[1]

    def built_for(self, statement: Node) -> list[CfgNode] | None:
        """
        The nodes built for *statement*, a member of a statement sequence this graph was built
        from: its own node, those of every construct nested in it, and the synthetic joins and
        resumption slots its build drew; `None` where it is any other element.
        """
        if (found := self._spans.get(id(statement))) is None or found[0] is not statement:
            return None
        span = found[1]
        return self.nodes[span.start:span.stop]

    def builder_of(self, statements: Iterable[Node]) -> Callable[[CfgNode], bool] | None:
        """
        A test for whether a node of this graph was built for one of *statements*, each a member of
        a statement sequence this graph was built from — its own node, those of every construct
        nested in it, and the synthetic joins and resumption slots its build drew — or `None` where
        one of them is any other element.

        Read off the order the builder appends nodes in: building a statement is one recursive call,
        so everything built for it is appended between that call's start and its end, and two such
        stretches are nested or apart, as the statements are. A test is one search among the
        outermost stretches however deep the statements nest. A walk that measures what a run does
        inside some statements asks this of every node it meets to see where the run has left them.
        """
        spans: list[range] = []
        for statement in statements:
            if (found := self._spans.get(id(statement))) is None or found[0] is not statement:
                return None
            spans.append(found[1])
        starts: list[int] = []
        stops: list[int] = []
        for span in sorted(spans, key=lambda span: (span.start, -span.stop)):
            if stops and span.start < stops[-1]:
                continue
            starts.append(span.start)
            stops.append(span.stop)
        positions = self._positions
        if positions is None:
            positions = self._positions = {id(node): index for index, node in enumerate(self.nodes)}

        def holds(node: CfgNode) -> bool:
            position = positions.get(id(node), -1)
            index = bisect_right(starts, position) - 1
            return index >= 0 and position < stops[index]

        return holds

    def edge_kind(self, source: CfgNode, target: CfgNode) -> CfgEdge:
        """
        What the edges from *source* to *target* say about the runs that take them — see `CfgEdge`.
        A pair joined more than once carries the kind of every edge between it, and an unrelated
        pair of nodes reads as `CfgEdge.NORMAL`.
        """
        return self._edge_kinds.get((id(source), id(target)), CfgEdge.NORMAL)

    def completes_into(self, source: CfgNode, target: CfgNode) -> bool:
        """
        Whether control passes from *source* to *target* on a run where *source* completed: a plain
        edge joins them, whatever other edge joins them as well.
        """
        kind = self._edge_kinds.get((id(source), id(target)))
        return kind is None or bool(kind.value & _NORMAL_MASK)

    def is_exceptional(self, source: CfgNode, target: CfgNode) -> bool:
        """
        Whether the error travels along the edge from *source* to *target*: the edge into a handler
        offered the throw, or outward from a set that declined it. This is the question `faults`
        asks — where an error goes — and it is *narrower* than `raise_taken`, because a handler that
        resumes swallows the error and carries on along an edge no error travels.
        """
        return bool(self.edge_kind(source, target).value & _ERROR_CARRYING_MASK)

    def raise_taken(self, source: CfgNode, target: CfgNode) -> bool:
        """
        Whether an edge from *source* to *target* is taken only when *source* throws rather than
        completing normally. A definition *source* makes is not guaranteed to have happened along
        such an edge, so a flow-sensitive analysis must not treat it as a kill there. A pair joined
        by such an edge and by a plain one as well answers for the first, and `completes_into` for
        the second; a consumer reading only this one reads the pair as the run that did not finish,
        which keeps every fact a kill would drop.

        This is the question about *completion*, and it is the one nearly every consumer means. A
        resumption edge answers it although no error travels along it: the statement that resumed
        the block is precisely the one that did not finish.
        """
        return bool(self.edge_kind(source, target).value & _RAISE_TAKEN_MASK)

    def exit_reach(self, source: CfgNode) -> tuple[frozenset[int], frozenset[int]]:
        """
        The ids of the nodes reached from *source* by first leaving it normally, and the ids of
        those reached by first leaving it along an edge `raise_taken` answers for — on a run where
        *source* threw before it completed. A store *source* makes has happened at every node of the
        first set and may not have happened at a node of the second. Once control has left *source*
        normally the store is done, so only the first edge out decides which set a node belongs to,
        and a node control rejoins both ways — the statement after a `try` whose handler swallowed
        the throw — is in both. A caller that needs only the second set asks
        `reached_after_a_throw`.
        """
        return self._reached_after_cached(source, raising=False), self.reached_after_a_throw(source)

    def reached_after_a_throw(self, source: CfgNode) -> frozenset[int]:
        """
        The second set `exit_reach` answers: the ids of the nodes reached from *source* by first
        leaving it along an edge `raise_taken` answers for. Each set is computed on first use and
        kept, so a caller asking only this one never pays for the other, which is most of the graph.
        """
        return self._reached_after_cached(source, raising=True)

    def _reached_after_cached(self, source: CfgNode, *, raising: bool) -> frozenset[int]:
        key = (id(source), raising)
        found = self._reached_after.get(key)
        if found is None:
            found = self._reached_after[key] = self._reached_after_uncached(source, raising=raising)
        return found

    def _reached_after_uncached(self, source: CfgNode, *, raising: bool) -> frozenset[int]:
        seen: set[int] = set()
        stack: list[CfgNode] = []
        leaves = self.raise_taken if raising else self.completes_into
        for successor in source.successors:
            if not leaves(source, successor):
                continue
            if id(successor) not in seen:
                seen.add(id(successor))
                stack.append(successor)
        while stack:
            node = stack.pop()
            for successor in node.successors:
                if id(successor) not in seen:
                    seen.add(id(successor))
                    stack.append(successor)
        return frozenset(seen)

    @property
    def hub_bound(self) -> Set[int]:
        """
        The ids of the nodes `CfgNode.is_hub_bound` is set on, for a caller holding node *ids*
        rather than nodes. Written beside the field by `CfgBuilder.mark_hub_bound`, which is the one
        place either is recorded. `refinery.lib.scripts.analysis.reaching.ReachabilityQuery` is one: its
        candidate sets are ids because the layers above memoize them that way, and turning them back
        into nodes to ask each one is what those caches exist to avoid.
        """
        return self._hub_bound

    @property
    def carries_resumption(self) -> bool:
        """
        Whether any handler of this body resumes the block it guards, which is the only shape over
        which the two projections of `CfgEdge` differ. A body with none — every script that writes
        no `trap`, and every one whose traps rethrow — draws no resumption edge and marks no node
        hub-bound, so a forward walk over it answers exactly what `reachable_from_any` does and may
        be answered by the cheaper one.
        """
        return self._resuming

    def reading(self, projection: Projection) -> Projection:
        """
        The reading a walk over this graph under *projection* takes: *projection* itself where the
        graph `carries_resumption`, and `Projection.MAY` where it does not, since a graph that draws
        no hub is walked alike under both. Everything kept per reading of a graph is keyed by this,
        here and in `refinery.lib.scripts.analysis.dominance.DominatorModel`, so that a condensation
        and a dominance tree asked for under one projection describe one relation.
        """
        return projection if self.carries_resumption else Projection.MAY

    def condensation(self, projection: Projection) -> Condensation[CfgNode]:
        """
        This graph condensed into its strongly connected components under *projection*, computed
        on first use and kept per `reading`, as `_reached_after` and `_positions` are kept: a graph
        does not change once it is built.

        The walk that numbers the components starts at the entry, so every component the entry
        reaches is numbered below every component it does not, whatever order `nodes` lists them
        in: a walk bounded by the number of a node the entry reaches stays within what it reaches.
        """
        reading = self.reading(projection)
        found = self._condensations.get(reading)
        if found is None:
            found = Condensation((self.entry, *self.nodes), reading.successors)
            self._condensations[reading] = found
        return found

Instance variables

var hub_bound

The ids of the nodes CfgNode.is_hub_bound is set on, for a caller holding node ids rather than nodes. Written beside the field by CfgBuilder.mark_hub_bound, which is the one place either is recorded. ReachabilityQuery is one: its candidate sets are ids because the layers above memoize them that way, and turning them back into nodes to ask each one is what those caches exist to avoid.

Expand source code Browse git
@property
def hub_bound(self) -> Set[int]:
    """
    The ids of the nodes `CfgNode.is_hub_bound` is set on, for a caller holding node *ids*
    rather than nodes. Written beside the field by `CfgBuilder.mark_hub_bound`, which is the one
    place either is recorded. `refinery.lib.scripts.analysis.reaching.ReachabilityQuery` is one: its
    candidate sets are ids because the layers above memoize them that way, and turning them back
    into nodes to ask each one is what those caches exist to avoid.
    """
    return self._hub_bound
var carries_resumption

Whether any handler of this body resumes the block it guards, which is the only shape over which the two projections of CfgEdge differ. A body with none — every script that writes no trap, and every one whose traps rethrow — draws no resumption edge and marks no node hub-bound, so a forward walk over it answers exactly what reachable_from_any does and may be answered by the cheaper one.

Expand source code Browse git
@property
def carries_resumption(self) -> bool:
    """
    Whether any handler of this body resumes the block it guards, which is the only shape over
    which the two projections of `CfgEdge` differ. A body with none — every script that writes
    no `trap`, and every one whose traps rethrow — draws no resumption edge and marks no node
    hub-bound, so a forward walk over it answers exactly what `reachable_from_any` does and may
    be answered by the cheaper one.
    """
    return self._resuming

Methods

def node_of(self, element)

The graph node standing for element, or None if element is not part of this body, or is a node the graph does not represent on its own such as a plain expression inside a statement.

Expand source code Browse git
def node_of(self, element: Node) -> CfgNode | None:
    """
    The graph node standing for *element*, or `None` if *element* is not part of this body, or is
    a node the graph does not represent on its own such as a plain expression inside a statement.
    """
    return self._node_of.get(id(element))
def completion_of(self, statement)

The nodes from which normal control falls through past statement, a member of a statement sequence this graph was built from (CfgBuilder.sequence), or None for any other element. An empty list is a statement control never runs off the end of.

Expand source code Browse git
def completion_of(self, statement: Node) -> list[CfgNode] | None:
    """
    The nodes from which normal control falls through past *statement*, a member of a statement
    sequence this graph was built from (`CfgBuilder.sequence`), or `None` for any other element.
    An empty list is a statement control never runs off the end of.
    """
    if (found := self._completions.get(id(statement))) is None or found[0] is not statement:
        return None
    return found[1]
def built_for(self, statement)

The nodes built for statement, a member of a statement sequence this graph was built from: its own node, those of every construct nested in it, and the synthetic joins and resumption slots its build drew; None where it is any other element.

Expand source code Browse git
def built_for(self, statement: Node) -> list[CfgNode] | None:
    """
    The nodes built for *statement*, a member of a statement sequence this graph was built
    from: its own node, those of every construct nested in it, and the synthetic joins and
    resumption slots its build drew; `None` where it is any other element.
    """
    if (found := self._spans.get(id(statement))) is None or found[0] is not statement:
        return None
    span = found[1]
    return self.nodes[span.start:span.stop]
def builder_of(self, statements)

A test for whether a node of this graph was built for one of statements, each a member of a statement sequence this graph was built from — its own node, those of every construct nested in it, and the synthetic joins and resumption slots its build drew — or None where one of them is any other element.

Read off the order the builder appends nodes in: building a statement is one recursive call, so everything built for it is appended between that call's start and its end, and two such stretches are nested or apart, as the statements are. A test is one search among the outermost stretches however deep the statements nest. A walk that measures what a run does inside some statements asks this of every node it meets to see where the run has left them.

Expand source code Browse git
def builder_of(self, statements: Iterable[Node]) -> Callable[[CfgNode], bool] | None:
    """
    A test for whether a node of this graph was built for one of *statements*, each a member of
    a statement sequence this graph was built from — its own node, those of every construct
    nested in it, and the synthetic joins and resumption slots its build drew — or `None` where
    one of them is any other element.

    Read off the order the builder appends nodes in: building a statement is one recursive call,
    so everything built for it is appended between that call's start and its end, and two such
    stretches are nested or apart, as the statements are. A test is one search among the
    outermost stretches however deep the statements nest. A walk that measures what a run does
    inside some statements asks this of every node it meets to see where the run has left them.
    """
    spans: list[range] = []
    for statement in statements:
        if (found := self._spans.get(id(statement))) is None or found[0] is not statement:
            return None
        spans.append(found[1])
    starts: list[int] = []
    stops: list[int] = []
    for span in sorted(spans, key=lambda span: (span.start, -span.stop)):
        if stops and span.start < stops[-1]:
            continue
        starts.append(span.start)
        stops.append(span.stop)
    positions = self._positions
    if positions is None:
        positions = self._positions = {id(node): index for index, node in enumerate(self.nodes)}

    def holds(node: CfgNode) -> bool:
        position = positions.get(id(node), -1)
        index = bisect_right(starts, position) - 1
        return index >= 0 and position < stops[index]

    return holds
def edge_kind(self, source, target)

What the edges from source to target say about the runs that take them — see CfgEdge. A pair joined more than once carries the kind of every edge between it, and an unrelated pair of nodes reads as CfgEdge.NORMAL.

Expand source code Browse git
def edge_kind(self, source: CfgNode, target: CfgNode) -> CfgEdge:
    """
    What the edges from *source* to *target* say about the runs that take them — see `CfgEdge`.
    A pair joined more than once carries the kind of every edge between it, and an unrelated
    pair of nodes reads as `CfgEdge.NORMAL`.
    """
    return self._edge_kinds.get((id(source), id(target)), CfgEdge.NORMAL)
def completes_into(self, source, target)

Whether control passes from source to target on a run where source completed: a plain edge joins them, whatever other edge joins them as well.

Expand source code Browse git
def completes_into(self, source: CfgNode, target: CfgNode) -> bool:
    """
    Whether control passes from *source* to *target* on a run where *source* completed: a plain
    edge joins them, whatever other edge joins them as well.
    """
    kind = self._edge_kinds.get((id(source), id(target)))
    return kind is None or bool(kind.value & _NORMAL_MASK)
def is_exceptional(self, source, target)

Whether the error travels along the edge from source to target: the edge into a handler offered the throw, or outward from a set that declined it. This is the question faults asks — where an error goes — and it is narrower than raise_taken, because a handler that resumes swallows the error and carries on along an edge no error travels.

Expand source code Browse git
def is_exceptional(self, source: CfgNode, target: CfgNode) -> bool:
    """
    Whether the error travels along the edge from *source* to *target*: the edge into a handler
    offered the throw, or outward from a set that declined it. This is the question `faults`
    asks — where an error goes — and it is *narrower* than `raise_taken`, because a handler that
    resumes swallows the error and carries on along an edge no error travels.
    """
    return bool(self.edge_kind(source, target).value & _ERROR_CARRYING_MASK)
def raise_taken(self, source, target)

Whether an edge from source to target is taken only when source throws rather than completing normally. A definition source makes is not guaranteed to have happened along such an edge, so a flow-sensitive analysis must not treat it as a kill there. A pair joined by such an edge and by a plain one as well answers for the first, and completes_into for the second; a consumer reading only this one reads the pair as the run that did not finish, which keeps every fact a kill would drop.

This is the question about completion, and it is the one nearly every consumer means. A resumption edge answers it although no error travels along it: the statement that resumed the block is precisely the one that did not finish.

Expand source code Browse git
def raise_taken(self, source: CfgNode, target: CfgNode) -> bool:
    """
    Whether an edge from *source* to *target* is taken only when *source* throws rather than
    completing normally. A definition *source* makes is not guaranteed to have happened along
    such an edge, so a flow-sensitive analysis must not treat it as a kill there. A pair joined
    by such an edge and by a plain one as well answers for the first, and `completes_into` for
    the second; a consumer reading only this one reads the pair as the run that did not finish,
    which keeps every fact a kill would drop.

    This is the question about *completion*, and it is the one nearly every consumer means. A
    resumption edge answers it although no error travels along it: the statement that resumed
    the block is precisely the one that did not finish.
    """
    return bool(self.edge_kind(source, target).value & _RAISE_TAKEN_MASK)
def exit_reach(self, source)

The ids of the nodes reached from source by first leaving it normally, and the ids of those reached by first leaving it along an edge raise_taken answers for — on a run where source threw before it completed. A store source makes has happened at every node of the first set and may not have happened at a node of the second. Once control has left source normally the store is done, so only the first edge out decides which set a node belongs to, and a node control rejoins both ways — the statement after a try whose handler swallowed the throw — is in both. A caller that needs only the second set asks reached_after_a_throw.

Expand source code Browse git
def exit_reach(self, source: CfgNode) -> tuple[frozenset[int], frozenset[int]]:
    """
    The ids of the nodes reached from *source* by first leaving it normally, and the ids of
    those reached by first leaving it along an edge `raise_taken` answers for — on a run where
    *source* threw before it completed. A store *source* makes has happened at every node of the
    first set and may not have happened at a node of the second. Once control has left *source*
    normally the store is done, so only the first edge out decides which set a node belongs to,
    and a node control rejoins both ways — the statement after a `try` whose handler swallowed
    the throw — is in both. A caller that needs only the second set asks
    `reached_after_a_throw`.
    """
    return self._reached_after_cached(source, raising=False), self.reached_after_a_throw(source)
def reached_after_a_throw(self, source)

The second set exit_reach answers: the ids of the nodes reached from source by first leaving it along an edge raise_taken answers for. Each set is computed on first use and kept, so a caller asking only this one never pays for the other, which is most of the graph.

Expand source code Browse git
def reached_after_a_throw(self, source: CfgNode) -> frozenset[int]:
    """
    The second set `exit_reach` answers: the ids of the nodes reached from *source* by first
    leaving it along an edge `raise_taken` answers for. Each set is computed on first use and
    kept, so a caller asking only this one never pays for the other, which is most of the graph.
    """
    return self._reached_after_cached(source, raising=True)
def reading(self, projection)

The reading a walk over this graph under projection takes: projection itself where the graph carries_resumption, and Projection.MAY where it does not, since a graph that draws no hub is walked alike under both. Everything kept per reading of a graph is keyed by this, here and in DominatorModel, so that a condensation and a dominance tree asked for under one projection describe one relation.

Expand source code Browse git
def reading(self, projection: Projection) -> Projection:
    """
    The reading a walk over this graph under *projection* takes: *projection* itself where the
    graph `carries_resumption`, and `Projection.MAY` where it does not, since a graph that draws
    no hub is walked alike under both. Everything kept per reading of a graph is keyed by this,
    here and in `refinery.lib.scripts.analysis.dominance.DominatorModel`, so that a condensation
    and a dominance tree asked for under one projection describe one relation.
    """
    return projection if self.carries_resumption else Projection.MAY
def condensation(self, projection)

This graph condensed into its strongly connected components under projection, computed on first use and kept per reading, as _reached_after and _positions are kept: a graph does not change once it is built.

The walk that numbers the components starts at the entry, so every component the entry reaches is numbered below every component it does not, whatever order nodes lists them in: a walk bounded by the number of a node the entry reaches stays within what it reaches.

Expand source code Browse git
def condensation(self, projection: Projection) -> Condensation[CfgNode]:
    """
    This graph condensed into its strongly connected components under *projection*, computed
    on first use and kept per `reading`, as `_reached_after` and `_positions` are kept: a graph
    does not change once it is built.

    The walk that numbers the components starts at the entry, so every component the entry
    reaches is numbered below every component it does not, whatever order `nodes` lists them
    in: a walk bounded by the number of a node the entry reaches stays within what it reaches.
    """
    reading = self.reading(projection)
    found = self._condensations.get(reading)
    if found is None:
        found = Condensation((self.entry, *self.nodes), reading.successors)
        self._condensations[reading] = found
    return found
class ControlFlowModel (graphs, apart)

The per-body control-flow graphs of one script, paired with the ElementLocator that maps any AST node to the graph node evaluating it. Built once over the script root — the graphs are purely syntactic and need no semantic model — and shared by every solver layered on it, which would otherwise each rebuild the whole set. apart is the ElementLocator argument of that name.

Expand source code Browse git
class ControlFlowModel:
    """
    The per-body control-flow graphs of one script, paired with the `ElementLocator` that maps any
    AST node to the graph node evaluating it. Built once over the script root — the graphs are purely
    syntactic and need no semantic model — and shared by every solver layered on it, which would
    otherwise each rebuild the whole set. *apart* is the `ElementLocator` argument of that name.
    """

    def __init__(self, graphs: dict[int, ControlFlowGraph], apart: Callable[[Node, Node], bool]):
        self.graphs = graphs
        self._locator = ElementLocator(graphs, apart)

    def graph_of(self, owner: Node) -> ControlFlowGraph | None:
        """
        The control-flow graph owned by *owner* — the script root or a node holding a part that runs
        at a time of its own — or `None` when it owns none.
        """
        return self.graphs.get(id(owner))

    def built_from(self, statement: Node) -> bool:
        """
        Whether *statement* is a member of a statement sequence one of the graphs was built from,
        rather than a statement made since, of which no graph holds anything.
        """
        return any(graph.built_for(statement) is not None for graph in self.graphs.values())

    def node_of(self, element: Node) -> CfgNode | None:
        """
        The control-flow node standing for *element*, or `None` when the graphs do not represent it
        directly. Delegates to the shared `ElementLocator`.
        """
        return self._locator.node_of(element)

    def locate(self, element: Node) -> tuple[ControlFlowGraph, CfgNode] | None:
        """
        The graph and node that evaluate *element*, climbing out of any enclosing expression, or
        `None` when it has no enclosing graph node. Delegates to the shared `ElementLocator`.
        """
        return self._locator.locate(element)

Subclasses

Methods

def graph_of(self, owner)

The control-flow graph owned by owner — the script root or a node holding a part that runs at a time of its own — or None when it owns none.

Expand source code Browse git
def graph_of(self, owner: Node) -> ControlFlowGraph | None:
    """
    The control-flow graph owned by *owner* — the script root or a node holding a part that runs
    at a time of its own — or `None` when it owns none.
    """
    return self.graphs.get(id(owner))
def built_from(self, statement)

Whether statement is a member of a statement sequence one of the graphs was built from, rather than a statement made since, of which no graph holds anything.

Expand source code Browse git
def built_from(self, statement: Node) -> bool:
    """
    Whether *statement* is a member of a statement sequence one of the graphs was built from,
    rather than a statement made since, of which no graph holds anything.
    """
    return any(graph.built_for(statement) is not None for graph in self.graphs.values())
def node_of(self, element)

The control-flow node standing for element, or None when the graphs do not represent it directly. Delegates to the shared ElementLocator.

Expand source code Browse git
def node_of(self, element: Node) -> CfgNode | None:
    """
    The control-flow node standing for *element*, or `None` when the graphs do not represent it
    directly. Delegates to the shared `ElementLocator`.
    """
    return self._locator.node_of(element)
def locate(self, element)

The graph and node that evaluate element, climbing out of any enclosing expression, or None when it has no enclosing graph node. Delegates to the shared ElementLocator.

Expand source code Browse git
def locate(self, element: Node) -> tuple[ControlFlowGraph, CfgNode] | None:
    """
    The graph and node that evaluate *element*, climbing out of any enclosing expression, or
    `None` when it has no enclosing graph node. Delegates to the shared `ElementLocator`.
    """
    return self._locator.locate(element)
class EffectModel (model, dominance=None, intrinsic_writes=None, settlement=None, refusals=None)

Per-function effect summaries for one script, built over a SemanticModel. Query a function's summary with summary_of and a call expression's purity with is_pure_call. Build through build_effects().

Expand source code Browse git
class EffectModel:
    """
    Per-function effect summaries for one script, built over a
    `refinery.lib.scripts.js.analysis.model.SemanticModel`. Query a function's summary with
    `summary_of` and a call expression's purity with `is_pure_call`. Build through `build_effects`.
    """

    def __init__(
        self,
        model: SemanticModel,
        dominance: DominanceModel | None = None,
        intrinsic_writes: IntrinsicWrites | None = None,
        settlement: ConversionSettlement | None = None,
        refusals: UnfollowedWriteRefusals | None = None,
    ):
        self.model = model
        self.assumed_keys = model.assumed_keys
        self._dominance = dominance
        self._dead_zone_safe_cache: dict[int, bool] = {}
        self.intrinsics_pristine = _intrinsics_pristine(model)
        self.global_pristine = _global_pristine(model, self.assumed_keys)
        if intrinsic_writes is None:
            intrinsic_writes = build_intrinsic_writes(model)
        self.intrinsic_writes = intrinsic_writes
        self._globals_written = intrinsic_writes.names
        self._summaries: dict[int, EffectSummary] = {}
        self._immutable_cache: dict[tuple[int, frozenset[str] | None, bool], bool] = {}
        self._member_write_cache: dict[int, _WriteClass] = {}
        self._uses_arguments_cache: dict[int, bool] = {}
        self._mutators_escape_cache: dict[int, bool] = {}
        self._escape_cache: dict[Node, bool] = {}
        self._keeps_closures_cache: dict[Node, bool] = {}
        self._some_mutator_cache: dict[int, bool] = {}
        self._settlement = settlement
        self._refusals = refusals
        self._function_use_cache: dict[tuple[Node, frozenset[Node]], FunctionUse] = {}
        self._name_use_cache: dict[tuple[Binding, int, frozenset[Node]], FunctionUse] = {}
        self._functions: list[Node] = self._collect_functions()
        self._compute()
        self.conversion_installs = ConversionInstalls(self)

    def summary_of(self, func: Node) -> EffectSummary:
        """
        The effect summary of a function node (or the script). An unknown node is reported as impure.
        """
        return self._summaries.get(id(func), EffectSummary(calls_unknown=True))

    def mutated_bindings(self, func: Node) -> frozenset[Binding]:
        """
        The outer bindings (captured locals and globals) a call to *func* may write, directly or through
        any function it transitively calls, each identified by its `Binding` rather than its name so a
        caller can ask whether one specific binding is mutated. Empty for a function with no such writes
        and for an unknown node alike — use `summary_of(func).calls_unknown` to tell those apart.
        """
        return frozenset(self.summary_of(func).written_bindings)

    def function_can_mutate(self, func: Node, binding: Binding) -> bool:
        """
        Whether a call to *func* may write *binding*, itself or through a transitive callee.
        """
        return binding in self.summary_of(func).written_bindings

    def function_escapes(self, func: Node) -> bool:
        """
        Whether the body of *func* may run at a point the surrounding scope cannot enumerate as a
        call `name(...)` whose callee `static_callee` resolves: an anonymous function (an IIFE, a
        callback, stored and called later), or a named function whose binding is exported, which
        hands it to every importer, reassigned, redeclared, or referenced anywhere other than as
        the identifier callee of a direct call (aliased, passed as an argument, `f.call(...)`, or
        called through the global object as `globalThis.f()`). A reference inside a dynamic scope —
        a name a `with` body resolves at runtime — counts too: the model cannot order or resolve
        it, so the function may be invoked or aliased there with no static call site.

        A body also runs where no reference of its name shows. A generator's body runs at each step
        of the object a call returns, wherever that object is stepped. The function may be called
        or handed out without its name, through the `caller` of another function, its own
        `arguments` or `new.target`
        (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`). The
        initializer of a field runs at each construction of its class, which no call names. And code
        outside the file may call it by a name it reaches
        (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_from_outside`): a host calls
        a declared entry point whenever the script hands it control, and code loading a library
        calls its functions once the file has run, as an importer calls an export.

        A call to such a function can land at a point no call site pins down; a function only ever
        called directly by name has all its invocations enumerated by those call sites. Memoized
        per function.
        """
        cached = self._escape_cache.get(func)
        if cached is None:
            cached = self._escape_cache[func] = self._compute_function_escapes(func)
        return cached

    def _compute_function_escapes(self, func: Node) -> bool:
        if not isinstance(func, FUNCTION_NODES):
            return True
        names = self.model.function_names(func)
        if names is None:
            return True
        if is_generator_function(func) or self.model.reached_without_its_name(func):
            return True
        for binding in names.bindings:
            if (
                binding.exported
                or self.model.reached_from_outside(binding)
                or binding.writes
                or binding.dynamic_refs
                or len(binding.declarations) != 1
            ):
                return True
            if not self.model.binding_pinned_to(binding, func):
                return True
            if not all(_is_called_by_name(ref) for ref in names.references_of(binding)):
                return True
        return False

    def mutators_escape(self, binding: Binding) -> bool:
        """
        Whether some function that may write *binding* — itself or through a transitive callee — escapes
        (`function_escapes`), so a write to *binding* may occur at a point no call site enumerates. When
        true, the places *binding* changes cannot be pinned down, and a caller reasoning about where its
        value survives must treat it as volatile everywhere. Memoized per binding.
        """
        cached = self._mutators_escape_cache.get(id(binding))
        if cached is None:
            cached = any(
                func is not self.model.root
                and binding in self.summary_of(func).written_bindings
                and self.function_escapes(func)
                for func in self._functions
            )
            self._mutators_escape_cache[id(binding)] = cached
        return cached

    def some_function_can_mutate(self, binding: Binding) -> bool:
        """
        Whether any function this file writes may write *binding*, itself or through a transitive
        callee. The answer a caller needs where a call runs a function it cannot name: not knowing
        which one runs, it has to reckon with every one that could. Memoized per binding.
        """
        cached = self._some_mutator_cache.get(id(binding))
        if cached is None:
            cached = any(
                func is not self.model.root and self.function_can_mutate(func, binding)
                for func in self._functions
            )
            self._some_mutator_cache[id(binding)] = cached
        return cached

    def is_pure_call(self, call: JsCallExpression | JsNewExpression) -> bool:
        """
        Whether evaluating *call* has no observable effect: it invokes a trusted pure intrinsic (under
        the pristine-intrinsics precondition) or a local function whose summary is pure.
        """
        callee = self._resolve_callee(call)
        if callee is _PURE:
            return True
        if isinstance(callee, Node):
            return self.summary_of(callee).is_pure
        return False

    def is_pure_call_discarded(self, call: JsCallExpression | JsNewExpression) -> bool:
        """
        Whether evaluating *call* and discarding its result has no observable effect. Like `is_pure_call`
        but resolved through `EffectSummary.is_effect_free_when_discarded`, so a callee whose only residual
        effect is a write it confines to its returned value qualifies — that write is unobservable once the
        result is thrown away. A caller may use this only in a position it has proven discards the value.
        """
        callee = self._resolve_callee(call)
        if callee is _PURE:
            return True
        if isinstance(callee, Node):
            return self.summary_of(callee).is_effect_free_when_discarded
        return False

    def call_clearable(
        self,
        call: JsCallExpression | JsNewExpression,
        callee_established: Callable[[Binding], bool],
        binding_established: Callable[[Binding], bool],
    ) -> bool:
        """
        Whether *call* may be cleared with respect to ordering — its callee in place before the
        call runs, and every outer lexical binding its body may read past that binding's
        temporal dead zone. Two ordering judgments the caller supplies, since only its layer can
        make them: *callee_established* tests the name a named local callee is read through, which
        holds the function from a point of its own, and
        *binding_established* tests a `dead_zone_reads` binding (whether the call sits after the
        binding's declaration). A trusted pure intrinsic qualifies unconditionally. Otherwise
        the callee resolves to a function, and the call is refused unless every binding the
        callee's summary defers here is `binding_established` — a dead-zone read would throw a
        `ReferenceError` the deletion of the call would silently drop, which no establishment of
        the callee itself excuses. Past that gate, an inline function-expression callee (defined
        at the call site, hence always in place) qualifies, and a call resolving to a single
        named local function qualifies when *callee_established* accepts it; an unresolved or
        ambiguous callee does not.

        The resolution, the intrinsic case, the dead-zone gate, and the inline-callee case live
        here so callers supply only the ordering judgments their layer can make. An ordering-free
        caller passes `binding_established = lambda binding: False`, which refuses any callee that
        reads an outer lexical binding. This certifies establishment ONLY, not purity — a caller
        deciding whether a call may be dropped must conjoin it with `is_pure_call`, as
        `side_effect_free` does, since an established callee may still run an effectful body.
        """
        resolved = self._resolve_callee(call)
        if resolved is _PURE:
            return True
        if isinstance(resolved, Node):
            deferred = self.summary_of(resolved).dead_zone_reads
            if deferred and not all(binding_established(binding) for binding in deferred):
                return False
            callee = call.callee
            if isinstance(strip_parens(callee), (JsFunctionExpression, JsArrowFunctionExpression)):
                return True
            name = self.model.resolve(callee) if isinstance(callee, JsIdentifier) else None
            return name is not None and callee_established(name)
        return False

    def _established_call_default(self, call: JsCallExpression | JsNewExpression) -> bool:
        """
        The ordering-free floor for `is_side_effect_free`: clears a trusted pure intrinsic, an
        inline function-expression callee (established at its call site), or a call through a name a
        function declaration hoists (empty
        `refinery.lib.scripts.js.analysis.model.SemanticModel.binding_establishment_sites`), whose
        value is in place before any statement runs. A non-hoisted named local callee — a
        `const`/`let`/`var` initializer, a bare assignment, or the copy Annex B makes — is
        refused, since this model cannot order the definition against the call; a caller that can
        supplies its own `call_established`. Being ordering-free, it passes `lambda binding: False`
        for the dead-zone gate, so a callee reading any outer lexical binding is refused here — the
        fail-closed floor a caller with a dominance model lifts by supplying its own
        `binding_established`.
        """
        return self.call_clearable(
            call,
            lambda binding: self.model.binding_establishment_sites(binding) == [],
            lambda binding: False,
        )

    def is_side_effect_free(
        self,
        node: Node,
        defunct: set[str] | None = None,
        member_safe: Callable[[JsMemberExpression], bool] | None = None,
        call_established: Callable[[JsCallExpression | JsNewExpression], bool] | None = None,
        discarded: bool = False,
        reads_may_throw: bool = False,
        read_established: Callable[[JsIdentifier], bool] | None = None,
        coercions_may_write: bool = False,
    ) -> bool:
        """
        Whether evaluating *node* can be dropped or reordered without an observable side effect, with
        the call leaf resolved through this model's `is_pure_call`: a call to a proven-pure function or
        trusted intrinsic is free, recursing into its arguments. *defunct* names bindings being removed,
        whose calls and property reads are treated as free. This is the model-aware form of the
        model-free `side_effect_free` in this module, which clears only calls to a defunct name; unlike
        it, an identifier read that resolves through a `with` body's dynamic
        scope is rejected here — reading the bare name may fire the `with` object's getter or throw (see
        `refinery.lib.scripts.js.analysis.model.SemanticModel.read_has_dynamic_effect`) — while a
        function value whose body performs such a read stays free, since defining it runs nothing. A
        caller with control-flow context passes *member_safe* to also clear a getter-free read through a
        local global-object alias it can prove established before the read; the default clears only the
        syntactic global case (`_is_trusted_global_read`).

        With *discarded* the caller asserts *node*'s own value is thrown away, so a top-level call leaf is
        cleared through `is_pure_call_discarded` and a callee that only mutates a local it returns is
        droppable — the removal contexts of `JsUnusedCodeRemoval` supply it.

        With *reads_may_throw* the read leaf is rejected not only when it fires a `with` object's getter
        but also when it may throw a `ReferenceError`, which is the throw half of the *read_effect*
        contract that `read_has_dynamic_effect` supplies only the getter half of. A caller asks for
        it of an expression whose value it drops with nothing left evaluating the read — an
        allocation discarded to answer its type or truthiness, the store-removal sweep, the
        discarded-test and dead-branch folds, the string-array dispatcher's dropped trailing
        arguments, and the reflective inliner's dropped comma-sequence prefix — so the throw that
        reading it would have raised is not muted. A pass that relocates the read into a position
        still evaluated — the object fold moving a property value to its use, an inliner
        substituting an argument into a body — keeps the throw at that position and asks this only
        of the sub-expression it truly drops (an unaccessed property, an unused parameter, a
        `typeof` operand). With *read_established* a caller passes its establishment proof — in
        every consumer the query of
        one shared `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel` — and a
        may-throw read the proof vouches a completed creating write for is free again, which is what
        lets a sweep still drop `X = 5; y = X;` whole rather than keep the read of `X` a kept store
        establishes. A proof is only ever offered against the throw it excuses, so supplying one
        asks for that half of the contract as much as *reads_may_throw* does, and neither is quietly
        ignored for want of the other.

        With *coercions_may_write* the caller drops the evaluation, so an operator that converts
        its operand is held to a value that cannot be an object — the conversion asks the operand's
        `valueOf` or `Symbol.toPrimitive`, which is code the operand carries, and the composition
        alone cannot see it. The same drop-versus-relocation divide as *reads_may_throw* applies:
        a pass that relocates the evaluation keeps the conversion where it runs and answers for it
        with its own gates. A call of a pure intrinsic converts its arguments
        (`calls_a_pure_intrinsic`), so it is held to arguments that cannot be objects as well.
        """
        read_effect = self.model.read_has_dynamic_effect
        if reads_may_throw or read_established is not None:
            read_effect = self.throwing_read_effect(read_established)
        return side_effect_free(
            node,
            defunct,
            self.is_pure_call,
            read_effect,
            member_safe or self._getter_free_read,
            call_established or self._established_call_default,
            discarded,
            self.is_pure_call_discarded,
            coercions_may_write,
            self.store_unobserved,
            self.calls_a_pure_intrinsic,
            assumptions=self.assumed_keys,
        )

    def throwing_read_effect(
        self, read_established: Callable[[JsIdentifier], bool] | None,
    ) -> Callable[[Node], bool]:
        """
        The read leaf for a caller that keeps the throw half of the contract: rejects a read that
        fires a `with` object's getter or may throw a `ReferenceError`, except one *read_established*
        vouches a creating write has certainly completed for. `is_side_effect_free` composes it for
        its own callers; a pass that reaches the module-level `side_effect_free` directly passes it
        as that function's *read_effect*, so every discarding context asks one read contract.
        """
        if read_established is None:
            return self._read_effectful_or_throwing
        return lambda node: self.read_throws(node, read_established)

    def read_throws(
        self, node: Node, read_established: Callable[[JsIdentifier], bool],
    ) -> bool:
        """
        Whether reading *node* fires a `with` object's getter or may throw a `ReferenceError`
        *read_established* does not vouch the read is past — a creating write that has completed, or
        a lexical binding whose declaration has run, ending its temporal dead zone. A read of a
        `let`/`const`/`class` binding is a candidate here even though the name resolves, since it
        throws all the same while the declaration has not run; the establishment proof clears it
        once the declaration is ordered before the read. The predicate `throwing_read_effect`
        composes, exposed directly so a caller that queries one node at a time (the reflective
        inliner) allocates no closure per read.
        """
        if self.model.read_has_dynamic_effect(node):
            return True
        if not isinstance(node, JsIdentifier):
            return False
        if not self.model.read_may_raise_reference_error(node):
            return False
        return not read_established(node)

    def _read_effectful_or_throwing(self, node: Node) -> bool:
        """
        Whether reading *node* fires a `with` object's getter or may throw a `ReferenceError` — the
        whole of the *read_effect* contract `side_effect_free` states, of which
        `read_has_dynamic_effect` alone answers the getter half. `read_may_raise_reference_error`
        asks whether the read may not resolve (a name the program neither declares nor is certain the
        host defines) or resolves to a lexical binding that may be in its temporal dead zone; with no
        establishment proof to clear the dead-zone case it fails closed, so an allocation discarded to
        answer its shape keeps the throw that reading it would have raised.
        """
        return self.model.read_has_dynamic_effect(node) or (
            isinstance(node, JsIdentifier) and self.model.read_may_raise_reference_error(node)
        )

    def binding_is_immutable_container(
        self,
        binding: Binding,
        *,
        inert_methods: frozenset[str] | None = None,
        exclude: Node | None = None,
        direct_eval_ordered: bool = False,
    ) -> bool:
        """
        Whether *binding* holds a container — an object or array — whose element and property values
        are stable after construction, so that an access into it may be soundly inlined at its read
        sites. Every reference must read through the container (`obj.k`, `obj[i]`) or plainly rebind
        the name (`obj = ...`, whose value the caller resolves by domination); a write through the
        container (`obj.k = v`, `obj[i]++`, `delete obj[i]`, a `for-of` or destructuring target)
        makes it mutable. A method invoked on the container (`obj.m(...)`) may mutate it — an
        array's `sort`/`push`/`splice` and so on — so by default it too counts as mutable, except
        a method of `Array.prototype` that only reads its receiver (`_ARRAY_READING_METHODS`)
        called on an array literal without holes while the program leaves that prototype trusted;
        a caller that knows some of the container's own methods cannot mutate it (an object literal
        with no `this`-bound property) names their keys in *inert_methods*: a call of one of those
        on the container is permitted, and so is a call on a value it holds (`obj.k.split(...)`),
        which such a caller vouches for. A method the container inherits is never one of them:
        `obj.valueOf()` hands the container back and `obj.__defineGetter__(k, g)` replaces a
        property. A reference that escapes is safe in three cases: it aliases another binding that
        is itself an immutable container (alias-following the textual predicates this replaces could
        not do, and the reason a reassigned-and-aliased lookup array stays inlinable), it is passed
        to a statically known function as an argument whose parameter is itself an immutable
        container (so the callee neither mutates nor further-escapes it), or it is the value of a
        store no code observes (`store_unobserved`). Any other escape — returned, stored as a
        property, passed to a call that cannot be resolved — is treated conservatively as mutable,
        and so is an exported binding or an alias of one: an importer holds the container and may
        change it after the module has run, which no reference in this file records. A mutation
        through a dynamic scope is modelled: a `with` body that names the container — a member
        write, method call, reassignment, or escape — is attributed to it as a dynamic reference and
        judged by the same role logic, so a `with` that never names it keeps it foldable, and a
        direct `eval` in a local container's own function makes it mutable. The one residual is a
        script-scope container reached by an opaque global surface — a direct `eval`, `Function`,
        timer, or dynamic global write whose code cannot be read — which cannot be frozen without
        also freezing the lookup arrays real samples fold, so it is left to the caller's reflection
        reasoning, the trust an unresolved external call already receives.

        The query is over a *resolved binding*, so it is shadowing-correct, and it descends through
        alias chains, callee parameters, and nested functions, so a capturing closure that mutates
        the container is caught. The answer is fixed for the model's lifetime — a binding's
        reference set does not change — so it is memoized per
        `(binding, inert_methods, direct_eval_ordered)`. A caller may pass *exclude* to
        disregard references within that subtree — asking whether the container is stable across
        the rest of the program, ignoring a read site about to be relocated into it; such a query is
        not memoized, since the answer depends on the excluded region.

        *direct_eval_ordered* is the caller's promise that it has ordered every direct-`eval` site
        of the container's owning function against the read it is folding — the definition-to-read
        span `ReachingModel.value_preserved` proves, whose kills hold those sites — so the eval
        leg does not refuse here. A caller that cannot order its reads against the sites keeps the
        default and the refusal; the promise covers the binding asked about and not the aliases
        its escapes are judged through, which keep the strict answer.
        """
        if exclude is not None:
            return self._immutable_container(
                binding, set(), inert_methods, exclude, direct_eval_ordered)
        key = (id(binding), inert_methods, direct_eval_ordered)
        cached = self._immutable_cache.get(key)
        if cached is None:
            cached = self._immutable_container(
                binding, set(), inert_methods, None, direct_eval_ordered)
            self._immutable_cache[key] = cached
        return cached

    def _immutable_container(
        self,
        binding: Binding,
        visiting: set[int],
        inert_methods: frozenset[str] | None,
        exclude: Node | None = None,
        direct_eval_ordered: bool = False,
    ) -> bool:
        key = id(binding)
        if key in visiting:
            return True
        if binding.exported:
            return False
        visiting = visiting | {key}
        if self._dynamic_scope_mutates(binding, inert_methods, exclude, direct_eval_ordered):
            return False
        for ref in self.model.references(binding, exclude=exclude):
            role = container_reference_role(ref)
            if role is ContainerRole.MEMBER_WRITE:
                return False
            if role is ContainerRole.MEMBER_CALL and not self._call_keeps_container(binding, ref, inert_methods):
                return False
            if role is ContainerRole.ESCAPE:
                if not isinstance(ref, JsIdentifier):
                    return False
                if not self._escape_keeps_container(ref, visiting, inert_methods):
                    return False
        return True

    def _call_keeps_container(
        self,
        binding: Binding,
        reference: Node,
        inert_methods: frozenset[str] | None,
    ) -> bool:
        """
        Whether the method call *reference* begins leaves the container *binding* holds as it was:
        a call its caller vouches for (`_method_call_is_inert`), or a call of one of
        `_ARRAY_READING_METHODS` where the one value of the binding is an array literal without
        holes and the program leaves `Array.prototype` trusted, so that the method called is the
        built-in and every position it reads is one the array owns.
        """
        if _method_call_is_inert(reference, inert_methods):
            return True
        if not _method_call_reads_an_array(reference):
            return False
        literal = self.model.singular_value(binding)
        return (
            isinstance(literal, JsArrayExpression)
            and all(element is not None for element in literal.elements)
            and self.trusted_prototype(list)
        )

    def _dynamic_scope_mutates(
        self,
        binding: Binding,
        inert_methods: frozenset[str] | None,
        exclude: Node | None,
        direct_eval_ordered: bool = False,
    ) -> bool:
        """
        Whether a dynamic scope may change the container *binding* holds. A direct `eval` in a
        local container's own function can rewrite it opaquely — a global is left to the caller's
        reflection reasoning, since freezing every global on any surface over-blocks — unless the
        caller has ordered the eval sites against its read (*direct_eval_ordered*), which carries
        the promise the public query documents. A `with` body's accesses are attributed by name: a
        member write, a reassignment, or an escape mutates it or may alias it out, and a method
        call may mutate it unless the caller vouches that its methods cannot; only a plain member
        read leaves it intact, so a `with` that never names the container is no threat. A dynamic
        escape or reassignment cannot be alias-followed or ordered the way a resolved one can, so
        either is treated as mutating.
        """
        if not direct_eval_ordered and self.model.local_reachable_by_direct_eval(binding):
            return True
        for ref in self.model.dynamic_references(binding, exclude=exclude):
            role = container_reference_role(ref)
            if role is ContainerRole.MEMBER_READ:
                continue
            if role is ContainerRole.MEMBER_CALL and _method_call_is_inert(ref, inert_methods):
                continue
            return True
        return False

    def _escape_keeps_container(
        self,
        ref: JsIdentifier,
        visiting: set[int],
        inert_methods: frozenset[str] | None,
    ) -> bool:
        """
        Whether an escaping reference leaves the container unmutated. Two escapes are precise: an
        alias (`var x = ref` or `x = ref`) keeps it when the aliased binding is itself an immutable
        container, and an argument passed to a statically known function (`f(ref)`) keeps it when
        the parameter it binds is itself an immutable container — interprocedural Case B, the
        parameter's own references decide whether the callee mutates or further-escapes it. A store
        no code observes (`store_unobserved`) hands the container to nobody, as long as the value of
        the assignment goes nowhere either. Every other escape is conservatively unsafe, an
        assignment that is no statement of its own among them: `var x = (y = ref)` stores the
        container in `x` as well as in `y`.
        """
        alias = self.model.name_holding(ref)
        if alias is not None:
            return self._immutable_container(alias, visiting, inert_methods)
        store = enclosing_operator(ref)
        if (
            isinstance(store, JsAssignmentExpression)
            and strip_parens(store.right) is ref
            and isinstance(target := strip_parens(store.left), JsMemberExpression)
            and self.store_unobserved(target)
        ):
            return _result_goes_nowhere(store)
        return self._argument_keeps_container(ref, visiting)

    def _argument_keeps_container(self, ref: JsIdentifier, visiting: set[int]) -> bool:
        """
        Case B: whether an argument *ref* passed to a statically known function leaves the container
        it holds unmutated — true when the parameter it binds is itself an immutable container,
        judged recursively from that parameter's own references, so the callee neither member-writes
        the argument nor lets it escape mutably. The parameter is judged with no inert method: the
        caller's *inert_methods* is its promise that the container's own methods cannot mutate it at
        the original site, and does not carry to a method the callee invokes on the argument or on
        one of its nested containers (`x.a.push(...)`), which may mutate it. False, conservatively,
        when the call cannot be analysed: the callee is not a single known function, it can reach
        the argument through its own `arguments` object, the argument is spread, a spread precedes
        it (so its runtime position shifts past the textual index and the parameter it binds cannot
        be pinned down), the slot it lands in is a rest or destructuring parameter, or the parameter
        is reachable through a `with` or direct `eval` in the callee that resolves a name at runtime
        (an unrecorded write the parameter's reference set cannot rule out). An argument with no
        parameter to bind — passed beyond the declared parameters of a function with no rest
        collector and no `arguments` reach, textual or reflective — is safe, since the callee cannot
        name it.
        """
        parent = ref.parent
        if not isinstance(parent, JsCallExpression) or ref not in parent.arguments:
            return False
        func = self.unambiguous_callee(parent)
        if func is None:
            return False
        if self._callee_uses_arguments(func):
            return False
        params = func.params
        if any(isinstance(param, JsRestElement) for param in params):
            return False
        index = parent.arguments.index(ref)
        if any(isinstance(arg, JsSpreadElement) for arg in parent.arguments[:index]):
            return False
        if index >= len(params):
            return True
        param = params[index]
        if not isinstance(param, JsIdentifier):
            return False
        binding = self.model.binding_of(param)
        if binding is None:
            return False
        if self.model.reflection_can_reach(binding):
            return False
        return self._immutable_container(binding, visiting, None)

    def _callee_uses_arguments(self, func: Node) -> bool:
        """
        Whether a non-arrow callee can reach its call's arguments through its own `arguments` object,
        which aliases the positional arguments — including any passed beyond the declared parameters — so
        that `arguments[i][...] = v` mutates a container the by-position parameter reasoning in
        `_argument_keeps_container` would otherwise miss. It is reached either by naming `arguments`
        directly, or reflectively: a `with` or a direct `eval` in the callee — or in a closure nested
        inside it, which inherits the callee's `arguments` — can read that object with no textual
        reference, so a reflectively reachable `arguments` counts too. An arrow has no `arguments` of its
        own (a reference inside it binds the enclosing function's, unrelated to the arrow's parameters),
        so it is exempt. When the callee can reach `arguments`, the escape is treated as mutable. The
        answer is a structural property of the callee, so it is memoized per function.
        """
        cached = self._uses_arguments_cache.get(id(func))
        if cached is None:
            cached = self._compute_callee_uses_arguments(func)
            self._uses_arguments_cache[id(func)] = cached
        return cached

    def _compute_callee_uses_arguments(self, func: Node) -> bool:
        if isinstance(func, JsArrowFunctionExpression):
            return False
        func_scope = self.model.parameter_scope(func)
        if func_scope is None:
            return False
        binding = func_scope.bindings.get('arguments')
        if binding is None:
            return False
        if self.model.references(binding):
            return True
        return self.model.reflection_can_reach(binding)

    def static_callee(
        self, call: JsCallExpression
    ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
        """
        The function a call invokes, resolved permissively through `function_of`: a direct function or
        arrow expression callee, or an identifier bound to a single function — a declaration, a
        `var`/`let`/`const` initializer, or the value a name is assigned exactly once. For a name that
        held a value and was then reassigned this returns the post-reassignment value, which is the
        running target only where that reassignment is established before the call; a consumer that
        cannot order the reassignment against the call must use `unambiguous_callee` instead. `None` for
        a method call, a parameter, a redeclared or dynamically-rebindable binding, or an unresolved name.
        """
        callee = call.callee
        if isinstance(callee, (JsFunctionExpression, JsArrowFunctionExpression)):
            return callee
        if not isinstance(callee, JsIdentifier):
            return None
        return self.function_of(self.model.resolve(callee))

    def a_name_this_file_binds_holds_the_callee(self, call: JsCallExpression) -> bool:
        """
        Whether *call* names its callee with an identifier this file binds, while `static_callee`
        declines to say which function that binding holds.

        A caller reasoning about what a call may have done reads `static_callee` answering `None` in
        two ways, and this tells them apart. Where the callee is a method, a host function, a name
        nothing here declares, or one holding what the host passed
        (`refinery.lib.scripts.js.analysis.model.SemanticModel.supplied_from_outside`), `None` means
        the call runs something outside this file's reckoning, which is a standing condition every
        such caller was written under. Where it is a name this file binds, `None` means the model
        saw the binding and would not state its value - one Annex B copies into a block's enclosing
        scope is such a function - and what it runs may be any of the ones written here, one that
        writes the very binding being reasoned about included.
        """
        callee = strip_parens(call.callee)
        if not isinstance(callee, JsIdentifier):
            return False
        if self.model.supplied_from_outside(self.model.resolve(callee)):
            return False
        return self.static_callee(call) is None

    def unambiguous_callee(
        self, call: JsCallExpression
    ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
        """
        The ordering-free twin of `static_callee`, for a consumer that reasons about a call without
        knowing where it sits in execution order. Identical except an identifier callee resolves through
        `unambiguous_function`, so a name that held a value and was then reassigned — whose running target
        depends on the call's position relative to the reassignment — yields `None` rather than the
        post-reassignment value.
        """
        return unambiguous_callee(self.model, call)

    def _collect_functions(self) -> list[Node]:
        functions: list[Node] = [self.model.root]
        for node in self.model.root.walk():
            if owns_a_run(node):
                functions.append(node)
        return functions

    def _compute(self):
        """
        The least fixpoint of the function summaries, by worklist: every function is scanned once, and a
        scan that changes a summary re-queues only the functions that call the changed one. An edge is
        recorded on every scan, including one that changes no summary, so the edge exists by the time its
        callee first changes even when the caller was scanned before it.
        """
        functions: dict[int, Node] = {}
        for func in self._functions:
            fid = id(func)
            functions[fid] = func
            self._summaries[fid] = EffectSummary()
        edges = _CallEdges()
        queue = deque(functions.values())
        queued = set(functions)
        while queue:
            func = queue.popleft()
            queued.discard(id(func))
            edges.caller = id(func)
            summary = self._scan(func, edges.record)
            if summary == self._summaries[id(func)]:
                continue
            self._summaries[id(func)] = summary
            for caller in edges.callers.get(id(func), ()):
                if caller not in queued:
                    queued.add(caller)
                    queue.append(functions[caller])

    def _scan(
        self,
        func: Node,
        record_call: Callable[[Node], None] | None = None,
    ) -> EffectSummary:
        summary = EffectSummary()
        if isinstance(func, FUNCTION_NODES) and wraps_return(func):
            summary.wraps_return = True
        for node in walk_run(func):
            if isinstance(node, JsThrowStatement):
                summary.throws = True
            elif isinstance(node, JsIdentifier):
                if not summary.throws and self.model.read_may_throw(node):
                    summary.throws = True
                lexical_read = self.model.lexical_binding_read(node)
                if lexical_read is not None:
                    summary.dead_zone_reads.add(lexical_read)
                if reference_role(node) is not Role.READ:
                    self._account_write(summary, node, func)
            elif isinstance(node, JsMemberExpression):
                base = node.object
                if base is not None and not self._base_is_safe(base):
                    summary.throws = True
                if is_member_write_target(node):
                    write_class = self._member_write_class(node, func)
                    if write_class is _WriteClass.OBSERVABLE:
                        summary.writes_global = True
                    elif write_class is _WriteClass.VIA_RESULT:
                        summary.mutates_returned_local = True
                elif base is not None and not self._getter_free_read(node):
                    summary.calls_unknown = True
            elif isinstance(node, (JsCallExpression, JsNewExpression, JsTaggedTemplateExpression)):
                self._account_call(summary, node, record_call)
            elif isinstance(node, JsImportExpression):
                summary.calls_unknown = True
        if func is not self.model.root and not (
            isinstance(func, FUNCTION_NODES) and is_generator_function(func)
        ):
            for binding in summary.dead_zone_reads:
                if (
                    self._owns_binding(binding, func)
                    and not self._binding_reads_past_dead_zone(binding)
                ):
                    summary.throws = True
            summary.dead_zone_reads = {
                binding
                for binding in summary.dead_zone_reads
                if not self._owns_binding(binding, func)
            }
        else:
            summary.dead_zone_reads = set()
        if func is not self.model.root:
            owned = {
                binding
                for binding in summary.captured_writes | summary.written_bindings
                if self._owns_binding(binding, func)
            }
            if owned and self._keeps_its_closures(func):
                summary.captured_writes = summary.captured_writes - owned
                summary.written_bindings = summary.written_bindings - owned
        return summary

    def _keeps_its_closures(self, func: Node) -> bool:
        """
        Whether no code outside the run of *func* that made a function written inside it can call
        that function: each runs only at the call where it stands (`_runs_where_it_stands`), or only
        where its name is called (`function_escapes`), which no code outside that run can reach. A
        write such a function makes to a local of *func* is then one to the local of the run that
        made it, which no caller of *func* sees, and the summary of *func* leaves it out; an `async`
        body an `await` suspends resumes in that run, called by no one. A function stored where a
        later run can call it writes the local of the run that made it:

            var saved; function f(n) { var t = 0; if (n) saved = function () { t++; }; ... }

        Memoized per function.
        """
        cached = self._keeps_closures_cache.get(func)
        if cached is None:
            cached = self._keeps_closures_cache[func] = all(
                self._runs_where_it_stands(nested) or not self.function_escapes(nested)
                for nested in func.walk()
                if nested is not func and owns_a_run(nested)
            )
        return cached

    def _runs_where_it_stands(self, func: Node) -> bool:
        """
        Whether *func* runs at the one call it is the callee of, written where it is called, and
        nowhere else: it is no generator, whose body would run at the steps of its object instead;
        nothing reaches it without its name
        (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`); and the
        name a function expression gives itself is only ever called.
        """
        call = enclosing_operator(func)
        if (
            not isinstance(call, JsCallExpression)
            or strip_parens(call.callee) is not func
            or not isinstance(func, FUNCTION_NODES)
            or is_generator_function(func)
            or self.model.reached_without_its_name(func)
        ):
            return False
        if not isinstance(func, JsFunctionExpression) or func.id is None:
            return True
        own = self.model.binding_of(func.id)
        return own is not None and not own.dynamic_refs and all(_is_called_by_name(read) for read in own.reads)

    def _binding_reads_past_dead_zone(self, binding: Binding) -> bool:
        """
        Whether every read of *binding* is guaranteed to run past its temporal dead zone, so no call
        that reaches one raises the `ReferenceError` a read before the declaration would. The reads
        are enumerated from the binding itself rather than the scan that reached it: a function that
        owns the binding may read it directly or only through a nested function whose summary the
        scan absorbed, and the question — does some read run before every declaration of it — is the
        one `DominanceModel.past_dead_zone` answers per read, ordering the read against its
        declaration across the nested call the same way the call-site gate orders an outer binding
        against the call. Answered once per binding: it is a pure function of the binding and the
        model, both fixed for this model's lifetime.

        A function's generator is the one shape that never asks: its body runs at an iteration
        rather than the call, so a dead-zone read inside it is not an effect the call carries.
        """
        cached = self._dead_zone_safe_cache.get(id(binding))
        if cached is not None:
            return cached
        dominance = self._dominance_model()
        cached = all(
            dominance.past_dead_zone(binding, read)
            for read in binding.reads
        )
        self._dead_zone_safe_cache[id(binding)] = cached
        return cached

    def _dominance_model(self) -> DominanceModel:
        """
        The dominance model this one was built with, or one built over the same semantic model
        where none was given.
        """
        if self._dominance is None:
            self._dominance = build_dominance(self.model, intrinsic_writes=self.intrinsic_writes)
        return self._dominance

    def _account_write(self, summary: EffectSummary, target: JsIdentifier, func: Node):
        binding = self.model.resolve(target)
        if binding is None:
            summary.writes_global = True
            return
        if self._owns_binding(binding, func):
            return
        if binding.is_read:
            summary.written_bindings.add(binding)
        if self._write_unobservable(binding):
            return
        if binding.kind is BindingKind.IMPLICIT_GLOBAL or binding.scope is self.model.root_scope:
            summary.writes_global = True
        else:
            summary.captured_writes.add(binding)

    def _write_unobservable(self, binding: Binding) -> bool:
        """
        Whether assigning *binding* has no observable consumer, so the assignment is not counted as
        a write: nothing reads the value of the binding, a `delete` of the name reading none
        (`refinery.lib.scripts.js.analysis.model.is_delete_operand`), the binding is not
        exported, which an importer reads across the module boundary, and the program is
        `global_pristine`: it exposes no reflection surface through which the name could be read
        and installs no accessor that an assignment to a global property could trigger as a setter.
        This ports the evaluator's sound permissiveness for an obfuscator's write-only scratch
        binding.

        A binding every reference of which lies in one function is observed all the same: a later
        run of the function reads what an earlier one stored, and a run that calls the function
        again reads what the inner run stored. In

            var done = false; function init() { if (done) return 1; done = true; return 2; }

        the second call of `init` returns `1` because the first one stored `true`. Whether every
        run reads only what it stored itself is the question
        `refinery.lib.scripts.js.analysis.localization` answers, and once it holds, that pass moves
        the declaration into the function, whose own binding a write to is not counted at all.
        """
        if not self.global_pristine or binding.exported:
            return False
        return all(is_delete_operand(read) for read in binding.reads)

    def _member_write_class(self, member: JsMemberExpression, func: Node) -> _WriteClass:
        """
        How observable the container written by *member* (`base.k = v`, `base[i]++`, `delete base[i]`) is
        to code outside *func* — the distinction that lets a mutation of an obfuscator's scratch container
        be tolerated without weakening purity. The base must be a fresh value: written directly on an
        object/array/function literal, or resolving to a binding *func* owns whose value is always freshly
        built — a rest parameter, which the language guarantees is a new array, or a local initialized
        only to an object/array/function literal. An object literal with an own setter — or one that
        installs a custom prototype through `__proto__:`, which may carry an inherited setter — does NOT
        qualify, since the write then runs an accessor a caller can observe. A plain parameter does NOT
        qualify either: it aliases the caller's object, so `function modify(a){ a[0] = 9; }` mutates the
        argument observably — the soundness boundary this rests on. Ownership is the exact test
        `_account_write` uses for a plain-identifier write (`func_scope.contains(binding.scope)` and not
        global), so a binding captured *from an enclosing scope* is not owned and its mutation stays
        `OBSERVABLE`, matching that a call mutating an outer local is a visible effect.

        For an owned fresh container the outcome splits on how it escapes. When no reference lets it out
        (`_container_non_escaping`) and no nested function captures it, the write is `UNOBSERVABLE` — the
        container dies with the call and no caller can ever reach it, so a function whose only effect is
        the mutation is pure. Otherwise the container — or a closure over it — leaves *func*, but every
        escape route other than the return value independently sets a blocking flag on the summary (a
        store to a global or captured binding, a leak into an unknown callee, a throw), so the only
        unflagged escape is `return`, whose value the caller may discard: the write is then `VIA_RESULT`,
        seen only if that value is used.

        A write hidden behind a dynamic scope — through a name a `with` body or direct `eval` resolves at
        runtime — is `OBSERVABLE`: the base resolves to no binding, so the write is conservatively kept,
        which is sound. The residual is the opaque-surface one `binding_is_immutable_container` documents:
        a reflective surface whose code cannot be read could install a prototype accessor that observes a
        write this deems unobservable, and freezing on it would refuse the obfuscator idioms this is meant
        to see through, so it is left to that boundary.

        The judgment is structural — fixed by the binding's declarations and reference set — so it is
        invariant across the fixpoint passes that recompute the summaries, and is memoized per member.
        """
        cached = self._member_write_cache.get(id(member))
        if cached is None:
            cached = self._classify_member_write(member, func)
            self._member_write_cache[id(member)] = cached
        return cached

    def _classify_member_write(self, member: JsMemberExpression, func: Node) -> _WriteClass:
        if self.store_unobserved(member):
            return _WriteClass.UNOBSERVABLE
        base = member.object
        if isinstance(base, (JsArrayExpression, JsFunctionExpression)):
            return _WriteClass.UNOBSERVABLE
        if isinstance(base, JsObjectExpression):
            if object_member_access_runs_accessor(base):
                return _WriteClass.OBSERVABLE
            return _WriteClass.UNOBSERVABLE
        if not isinstance(base, JsIdentifier):
            return _WriteClass.OBSERVABLE
        binding = self.model.resolve(base)
        if binding is None or not self._owns_binding(binding, func):
            return _WriteClass.OBSERVABLE
        if not self._fresh_container_origin(binding, func):
            return _WriteClass.OBSERVABLE
        if not binding.captured and self._container_non_escaping(binding):
            return _WriteClass.UNOBSERVABLE
        return _WriteClass.VIA_RESULT

    def store_unobserved(self, member: JsMemberExpression) -> bool:
        """
        Whether the store whose target is *member* is observed by no code: it is the store
        `module.exports = v` through the `module` the host passed, and nothing reads what the file
        stores there (`SemanticModel.module_exports_unread`). The host made `exports` a data
        property of its own on that object, so the store runs no setter and cannot fail; under the
        reading as an ES module, where the name may be absent, the read of `module` keeps its own
        throw.
        """
        if not is_plain_store_target(member):
            return False
        base = strip_parens(member.object)
        if not isinstance(base, JsIdentifier) or base.name != 'module':
            return False
        if not key_is(member, _EXPORTS, NO_IS_STRICTER):
            return False
        binding = self.model.resolve(base)
        if binding is None or binding is not self.model.root_scope.bindings.get('module'):
            return False
        return self.model.module_exports_unread()

    def _owns_binding(self, binding: Binding, func: Node) -> bool:
        """
        Whether *binding* is declared within *func* rather than reaching in from an enclosing scope or the
        global object — the exact ownership test `_account_write` applies to a plain-identifier write, so a
        mutation of an owned local and a mutation of its name agree on observability. A binding *func* owns
        has all its references inside *func*'s subtree, so the summary scan sees every one of its escapes.

        A function whose parameter list holds an expression introduces its parameters and its own
        name in scopes standing *outside* its body's, so containment alone would read a write to its
        own parameter as a write reaching in from elsewhere. `Scope.closure_home` says those scopes
        are the same call as the body, and answers for all three shapes a function is built in.
        """
        if binding.kind is BindingKind.IMPLICIT_GLOBAL or binding.scope is self.model.root_scope:
            return False
        func_scope = self.model.function_scope(func)
        if func_scope is None:
            return False
        return func_scope.contains(binding.scope) or binding.scope.closure_home is func_scope

    def _fresh_container_origin(self, binding: Binding, func: Node) -> bool:
        """
        Whether *binding* only ever holds a container freshly built inside *func*, so a member write through
        it cannot be observed anywhere else. The binding-level face of `_fresh_kind`; see that method.
        """
        return self._binding_fresh_kind(binding, func, frozenset()).is_fresh

    def _binding_fresh_kind(
        self, binding: Binding, func: Node, visiting: frozenset[int]
    ) -> _FreshKind:
        """
        The kind of container *binding* is known to hold on every path, judged from inside *func*. A rest
        parameter is a fresh array by language guarantee. Otherwise the binding must be one *func* owns — a
        name reaching in from an enclosing scope or the global object denotes a container other code can
        already reach, however freshly its initializer built it — and *every* value it can take must be
        fresh: each declaration's initializer and each later write, because a name that holds an outer
        container even once makes a write through it observable there. A binding written through a pattern
        rather than a plain assignment target has no single value expression to judge, so it fails.

        A binding whose `constructor` or `__proto__` is written anywhere is never an `ARRAY`, however it was
        built. Those two properties decide what an allocating `Array.prototype` method actually returns:
        `slice` and its neighbours route through ArraySpeciesCreate, which reads
        `constructor[Symbol.species]` off the receiver, so a program that writes either one can make the
        "new" array be a shared object — or make the call throw, by leaving a primitive there. The container
        is still fresh, so a write *into* it stays unobservable; it is only the array guarantee that is lost.

        The kind is the weakest of the values, since a consumer may only rely on what holds for all of them.
        """
        if self._is_rest_param(binding):
            return _FreshKind.ARRAY
        if binding.kind not in (BindingKind.VAR, BindingKind.LET, BindingKind.CONST):
            return _FreshKind.NOT_FRESH
        if not self._owns_binding(binding, func):
            return _FreshKind.NOT_FRESH
        if not binding.declarations or id(binding) in visiting:
            return _FreshKind.NOT_FRESH
        visiting = visiting | {id(binding)}
        kind = _FreshKind.ARRAY
        if self._species_written(binding):
            kind = _FreshKind.CONTAINER
        for decl in binding.declarations:
            declarator = decl.parent
            if not isinstance(declarator, JsVariableDeclarator):
                return _FreshKind.NOT_FRESH
            kind = _weakest(kind, self._fresh_kind(declarator.init, func, visiting))
            if not kind.is_fresh:
                return _FreshKind.NOT_FRESH
        for ref in self.model.references(binding):
            if reference_role(ref) is Role.READ:
                continue
            parent = ref.parent
            if not isinstance(parent, JsAssignmentExpression) or parent.left is not ref:
                return _FreshKind.NOT_FRESH
            if parent.operator != '=':
                return _FreshKind.NOT_FRESH
            kind = _weakest(kind, self._fresh_kind(parent.right, func, visiting))
            if not kind.is_fresh:
                return _FreshKind.NOT_FRESH
        return kind

    def _species_written(self, binding: Binding) -> bool:
        """
        Whether any reference to *binding* writes a property that decides what an allocating
        `Array.prototype` method returns. Only `constructor` and `__proto__` do: ArraySpeciesCreate reads
        `constructor[Symbol.species]` off the receiver, and `__proto__` replaces the prototype the
        `constructor` lookup walks.

        A key not known counts, because it may name either one. That conservatism is affordable
        here and would not be in a whole-program rule: the question is asked about the handful of references
        to one binding, so an ordinary `s[i] = v` element write on a *different* binding is untouched. It is
        also the direction that survives constant folding — a fold turns `a['const' + 'ructor']` into
        `a.constructor`, moving the answer from unsafe to unsafe rather than from safe to unsafe.
        """
        for ref in self.model.references(binding):
            parent = ref.parent
            if not isinstance(parent, JsMemberExpression) or parent.object is not ref:
                continue
            if not is_member_write_target(parent):
                continue
            if key_may_be(parent, SPECIES_KEYS):
                return True
        return False

    def _fresh_kind(self, node: Node | None, func: Node, visiting: frozenset[int]) -> _FreshKind:
        """
        The kind of container the expression *node* is known to build when evaluated inside *func*, or
        `NOT_FRESH` when it may evaluate to something other code can already reach. This is a *must* analysis:
        it answers only for expressions whose result is provably a new object, which is what lets a member
        write through the result be classified as unobservable.

        Five forms qualify. A container literal builds its value on the spot — unless its member access runs an
        accessor, the shared `container_literal_access_is_plain` test. An identifier resolves through its
        binding, which *func* must own. An allocating `Array.prototype` method
        (`_FRESH_ARRAY_RESULT_METHODS`) returns a new array, but only when the prototype is undisturbed and
        the receiver is itself known to be an array: a fresh object literal carrying its own `slice` is not,
        and neither is a value of unknown type such as a plain parameter. A call the model resolves to one
        function qualifies when every `return` in that function yields a fresh container — and a function with
        a path that returns no value does not, since that path yields `undefined`. `new Array(...)` is
        deferred to `_pure_construct`, which owns the argument rule for that root.

        Deliberately *not* a may-allocate analysis. `JsObjectFold._value_allocates` asks the opposite
        question — whether an expression might mint an object whose identity a fold would duplicate — and
        answers `True` for any nested call, where this answers `NOT_FRESH` for nearly all of them. The two are
        not monotone in one another and must not be merged.
        """
        node = strip_parens(node)
        if node is None:
            return _FreshKind.NOT_FRESH
        if isinstance(node, JsArrayExpression):
            return _FreshKind.ARRAY
        if isinstance(node, (JsObjectExpression, JsFunctionExpression, JsArrowFunctionExpression)):
            return _FreshKind.CONTAINER if container_literal_access_is_plain(node) else _FreshKind.NOT_FRESH
        if isinstance(node, JsIdentifier):
            binding = self.model.resolve(node)
            if binding is None:
                return _FreshKind.NOT_FRESH
            return self._binding_fresh_kind(binding, func, visiting)
        if isinstance(node, JsNewExpression):
            return _FreshKind.ARRAY if self._pure_construct(node) else _FreshKind.NOT_FRESH
        if isinstance(node, JsCallExpression):
            return self._call_fresh_kind(node, func, visiting)
        return _FreshKind.NOT_FRESH

    def _call_fresh_kind(
        self, call: JsCallExpression, func: Node, visiting: frozenset[int]
    ) -> _FreshKind:
        callee = strip_parens(call.callee)
        if isinstance(callee, JsMemberExpression) and not callee.computed:
            if not key_is(callee, _FRESH_ARRAY_RESULT_METHODS, NO_IS_STRICTER):
                return _FreshKind.NOT_FRESH
            if not self.trusted_prototype(list):
                return _FreshKind.NOT_FRESH
            if self._fresh_kind(callee.object, func, visiting) is not _FreshKind.ARRAY:
                return _FreshKind.NOT_FRESH
            return _FreshKind.ARRAY
        if isinstance(callee, JsIdentifier):
            callee_func = self.unambiguous_function(self.model.resolve(callee))
            if callee_func is None or id(callee_func) in visiting:
                return _FreshKind.NOT_FRESH
            visiting = visiting | {id(callee_func)}
            returns = [n for n in walk_run(callee_func) if isinstance(n, JsReturnStatement)]
            if not returns or not _returns_on_every_path(callee_func):
                return _FreshKind.NOT_FRESH
            kind = _FreshKind.ARRAY
            for statement in returns:
                kind = _weakest(kind, self._fresh_kind(statement.argument, callee_func, visiting))
                if not kind.is_fresh:
                    return _FreshKind.NOT_FRESH
            return kind

        return _FreshKind.NOT_FRESH

    @staticmethod
    def _is_rest_param(binding: Binding) -> bool:
        """
        Whether *binding* is a function's rest parameter (`function f(...xs)`), whose value the language
        guarantees is a fresh array on every call.
        """
        return binding.kind is BindingKind.PARAM and any(
            isinstance(decl.parent, JsRestElement) for decl in binding.declarations
        )

    def _container_non_escaping(self, binding: Binding) -> bool:
        """
        Whether every reference to *binding* keeps its container contained: each is a member read or
        write (`obj.k`, `obj[i] = v`), a verdict read (`!obj`, `if (obj)`) that consumes the value
        whole and forwards nothing, or — failing those — an escape, rebinding, or method call
        through which the container could be aliased out, mutated by other code, or replaced. The
        tightest form of the escape check, since a mutation only stays unobservable while no other
        code can reach the object.

        Orthogonal to freshness, and deliberately not merged with `_binding_fresh_kind`: this asks where a
        container *goes*, that asks where it *came from*. Both are needed and neither implies the other — a
        fresh literal can escape, and a parameter that never escapes was still not built here.
        `_classify_member_write` is where the two compose.
        """
        for ref in self.model.references(binding):
            if container_reference_role(ref) not in (
                ContainerRole.MEMBER_READ, ContainerRole.MEMBER_WRITE, ContainerRole.VERDICT_READ,
            ):
                return False
        return True

    def _account_call(
        self,
        summary: EffectSummary,
        call: JsCallExpression | JsNewExpression | JsTaggedTemplateExpression,
        record_call: Callable[[Node], None] | None = None,
    ):
        """
        Absorb into *summary* what *call* runs: a call, a construction, or a tagged template, which
        calls its tag with the template's parts.
        """
        callee = self._resolve_callee(call)
        if callee is _PURE:
            return
        if isinstance(callee, Node):
            if record_call is not None:
                record_call(callee)
            summary.absorb(self.summary_of(callee))
        else:
            summary.calls_unknown = True

    def _resolve_callee(
        self,
        call: JsCallExpression | JsNewExpression | JsTaggedTemplateExpression,
    ) -> Node | _PureCall | None:
        callee = call.tag if isinstance(call, JsTaggedTemplateExpression) else call.callee
        if isinstance(call, JsNewExpression) and self._pure_construct(call):
            return _PURE
        if isinstance(callee, (JsFunctionExpression, JsArrowFunctionExpression)):
            return callee
        if self.calls_a_pure_intrinsic(call):
            return _PURE
        if isinstance(callee, JsIdentifier):
            return self.unambiguous_function(self.model.resolve(callee))
        return None

    def calls_a_pure_intrinsic(
        self,
        call: JsCallExpression | JsNewExpression | JsTaggedTemplateExpression,
    ) -> bool:
        """
        Whether *call* invokes one of the trusted pure intrinsic functions, `PURE_INTRINSIC_METHODS`
        and `PURE_GLOBAL_FUNCTIONS`. Each of them converts the arguments it is handed, and converting
        an object runs code the program may have installed, as `parseInt([5])` runs
        `Array.prototype.join`; the purity of the call covers the intrinsic and not that conversion.
        """
        callee = call.tag if isinstance(call, JsTaggedTemplateExpression) else call.callee
        if isinstance(callee, JsMemberExpression) and not callee.computed:
            base = callee.object
            return (
                isinstance(base, JsIdentifier)
                and key_is(callee, _PURE_METHODS_OF.get(base.name, frozenset()), NO_IS_STRICTER)
                and self._is_global_intrinsic(base)
            )
        return (
            isinstance(callee, JsIdentifier)
            and callee.name in PURE_GLOBAL_FUNCTIONS
            and self._is_global_intrinsic(callee)
        )

    def _pure_construct(self, call: JsNewExpression) -> bool:
        """
        Whether `new <callee>(...)` is a pure allocation: the callee denotes a pristine constructor root in
        `_PURE_CONSTRUCTOR_ROOTS` and its arguments are safe for that root. `Array` — the only such root
        today — throws only on a bad single numeric length, decided by `_array_construct_is_pure`; a root
        added to the set needs its own argument rule wired in here rather than reusing Array's.

        Purity of the construction, not freshness of its result: those are separate questions, and this stays
        separate from `_fresh_kind` even though that predicate's `new Array(n)` form calls it. A construction
        can be impure and still yield a fresh object, so a caller wanting freshness must ask `_fresh_kind`.
        """
        root = self.intrinsic_of(call.callee)
        if not (isinstance(root, str) and root in _PURE_CONSTRUCTOR_ROOTS):
            return False
        return _array_construct_is_pure(call.arguments)

    def function_of(
        self, binding: Binding | None
    ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
        """
        The single function a *binding* stably resolves to — a sole declaration's function declaration or
        function/arrow initializer, or a name assigned a function exactly once (`f = function(){}`, the
        form namespace flattening leaves) — or `None` when the binding is absent, redeclared, reassigned
        to more than one value, dynamically rebindable, or not bound to a function. A lone assignment
        counts because the name denotes that one function wherever it is not in the value's temporal dead
        zone; a caller that also needs the value established before a use orders it separately. The
        binding-level twin of `static_callee`, and the function-typed specialization of
        `SemanticModel.singular_value`: it filters that value-resolution to a function node.
        """
        value = self.model.singular_value(binding)
        if isinstance(value, FUNCTION_NODES):
            return value
        return None

    def unambiguous_function(
        self, binding: Binding | None
    ) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
        """
        The single function *binding* names for a consumer that resolves calls without execution ordering
        — the interpreter — or `None`. A name that carried a value from its declaration and is then
        reassigned holds two values across its life and is refused; since `SemanticModel.singular_value`
        took the complete-singleton contract, that refusal is the query's own, and this answer equals
        `function_of`. The name survives for the intent it documents at its call sites — the ordering-free
        view the evaluator's visible-functions map applied before interpretation routed resolution
        through the model.
        """
        return unambiguous_function(self.model, binding)

    def _is_global_intrinsic(self, name: JsIdentifier) -> bool:
        """
        Whether *name* denotes a trusted intrinsic root that the program leaves pristine and does not
        shadow with a local binding at this use site.
        """
        if not self.intrinsics_pristine:
            return False
        return self.model.lookup(name.name, self.model.scope_of(name)) is None

    def intrinsic_of(self, node: Node | None) -> str | _GlobalObject | None:
        """
        The pristine intrinsic value *node* provably denotes: `GLOBAL_OBJECT` for the global object,
        an intrinsic root name (`'Array'`, `'String'`, …) for a named intrinsic, or `None`. A name
        is returned only under `intrinsics_pristine` and where the identifier is unshadowed at this
        use site, so the result may be *value-trusted* — used to construct, to clear a getter-free
        static read, or to fold `A || B`. Every value it can return — `globalThis` and every
        `refinery.lib.scripts.js.analysis.intrinsics.PURE_INTRINSIC_ROOTS` member — is truthy, so
        `A || B` evaluates to `A` whenever `intrinsic_of(A)` is not `None`; a contributor extending
        this must preserve that truthiness invariant and never return a falsy name such as
        `NaN`/`undefined`.

        It deliberately does NOT follow a local alias through its value — `intrinsic_of` of an identifier
        bound to `var x = Array` is `None` — because a local's value holds only where it is established, a
        control-flow fact this flow-insensitive query cannot certify; a consumer that owns dominance
        resolves the alias itself against `singular_value`. It likewise does not treat `<global-object>.Name`
        as a value: that read's getter-freeness rests on `global_pristine`, a weaker premise than value
        trust, so it stays the concern of `_is_trusted_global_read`.
        """
        node = strip_parens(node)
        if isinstance(node, JsIdentifier):
            if node.name == 'globalThis' and self.model.lookup(node.name, self.model.scope_of(node)) is None:
                return GLOBAL_OBJECT
            if node.name in PURE_INTRINSIC_ROOTS and self._is_global_intrinsic(node):
                return node.name
            return None
        if isinstance(node, JsLogicalExpression) and node.operator == '||':
            return self.intrinsic_of(node.left)
        return None

    def trusted_intrinsic(self, node: Node | None) -> str | None:
        """
        The global name *node* denotes, when that one name is provably still the built-in, or `None`. A
        name qualifies when the program never binds it, never assigns to it, never writes or updates a
        property anywhere on it, and neither exposes a reflection surface through which it could be
        replaced at runtime nor stores a property on the global object under a runtime key
        (`SemanticModel.has_opaque_global_write`), which may replace that name's value the same way.

        This differs from `intrinsic_of` in *scope of the question*, not in strictness. `intrinsic_of`
        rests on `intrinsics_pristine`, one program-wide flag over a fixed root set, so a single
        `Object.prototype.x = 1` withdraws trust from every intrinsic in the file — including `Math`,
        which that line cannot affect. This query answers per name, so the same program still folds
        `Math.floor` while declining `Object.keys`. Its callers are the constant folds, which need to know
        whether *this* built-in is intact; `intrinsic_of` answers the stronger question of whether a value
        may be trusted for construction or `A || B` folding, and keeps its own callers and vocabulary.

        The name is not checked against a list of blessed intrinsics: whether a fold *knows how* to
        evaluate the call is the caller's question, answered by its own registry lookup. Conflating the two
        is what let a registry entry exist with no matching trust rule.

        Shadowing needs no per-site scope resolution, because a name bound *anywhere* in the program is
        already disqualified: `_globals_written` collects every binding in every scope. That is stricter
        than JavaScript requires — a shadow inside an unrelated function does not affect this use site —
        and deliberately so. A name the program binds at all is one an obfuscator may be routing values
        through, and the price of refusing is an unfolded call rather than a wrong value.
        """
        node = strip_parens(node)
        if not isinstance(node, JsIdentifier):
            return None
        if not self.trusted_intrinsic_name(node.name):
            return None
        return node.name

    def program_trust(self) -> AnchoredTrust:
        """
        The trust questions of a fold no point is given for, each answered for the whole program:
        the oracle vouches for no anchor, and a load of code anywhere may precede the fold.
        """
        model = self.model
        return AnchoredTrust(self, lambda: False, lambda: model.imports_code() or bool(model.code_load_sites()))

    def trusted_intrinsic_name(self, name: str) -> bool:
        """
        `EffectModel.trusted_intrinsic` for a caller that holds a global name rather than a node
        spelling it: a regular-expression literal builds its object from `RegExp` without naming it,
        so a fold deciding what `/a/g` matches has no identifier to ask about.
        """
        if self.model.has_reflection_surface() or self.model.has_opaque_global_write():
            return False
        return name not in self._globals_written

    def intrinsic_name_unwritten(self, node: Node | None) -> str | None:
        """
        The per-name arm of `trusted_intrinsic`: the global name *node* denotes when the program
        never binds it, never assigns to it, and never writes or updates a property anywhere on it.
        The question a consumer that holds an oracle clearance composes with it — the oracle
        answered the reflection terms at the anchor, so the name question is what remains. A
        consumer without one takes `trusted_intrinsic`, which is this plus those terms.
        """
        node = strip_parens(node)
        if not isinstance(node, JsIdentifier):
            return None
        if node.name in self._globals_written:
            return None
        return node.name

    def prototype_name_unwritten(self, value_type: type) -> bool:
        """
        The per-name arm of `trusted_prototype`: whether the intrinsic owning *value_type*'s
        prototype was never written. The question a consumer that holds an oracle clearance
        composes with it; a consumer without one takes `trusted_prototype`, which is this plus its
        reflection term.
        """
        owner = PROTOTYPE_OWNERS.get(value_type.__name__)
        return owner is not None and owner not in self._globals_written

    def trusted_prototype(self, value_type: type) -> bool:
        """
        Whether the prototype supplying *value_type*'s methods is provably unmodified, so a method call on
        a receiver of that type still means what the language says. A method call on a literal receiver
        names no global, which is exactly why it needs its own question: `trusted_intrinsic` can only
        judge names the expression mentions, and `'ab'.toUpperCase()` mentions none.

        The owning intrinsic is looked up rather than assumed, and then asked the same per-name question a
        named callee gets — `String.prototype.toUpperCase = f` and
        `Object.defineProperty(Array.prototype, ...)` are both already recorded as writes to `String` and
        `Array` by `refinery.lib.scripts.js.analysis.intrinsics.build_intrinsic_writes`. A type with
        no known owner is never trusted.
        """
        owner = PROTOTYPE_OWNERS.get(value_type.__name__)
        if owner is None:
            return False
        if self.model.has_reflection_surface():
            return False
        return self.prototype_name_unwritten(value_type)

    def global_key_written(self, name: str, key: str) -> bool:
        """
        Whether the program writes the property *key* on a chain rooted at the global *name*:
        `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor
        installed for that key. The per-name question `_globals_written` answers is too coarse for a
        caller that cares which property was replaced — a file patching `Object.prototype.z` has
        written `Object`, and refusing everything about `Object` on that basis refuses the very
        files the question is asked about.

        The write is attributed to a *name* rather than to a receiver, which is what makes it
        answerable at all: the receiver of `Object.prototype.constructor = C` is a value no static
        analysis names, while the chain it is written through is rooted in one that is. A name whose
        written keys cannot be bounded — one the program binds, hands to code this analysis cannot
        read, writes a computed key on, or installs a descriptor on from a value it cannot read —
        answers `True` for every key, and so does a name outside
        `refinery.lib.scripts.js.analysis.intrinsics.KEYED_WRITE_ROOTS`, which the scan records
        nothing about at all.
        """
        return self.intrinsic_writes.key_written(name, key)

    def builtin_writes_followed(self) -> bool:
        """
        Whether every write the program makes on a built-in object is one the questions about names
        see (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.followable`). A fold that
        reads a built-in's chain or converts an object without spelling the owner of either asks
        this beside `read_chain_intact`: `[].constructor.prototype.join = f` replaces the join of
        every array while writing `Array` by no name. A no is noted on the run's record
        (`refinery.lib.scripts.js.analysis.intrinsics.UnfollowedWriteRefusals`).
        """
        followed = self.intrinsic_writes.followable
        if not followed and self._refusals is not None:
            self._refusals.note()
        return followed

    def builtin_function_text(self, function: BuiltinFunction) -> str | None:
        """
        The string converting the built-in *function* gives in the engine of the host the model
        assumes (`refinery.lib.scripts.js.analysis.environment.JsEngine.function_text`), or `None`
        where that host has no one engine or the conversion may run code the program installed. It
        runs
        `Function.prototype.toString` unless the program gave a conversion key
        (`refinery.lib.scripts.js.analysis.conversions.CONVERSION_KEYS`) to `Function.prototype`,
        to `Object.prototype`, or to the constructor *function* names; a write to the function
        through no name is one `builtin_writes_followed` refuses, and so is a write by a code
        string a reflection surface may run.
        """
        if not self.builtin_writes_followed() or self.model.has_reflection_surface():
            return None
        owners = ['Function', 'Object']
        if function.is_constructor:
            owners.append(function.name)
        if any(self.global_key_written(owner, key) for owner in owners for key in CONVERSION_KEYS):
            return None
        engine = self.model.environment.engine
        return None if engine is None else engine.function_text(function.name)

    def array_iterator_conversion_untouched(self) -> bool:
        """
        Whether converting an array iterator
        (`refinery.lib.scripts.js.values.ArrayIterator`) runs `Object.prototype.toString` and gives
        `[object Array Iterator]`. The scan has to follow every write on a built-in object, since the
        prototype of every array iterator is reached through no name; the program may give `Object`
        no conversion key (`refinery.lib.scripts.js.analysis.conversions.CONVERSION_KEYS`) and write
        no `Iterator`, whose prototype every array iterator inherits; and no reflection surface may
        run a code string that does either.
        """
        if not self.builtin_writes_followed() or self.model.has_reflection_surface():
            return False
        if 'Iterator' in self.intrinsic_writes.names:
            return False
        return not any(self.global_key_written('Object', key) for key in CONVERSION_KEYS)

    def regexp_text(self, value: RegExpObject) -> str | None:
        """
        The string converting the regular expression *value* gives in the engine of the host the
        model assumes (`refinery.lib.scripts.js.analysis.environment.JsEngine.regexp_text`), or
        `None` where that host has no one engine or the conversion may run code the program
        installed. It runs `RegExp.prototype.toString`, which reads the getters of
        `_REGEXP_TEXT_KEYS`, unless the program writes one of those keys on `RegExp` or a conversion
        key on `Object`; a write through no name is one `builtin_writes_followed` refuses, and so
        is a write by a code string a reflection surface may run.
        """
        engine = self.model.environment.engine
        if engine is None or not self.builtin_writes_followed() or self.model.has_reflection_surface():
            return None
        if any(self.global_key_written('RegExp', key) for key in _REGEXP_TEXT_KEYS):
            return None
        if any(self.global_key_written('Object', key) for key in CONVERSION_KEYS):
            return None
        return engine.regexp_text(value.source)

    def builtin_function_named(self, function: BuiltinFunction) -> bool:
        """
        Whether the built-in *function* still owns the `name` the language gave it: the program
        writes no `name` key on the constructor *function* names, and none on any built-in through
        no name (`builtin_writes_followed`).
        """
        if not self.builtin_writes_followed():
            return False
        return not function.is_constructor or not self.global_key_written(function.name, 'name')

    def builtin_function_constructor_untouched(self, function: BuiltinFunction) -> bool:
        """
        Whether reading `constructor` off the built-in *function* gives `Function`: a function owns
        no `constructor`, so the read reaches `Function.prototype`, unless the program writes that
        key there or gives the function a `constructor` or a prototype of its own. Each is a write
        on `Function` or on the constructor *function* names, or one through no name
        (`builtin_writes_followed`).
        """
        if not self.builtin_writes_followed() or self.global_key_written('Function', 'constructor'):
            return False
        if not function.is_constructor:
            return True
        return not any(self.global_key_written(function.name, key) for key in SPECIES_KEYS)

    def _prototypes_intact(self, owner: str, roots: frozenset[str], region: list[Node] | None = None) -> bool:
        """
        `refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.roots_unwritten` with the
        reflection term, which is what separates the two questions `read_chain_intact` and
        `chain_roots_unwritten` ask. Neither is spelled out twice, so a term added to one chain
        question reaches both arms rather than only the one it was written into.

        With *region*, the reflection term is asked of the opaque surfaces the nodes of *region*
        leave standing rather than of the whole program — the tree an edit that deletes them
        leaves, which is the tree a fold deciding what that same edit may drop has to judge on.
        """
        if region is None:
            if self.model.has_reflection_surface():
                return False
        elif any(
            not any(site is root or site.is_descendant_of(root) for root in region)
            for site in self.model.opaque_reflection_sites()
        ):
            return False
        return self.intrinsic_writes.roots_unwritten(owner, roots)

    def read_chain_intact(self, value_type: type, region: list[Node] | None = None) -> bool:
        """
        Whether every prototype a plain property read on a value of *value_type* consults is unmodified, so
        the read touches a data slot and runs nothing. Strictly stronger than `trusted_prototype`, which
        answers the neighbouring question for a method *call*, and the two must not be merged: a method
        resolves on the prototype that owns it, so `Array.prototype.join` shadows anything installed on
        `Object.prototype` and a patch there cannot change what `[1, 2].join('-')` means. A read of an
        arbitrary name has no such shadow — `Object.prototype` roots every chain, so a getter installed
        there is reached by a read on an array literal, on a primitive, and on `Math` alike.

        Confirmed against Node in both directions rather than reasoned from the specification: patching
        `Object.prototype.join` leaves `[1, 2].join('-')` intact, while patching `Object.prototype.zz`
        makes `[1, 2].zz` run a getter.

        Two separate facts have to hold and this is their conjunction: no chain root was written,
        which is `chain_roots_unwritten`, and no reflective surface could have written one without
        saying so. A caller for which the second costs more than it buys takes the first alone — see
        the note there for what that trade is and where it is made.
        """
        owner = PROTOTYPE_OWNERS.get(value_type.__name__)
        if owner is None:
            return False
        return self._prototypes_intact(owner, INHERITED_CHAIN_ROOTS, region)

    def chain_roots_unwritten(self, value_type: type) -> bool:
        """
        Whether the program writes no prototype a plain property read on a value of *value_type*
        consults. This is `read_chain_intact` without its reflection term, and the difference is not
        a weakening of the same question but a different one: `read_chain_intact` also refuses
        wherever `SemanticModel.has_reflection_surface` holds, and that predicate answers whether
        code could reference a global *by name* — true of `new Function('return this')`, which
        writes no prototype at all.

        The distinction is what the answer costs where it is wrong, and it decides who may ask this
        rather than being a judgement each caller makes for itself. A caller folding one expression
        pays an unfolded expression for refusing, so it may as well refuse under a surface, and
        every one of them asks `read_chain_intact`. A caller deciding whether a whole *pass* may run
        pays the pass, and a reflective surface is exactly what real obfuscated files carry: the
        surface is present in the input and gone from the finished output, so a pass gated on it
        never runs and never clears the surface that was gating it. Only those callers ask this —
        today namespace flattening and the dispatcher unwrapper — and they accept that an
        unresolvable `eval` could in principle have written a prototype, which is no worse than the
        nothing they asked before.
        """
        return self.intrinsic_writes.chain_roots_unwritten(value_type)

    def function_object_untouched(self, function: Node, exempt: Sequence[Node] = ()) -> bool:
        """
        Whether no code but the reads in *exempt* can give the function object *function* a
        property of its own, or hand it to code that could: whether every use of it is
        `FunctionUse.IN_PLACE`. A fold deciding whether the object may carry a property it reads
        asks this; a conversion asks `function_conversion_untouched`.

        The object is reached only through the names holding it — its declaring name, the one name
        a declarator or an assignment statement stores it into, a named expression's own name — or
        as the value of a property of an object literal that one name holds as a read-only
        container, and a literal no name holds is reached only where it is written. An assignment
        that is not a statement of its own hands its value on as well, as `f(W = function () {})`
        does, so a function written there is not left in place. Every read of those names, and the
        position of a literal no name holds, has to leave the object where it is: call it, run
        `call` or `apply` on it, read its `toString`, `name` or `length`, convert, compare or test
        it, or hand it to `String` while `String` is the built-in. A read may also copy it into
        another name, or hand it to a parameter of a function the call certainly runs, when the
        reads of that name or parameter answer the same question in turn, up to
        `_FUNCTION_FOLLOW_LIMIT` steps. The left of
        `instanceof` is handed to a method of the right operand, a tagged template hands its holes
        to the tag, and `new` hands the function to its own body as `new.target` and to the object
        it builds as its `constructor`, so none of these leave it in place. Its body may use its own
        `arguments` only by index, length, spread or as the second argument of `apply`, since
        `arguments.callee` is the function. A program that spells `caller` anywhere is refused:
        `g.caller` is whichever function called `g`.
        """
        return self._function_use(function, frozenset(exempt)) is FunctionUse.IN_PLACE

    def function_conversion_untouched(self, function: Node, exempt: Sequence[Node] = ()) -> bool:
        """
        Whether no code but the reads in *exempt* can give the function object *function* a
        `toString`, `valueOf` or `Symbol.toPrimitive` of its own, or a new prototype, so that
        converting it answers the text it was written with as far as the function itself goes. A
        function `function_object_untouched` accepts converts so, and a use that may install a
        conversion (`FunctionUse.CONVERSION`) refuses.

        A function that is only handed on converts so when no install the program makes can reach
        it (`refinery.lib.scripts.js.analysis.conversions.ConversionInstalls`), but this is asked
        only of the tree the run has settled, since a key the run decodes later may yet spell an
        install. On any other tree the question is refused and the run record notes the tree
        (`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`); a model built without
        a record refuses it and notes nothing. The tree is read as it stands, not as this model saw
        it, so a model a pass holds across its own changes refuses as soon as the tree has moved. A
        function of another tree, such as one in the body of a `new Function` the interpreter
        parsed, is judged by its own position alone: this model holds none of its names, and the
        scan never read that tree.

        The run record and the tree are read on every question; only the grade and the scan are
        remembered.
        """
        use = self._function_use(function, frozenset(exempt))
        if use is FunctionUse.IN_PLACE:
            return True
        if use is FunctionUse.CONVERSION:
            return False
        if tree_root(function) is not self.model.root:
            return False
        if self.conversion_installs.may_reach(function):
            return False
        settlement = self._settlement
        if settlement is None:
            return False
        version = tree_version(self.model.root)
        if settlement.settled(version):
            return True
        settlement.defer(version)
        return False

    def _function_use(self, function: Node, spared: frozenset[Node]) -> FunctionUse:
        """
        The most a use of the function object *function* may do to it, the reads in *spared*
        aside. The answer is fixed for the model's lifetime, so it is memoized per function and set
        of spared reads: every conversion of a function the interpreter folds asks it again.
        """
        key = (function, spared)
        use = self._function_use_cache.get(key)
        if use is None:
            use = self._function_use_cache[key] = self._compute_function_use(function, spared)
        return use

    def _compute_function_use(self, function: Node, spared: frozenset[Node]) -> FunctionUse:
        use = FunctionUse.IN_PLACE
        if self.model.reached_without_its_name(function):
            use = FunctionUse.TOUCHED
        if isinstance(function, JsFunctionExpression) and isinstance(function.id, JsIdentifier):
            own = self.model.binding_of(function.id)
            if own is None:
                use = max(use, FunctionUse.TOUCHED)
            else:
                use = max(use, self._name_use(own, spared, 0))
        if isinstance(function, JsFunctionDeclaration):
            names = self.model.function_names(function)
            if names is None:
                return max(use, FunctionUse.TOUCHED)
            for binding in names.bindings:
                use = max(use, self._name_use(binding, spared | {function}, 0))
            return use
        method = self._method_use(function)
        if method is not None:
            return max(use, method)
        name = self.model.name_holding(function)
        if name is not None:
            return max(use, self._name_use(name, spared, 0))
        return max(use, self._function_read_use(function, 0))

    def _name_use(self, name: Binding, spared: frozenset[Node], depth: int) -> FunctionUse:
        """
        The most a read of the name *name*, which may hold a function object, may do to that object,
        the reads in *spared* aside; a name code can reach without a read
        (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_a_read`) is
        `FunctionUse.TOUCHED` at least. Memoized: a parameter is asked once for every read that
        hands a function to it.
        """
        key = (name, depth, spared)
        use = self._name_use_cache.get(key)
        if use is None:
            use = FunctionUse.IN_PLACE
            if self.model.reached_without_a_read(name):
                use = FunctionUse.TOUCHED
            for read in name.reads:
                if read in spared:
                    continue
                use = max(use, self._function_read_use(read, depth))
                if use is FunctionUse.CONVERSION:
                    break
            self._name_use_cache[key] = use
        return use

    def _method_use(self, function: Node) -> FunctionUse | None:
        """
        `_function_use` for a function written as the value of a property of an object literal, or
        `None` for a function written anywhere else. The literal has to be the one value of a name
        that holds it as a read-only container, and every read of that name has to take a key off
        it; a read that may take the function's key is a read of the function.
        """
        parent = enclosing_operator(function)
        if not isinstance(parent, JsProperty) or strip_parens(parent.value) is not function:
            return None
        key = key_text(parent)
        holder = parent.parent
        if parent.kind is not JsPropertyKind.INIT or key is None or not isinstance(holder, JsObjectExpression):
            return FunctionUse.TOUCHED
        declarator = enclosing_operator(holder)
        if (
            not isinstance(declarator, JsVariableDeclarator)
            or strip_parens(declarator.init) is not holder
            or not isinstance(declarator.id, JsIdentifier)
        ):
            return FunctionUse.TOUCHED
        binding = self.model.binding_of(declarator.id)
        if binding is None or strip_parens(self.model.singular_value(binding)) is not holder:
            return FunctionUse.TOUCHED
        if not self.binding_is_immutable_container(binding) or self.model.reflection_can_reach(binding):
            return FunctionUse.TOUCHED
        use = FunctionUse.IN_PLACE
        names = frozenset({key})
        for read in binding.reads:
            member = enclosing_operator(read)
            if not isinstance(member, JsMemberExpression) or strip_parens(member.object) is not read:
                use = max(use, FunctionUse.TOUCHED)
                continue
            if key_may_be(member, names):
                use = max(use, self._function_read_use(member, 0))
        return use

    def _function_read_use(self, read: Node, depth: int) -> FunctionUse:
        """
        The most the read *read* of a function object may do to the object, in the sense of
        `function_object_untouched`; *depth* counts the names and parameters followed to reach it.
        """
        parent = enclosing_operator(read)
        if isinstance(parent, JsCallExpression):
            return self._call_use(parent, read, depth)
        if isinstance(parent, (JsVariableDeclarator, JsAssignmentExpression)):
            if depth >= _FUNCTION_FOLLOW_LIMIT:
                return FunctionUse.TOUCHED
            alias = self.model.name_holding(read)
            if alias is None:
                return FunctionUse.TOUCHED
            return self._name_use(alias, frozenset(), depth + 1)
        if isinstance(parent, JsMemberExpression):
            return self._member_use(parent, read)
        if isinstance(parent, JsTaggedTemplateExpression):
            in_place = strip_parens(parent.tag) is read
        elif isinstance(parent, JsBinaryExpression):
            in_place = parent.operator != 'instanceof' or strip_parens(parent.right) is read
        elif isinstance(parent, JsUnaryExpression):
            in_place = parent.operator in _UNARY_OPERATORS_READING_A_FUNCTION
        elif isinstance(parent, JsTemplateLiteral):
            in_place = not isinstance(parent.parent, JsTaggedTemplateExpression)
        elif isinstance(parent, _TESTED_FORMS):
            in_place = strip_parens(parent.test) is read
        else:
            in_place = False
        return FunctionUse.IN_PLACE if in_place else FunctionUse.TOUCHED

    def _call_use(self, call: JsCallExpression, read: Node, depth: int) -> FunctionUse:
        """
        `_function_read_use` for a function read as the callee or an argument of *call*. A call
        through a key known only at runtime may run an installer, and so is a `CONVERSION`.
        """
        callee = strip_parens(call.callee)
        if callee is read:
            return FunctionUse.IN_PLACE
        if isinstance(callee, JsMemberExpression) and key_text(callee) is None:
            return FunctionUse.CONVERSION
        installer = self.conversion_installs.installer_of_call(call)
        if installer is not None:
            for target, argument in enumerate(call.arguments):
                argument = strip_parens(argument)
                if isinstance(argument, JsSpreadElement):
                    break
                if argument is read:
                    return self._install_use(call, installer, target)
            return FunctionUse.CONVERSION
        if self.trusted_intrinsic(call.callee) == 'String':
            return FunctionUse.IN_PLACE
        return self._parameter_use(call, read, depth)

    def _member_use(self, member: JsMemberExpression, read: Node) -> FunctionUse:
        """
        `_function_read_use` for a function read as the object or the key of *member*. A write
        under a key that may be a conversion key, and a call of a method under a key known only at
        runtime, are a `CONVERSION`; reading `call`, `apply`, `toString`, `name` or `length` leaves
        the function in place.
        """
        if strip_parens(member.object) is not read:
            return FunctionUse.TOUCHED
        if is_member_write_target(member):
            governor = enclosing_operator(member)
            if isinstance(governor, JsUnaryExpression) and governor.operator == 'delete':
                return FunctionUse.TOUCHED
            if self.conversion_installs.spells_member_key(member.property, member.computed) is False:
                return FunctionUse.TOUCHED
            return FunctionUse.CONVERSION
        key = key_text(member)
        call = enclosing_operator(member)
        if isinstance(call, JsCallExpression) and strip_parens(call.callee) is member:
            if key is None:
                return FunctionUse.CONVERSION
            if key_is(member, RECEIVER_INSTALL_METHODS, NO_IS_STRICTER):
                return self._install_use(call, key, -1)
        if key_is(member, _FUNCTION_KEYS_READ_IN_PLACE, NO_IS_STRICTER):
            return FunctionUse.IN_PLACE
        return FunctionUse.TOUCHED

    def _install_use(self, call: JsCallExpression, installer: str, target: int) -> FunctionUse:
        """
        The use the installer call *call* makes of the value at argument *target*, or of its
        receiver when *target* is -1: a `CONVERSION` unless it certainly installs no conversion.
        """
        if self.conversion_installs.install_spelled(call, installer, target) is False:
            return FunctionUse.TOUCHED
        return FunctionUse.CONVERSION

    def _parameter_use(self, call: JsCallExpression, read: Node, depth: int) -> FunctionUse:
        """
        The use handing the function read at *read* to *call* makes of it: the use the receiving
        parameter makes, when the call certainly runs one function whose parameters are plain
        names, together with the use its own `arguments` make of it (`_element_use`). A value past
        the last parameter is reached through `arguments` alone.
        """
        if depth >= _FUNCTION_FOLLOW_LIMIT:
            return FunctionUse.TOUCHED
        callee = self.unambiguous_callee(call)
        if callee is None or not all(isinstance(param, JsIdentifier) for param in callee.params):
            return FunctionUse.TOUCHED
        position = None
        for index, argument in enumerate(call.arguments):
            if isinstance(argument, JsSpreadElement):
                return FunctionUse.TOUCHED
            if strip_parens(argument) is read:
                position = index
                break
        if position is None:
            return FunctionUse.TOUCHED
        use = FunctionUse.IN_PLACE
        if self._callee_uses_arguments(callee):
            use = self._element_use(callee, position, depth)
        if position >= len(callee.params) or use is FunctionUse.CONVERSION:
            return use
        param = callee.params[position]
        binding = self.model.binding_of(param) if isinstance(param, JsIdentifier) else None
        if binding is None:
            return FunctionUse.TOUCHED
        return max(use, self._name_use(binding, frozenset(), depth + 1))

    def _element_use(self, callee: Node, position: int, depth: int) -> FunctionUse:
        """
        The use the function *callee*, which can reach its own `arguments`, makes through them of
        the value its call passes at *position*. It is `TOUCHED` at least, and where every
        reference reads the object element by element (`arguments_reads_only_elements`), the most
        any read of an element that may be that one does to the value.
        """
        own = own_arguments_binding(self.model, callee)
        if own is None or self.model.reflection_can_reach(own) or not arguments_reads_only_elements(own):
            return FunctionUse.TOUCHED
        use = FunctionUse.TOUCHED
        for reference in own.reads:
            element = enclosing_operator(reference)
            if not isinstance(element, JsMemberExpression):
                continue
            positions = element_positions(element)
            if positions is None or position in positions:
                use = max(use, self._function_read_use(element, depth + 1))
            if use is FunctionUse.CONVERSION:
                break
        return use

    def call_is_foldable(
        self,
        node: JsCallExpression,
        *,
        receiver_type: type | None = None,
        trust: AnchoredTrust | None = None,
    ) -> bool:
        """
        Whether the call *node* may be evaluated to a constant and have its result replace it. This is the
        one admission gate every fold shares, so that a rule proven necessary at one site cannot be missing
        at another — the fold surface acquired its divergent hand-rolled checks precisely because each
        transform owned its own.

        Three questions must all answer yes:

        - The callee is still the built-in it is spelled as. A named callee (`parseInt(...)`,
          `String.fromCharCode(...)`) is judged by `trusted_intrinsic` on its root name; a call on a
          literal receiver is judged by `trusted_prototype` on the type that literal's syntax fixes; and a
          call on another call — a chain link — is judged by asking this whole question of that inner call.
          `receiver_type` covers the remaining case, a caller holding an already-evaluated receiver whose
          type it knows and this model does not.
        - Every function-valued argument writes nothing outside itself. `is_effect_free_when_discarded` is
          the right predicate rather than `is_pure`: a callback that mutates a fresh local and returns it —
          the `reduce` accumulator idiom — is not pure, but that mutation is the value being computed and an
          evaluator that runs the call reproduces it.
        - Nothing in the argument list is itself a call this gate does not also clear, so admitting an outer
          call cannot smuggle in an inner one. A nested built-in (`Math.floor(Math.abs(-1.7))`) is fine
          precisely because the same questions are asked of it.
        - No part of the call stores anything the residual would go on to read. The fold deletes the
          whole expression, so a store is lost wherever in it the store was written — an argument, the
          receiver, or the computed key naming the method; see `_call_stores_nothing`.

        Trust is not evaluability, and this answers only the first. `trusted_intrinsic` says `unknownFn` is
        undisturbed — true, and no help, since no such built-in exists to fold. Whether a callee can
        actually be evaluated is the caller's question, answered by its own registry lookup before it asks
        this one; conflating the two is what let a registry entry exist with no matching trust rule.

        The first question is asked of the whole program, or at a point where *trust* is given: the
        `AnchoredTrust` of that point answers it, for the call and for every call nested in its
        arguments.
        """
        if trust is None:
            if not self._callee_is_trusted(node, receiver_type):
                return False
        elif not trust.callee(node, receiver_type):
            return False
        if not self._call_stores_nothing(node):
            return False
        return all(self._argument_is_admissible(arg, trust) for arg in node.arguments)

    def call_names_an_unwritten_builtin(
        self,
        node: JsCallExpression,
        *,
        receiver_type: type | None = None,
    ) -> bool:
        """
        `call_is_foldable`'s callee-trust question with the reflection terms lifted: whether the
        built-in *node* spells is still that built-in, judged by attributed writes alone. A named
        callee is asked `intrinsic_name_unwritten`; a literal receiver's or known receiver type's
        method is asked `prototype_name_unwritten`; and a chained call is asked this same question
        of its inner link. The composition a consumer that holds an oracle clearance makes — the
        oracle answered the reflection terms at the anchor, so the name questions are what remain.
        A consumer without one takes `call_is_foldable`, whose trust leg is this plus those terms.
        """
        callee = strip_parens(node.callee)
        if isinstance(callee, JsIdentifier):
            return self.intrinsic_name_unwritten(callee) is not None
        if not isinstance(callee, JsMemberExpression):
            return False
        base = strip_parens(callee.object)
        if isinstance(base, JsIdentifier):
            return self.intrinsic_name_unwritten(base) is not None
        if isinstance(base, JsCallExpression):
            return self.call_names_an_unwritten_builtin(base)
        literal_type = _LITERAL_RECEIVER_TYPES.get(type(base))
        if literal_type is not None:
            return self.prototype_name_unwritten(literal_type)
        if receiver_type is None:
            return False
        return self.prototype_name_unwritten(receiver_type)

    def _call_stores_nothing(self, node: JsCallExpression) -> bool:
        """
        Whether the parts of *node* that name what is being called store anything: the receiver it is
        called on, and the computed key that says which method. The fold deletes the whole call
        expression, so a store written into either is lost exactly as one written into an argument is,
        and `Math[v = 'floor'](1.9)` drops the write to `v` while answering `1`.

        A receiver that is itself a call is not asked here. It is a link in a chain, and
        `_callee_is_trusted` already puts it through this same gate in full, where its own receiver,
        key and arguments are each accounted for.
        """
        callee = strip_parens(node.callee)
        if not isinstance(callee, JsMemberExpression):
            return True
        base = strip_parens(callee.object)
        if not isinstance(base, JsCallExpression) and not self._stores_nothing(base):
            return False
        return not callee.computed or self._stores_nothing(callee.property)

    def _callee_is_trusted(self, node: JsCallExpression, receiver_type: type | None) -> bool:
        callee = strip_parens(node.callee)
        if isinstance(callee, JsIdentifier):
            return self.trusted_intrinsic(callee) is not None
        if not isinstance(callee, JsMemberExpression):
            return False
        base = strip_parens(callee.object)
        if isinstance(base, JsIdentifier):
            return self.trusted_intrinsic(base) is not None
        if isinstance(base, JsCallExpression):
            # A chained call (`Buffer.from(x).toString('hex')`, `[1, 2].map(f).join('')`) has no name at
            # this link. Its receiver is whatever the inner call returned, so it is trustworthy exactly
            # when that inner call is admissible in full — including its arguments, since an effectful
            # argument to the inner link is just as observable as one to the outer.
            return self.call_is_foldable(base)
        literal_type = _LITERAL_RECEIVER_TYPES.get(type(base))
        if literal_type is not None:
            return self.trusted_prototype(literal_type)
        if receiver_type is None:
            return False
        return self.trusted_prototype(receiver_type)

    def _argument_is_admissible(self, arg: Node | None, trust: AnchoredTrust | None = None) -> bool:
        """
        Whether *arg* may be evaluated as part of a fold that then replaces the whole call, deleting
        the argument's text along with it.

        A function-valued argument is judged by what calling it would do, because the call is what the
        fold performs. A call is judged by this same gate in full, so that admitting an outer call
        cannot smuggle in an inner one. Everything else is judged by whether evaluating it can be
        dropped at all, which is the question `is_side_effect_free` already answers over the whole
        subtree — and it is the argument's *subtree* that matters, because a store need not be the
        argument itself. `Math.floor(v = 4)` is only the plainest spelling of it; the same store hides
        in a summand, a comma operand, a template substitution, and a compound or logical assignment,
        each of which the fold would delete while the residual keeps reading the old value.
        """
        node = strip_parens(arg)
        if isinstance(node, FUNCTION_NODES):
            summary = self.summary_of(node)
            return summary.is_effect_free_when_discarded
        if isinstance(node, JsCallExpression):
            return self.call_is_foldable(node, trust=trust)
        return self._stores_nothing(node)

    def _stores_nothing(self, node: Node | None) -> bool:
        """
        Whether evaluating the expression *node* performs no store, so that a fold which replaces it
        with the value it computed loses nothing a later statement could read.

        A fold deletes the expression it replaces. Where that expression assigned, updated or deleted
        something, the store went with it and the residual program keeps reading the old value, which
        is what makes `Math.floor(v = 4)` answer `4` and leave `v` at `0`. The store is rarely the
        whole argument — it hides in a summand, a comma operand, a template substitution, a compound
        or a logical assignment — so the question is asked of the whole subtree and not of the shape
        at its root.

        A `yield` or an `await` is refused under the same heading: neither stores, but both hand
        control somewhere that can, and a fold that runs them decides when they resume.

        A function written down inside the expression stores nothing by being evaluated, since it is a
        value and only calling it could store; its body is therefore not walked.

        A call is refused when its callee can be named and that callee is known to write. It is
        admitted when the callee cannot be named, which is where this predicate stops being a proof:
        `s.charCodeAt(i)` on a parameter resolves to nothing this model can summarize and stores
        nothing, and refusing every unnameable call would decline the string decoders this tool exists
        to read. A `new` expression is admitted on the same terms. Admitting them is what the gate did
        for every call in this position before, so the rule only ever narrows what folds.
        """
        if node is None:
            return True
        pending: list[Node] = [node]
        while pending:
            current = pending.pop()
            if isinstance(current, FUNCTION_NODES):
                continue
            if isinstance(current, (JsAssignmentExpression, JsUpdateExpression)):
                return False
            if isinstance(current, JsUnaryExpression) and current.operator == 'delete':
                return False
            if isinstance(current, (JsYieldExpression, JsAwaitExpression)):
                return False
            if isinstance(current, JsCallExpression):
                callee = self.unambiguous_callee(current)
                if callee is not None and self.summary_of(callee).written_bindings:
                    return False
            pending.extend(current.children())
        return True

    def _is_trusted_global_read(self, member: JsMemberExpression) -> bool:
        """
        Whether reading *member* off the global object runs no user getter, so the read carries no
        observable effect: a non-computed access of a trusted intrinsic-named data property on the global
        object, sound only under the `global_pristine` precondition. This mirrors the intrinsic-call trust
        of `_resolve_callee`, lifted from methods to global data-property reads.
        """
        if not self.global_pristine or member.computed or member.object is None:
            return False
        if not key_is(member, _GLOBAL_DATA_PROPERTIES, NO_IS_STRICTER):
            return False
        return self._base_is_global_object(member.object)

    def _base_is_global_object(self, node: Node) -> bool:
        """
        Whether *node* denotes the global object through a base read that itself cannot throw: an
        unshadowed `globalThis`, the one global-object alias the language mandates in every host.
        Both callers remove code on the strength of this answer — `_base_is_safe` clears a property
        access and `_is_trusted_global_read` clears a getter — so a host-conditional alias
        (`window`, `self`, `top`, `frames`, `global`) is refused here: it denotes the global object
        where it resolves, but a host that lacks it throws a `ReferenceError` on the base read, and
        clearing that read would drop the throw (`SemanticModel.read_may_throw`). An analyst who
        pins the host recovers the alias as certain. A local that only holds the global from an
        establishing definition is resolved separately by `_trusted_global_alias_read`, whose caller
        orders that definition before the read.
        """
        if isinstance(node, JsIdentifier) and node.name == 'globalThis':
            return self.model.lookup(node.name, self.model.scope_of(node)) is None
        return False

    def member_read_getter_free(
        self,
        member: JsMemberExpression,
        established: Callable[[Binding, JsMemberExpression], bool] | None = None,
    ) -> bool:
        """
        Whether reading *member* runs no user getter, so it carries no observable effect: a
        getter-free read off a pristine value (a fresh literal or a pristine intrinsic root) or a
        trusted global data-property read off a syntactic global-object alias — always, since
        neither is nullish — or off a local single-assigned to the global object, which holds it
        only from its establishing definition onward, or off a local the model knows holds a
        function, of a key outside the accessor-poisoned set. The local cases qualify only when
        *established* confirms the single definition reaches the read, an ordering this effect
        model cannot decide on its own (see
        `refinery.lib.scripts.js.analysis.reaching.ReachingModel.value_preserved`).
        """
        if self._getter_free_read(member):
            return True
        if established is None:
            return False
        binding = self._trusted_global_alias_read(member)
        if binding is not None and established(binding, member):
            return True
        return self._typed_local_function_read(member, established)

    def _typed_local_function_read(
        self,
        member: JsMemberExpression,
        established: Callable[[Binding, JsMemberExpression], bool],
    ) -> bool:
        """
        Whether *member* is a non-computed read off a local the model knows holds a function, reading
        a key a function cannot answer with a trap: `caller` and `arguments` are accessors that
        throw off a strict-mode function, and `__proto__` is an accessor every prototype chain
        carries, so none of the three is provably throw-free or getter-free. Every other name on a
        function resolves through the prototype chain `read_chain_intact` vouches for — which only
        holds where no reflective surface stands, the term the trusting model clears — and an own
        property of the key can only be there through a write, so the container the binding holds
        must be immutable (`binding_is_immutable_container`, the alias-aware own-write check: a
        write through any alias of the binding refuses it). The value must be in place at the read
        (`established`), since a hoisted `var` reads `undefined` before its initializer runs and
        `undefined.constructor` throws.
        """
        if member.computed or key_text(member) is None or _is_poison_pill_property(member, self.assumed_keys):
            return False
        base = member.object
        if not isinstance(base, JsIdentifier):
            return False
        binding = self.model.resolve(base)
        if binding is None or self.model.reflection_can_reach(binding):
            return False
        value = self.model.singular_value(binding)
        if not isinstance(value, (JsFunctionDeclaration, JsFunctionExpression, JsArrowFunctionExpression)):
            return False
        if not self.read_chain_intact(type(value)):
            return False
        if not self.binding_is_immutable_container(binding):
            return False
        return established(binding, member)

    def _trusted_global_alias_read(self, member: JsMemberExpression) -> Binding | None:
        """
        The local binding *member*'s base reads when *member* is a non-computed access of a trusted
        global data property through a local provably holding the global object — a `globalThis` alias
        or a `globalThis || ...` guard — under `global_pristine`; `None` otherwise. This answer drives
        a rewrite, so it takes the removing polarity over `SemanticModel.binding_values`: the value set
        must be complete, non-empty (a complete empty set answers the universal question vacuously),
        and EVERY value must be the object — the opposite reading from the recording
        `names_the_global_object`, which admits on ANY value. The binding is returned rather than a
        verdict because whether it already holds the global where it is read is an ordering question
        for a layer that sees control flow.
        """
        if not self.global_pristine or member.computed:
            return None
        if not key_is(member, _GLOBAL_DATA_PROPERTIES, NO_IS_STRICTER):
            return None
        base = member.object
        if not isinstance(base, JsIdentifier) or base.name in GLOBAL_OBJECT_ALIASES:
            return None
        binding = self.model.resolve(base)
        if binding is None or self.model.reflection_can_reach(binding):
            return None
        values, complete = self.model.binding_values(binding)
        if not complete or not values:
            return None
        return binding if all(self._value_is_global_object(value) for value in values) else None

    def _value_is_global_object(self, node: Node | None) -> bool:
        """
        Whether *node*, one value a local holds, is provably the global object: the canonical
        `globalThis`, or a `globalThis || ...` existence guard whose truthy left is exactly it. A host
        alias that may be `undefined` is excluded, so a read through the local cannot throw on a
        nullish base.
        """
        return self.intrinsic_of(node) is GLOBAL_OBJECT

    def _base_is_safe(self, node: Node) -> bool:
        """
        Whether a property access on *node* cannot throw because *node* is known not to be nullish: a
        container literal whose access is plain, a primitive literal other than `null`, the global object, a
        pristine intrinsic root, a name holding what the CommonJS host passed where it passes it (none of
        which is nullish), or a never-rebound rest parameter. A rest parameter is bound to a fresh array
        at function entry, before any body statement runs — no temporal-dead-zone or hoisted-`undefined` window
        a flow-insensitive check could miss — so a member access on it is safe wherever it appears, provided the
        name is never reassigned to a value that could be nullish. A `var`/`let`/`const` local initialized to a
        literal is deliberately NOT admitted here: its initializer may not have run yet at the access
        (`function(){ a.x = 1; var a = []; }` throws), which this flow-insensitive predicate cannot rule out.
        The global object is recognized through `_base_is_global_object`, so an alias spelling a
        local declaration shadows names that local and is decided by the same rules as any other
        name: a shadowed `window` is the hoisted-`undefined` window this predicate refuses
        elsewhere.

        Non-nullishness is a *different* question from freshness and from getter-freeness, so this shares only
        the container-literal atom with its neighbours: a primitive qualifies here and is not fresh, while a
        fresh local qualifies as fresh and not here. It needs no prototype question either, unlike
        `_base_getter_safe`: no patch to any prototype can make `[1, 2]` nullish, and a throwing getter
        reached through a patched chain is that predicate's concern. There is no member-chain arm, because
        `root.a` may be `undefined` however safe *root* is, and the second read would then throw.
        """
        if container_literal_access_is_plain(node):
            return True
        if isinstance(node, (JsStringLiteral, JsNumericLiteral, JsBooleanLiteral)):
            return True
        if isinstance(node, JsIdentifier):
            if self._base_is_global_object(node):
                return True
            if isinstance(self.intrinsic_of(node), str):
                return True
            binding = self.model.resolve(node)
            if binding is None:
                return False
            if binding.supplied_by_host and not binding.may_be_absent:
                return self.model.holds_what_the_host_passed(binding)
            return self._is_rest_param(binding) and not binding.writes
        return False

    def _base_getter_safe(self, node: Node, region: list[Node] | None = None) -> bool:
        """
        Whether reading a property of *node* cannot run a user-defined getter, so the read carries no
        hidden effect: a literal, or a pristine intrinsic root, whose entire prototype chain the program
        leaves alone. Unlike `_base_is_safe`, the global object does not qualify — a global property such as
        `location` may be an accessor — so a read through it is treated as an unknown call. Unlike
        freshness, a primitive and an intrinsic root do qualify: neither is a newly built container, and
        neither runs user code on a read of a pristine chain.

        Syntax alone settles the *type* of a literal base but not its behaviour, which is why every arm ends
        in `read_chain_intact` rather than returning on the node kind: the prototype may carry a getter. A
        container literal must additionally declare no
        accessor of its own, the shared `container_literal_access_is_plain` question, since a getter written
        into the literal needs no prototype at all. There is no member-chain arm, for the reason given on
        `_base_is_safe`. With *region*, the chain question is asked of the tree an edit deleting the nodes
        of *region* leaves standing — see `getter_free_read_dropped_with`.
        """
        inner = strip_parens(node)
        value_type = _LITERAL_READ_TYPES.get(type(inner))
        if value_type is not None:
            if self._literal_declares_accessor(inner):
                return False
            return self.read_chain_intact(value_type, region)
        if isinstance(inner, JsIdentifier) and isinstance(self.intrinsic_of(inner), str):
            return self._prototypes_intact('Object', _INTRINSIC_CHAIN_ROOTS, region)
        return False

    def _literal_declares_accessor(self, node: Node) -> bool:
        """
        Whether *node* is a container literal that declares an accessor of its own, so a member access on it
        runs user code with no prototype involved. Separate from the chain question because it needs no
        model: the getter is written into the expression.
        """
        if not isinstance(node, (JsObjectExpression, JsArrayExpression)):
            return False
        return not container_literal_access_is_plain(node)

    def _getter_free_read(self, member: JsMemberExpression, region: list[Node] | None = None) -> bool:
        """
        Whether reading *member* runs no user getter and cannot fire a poison-pill accessor: the base is a
        getter-safe value (a fresh literal or a pristine intrinsic root) or a trusted global-object data
        property, and the property is not one of the poison-pill names whose read may throw or run an
        `Object.prototype` accessor. This is the single getter-freeness gate the summary scan and
        `is_side_effect_free` share. With *region*, the chain question on the base is asked of the tree an
        edit deleting the nodes of *region* leaves standing — see `getter_free_read_dropped_with`.
        """
        if _is_poison_pill_property(member, self.assumed_keys):
            return False
        if member.object is not None and self._base_getter_safe(member.object, region):
            return True
        return self._is_trusted_global_read(member)

    def getter_free_read_dropped_with(self, member: JsMemberExpression, region: list[Node]) -> bool:
        """
        `member_read_getter_free` asked of the tree an edit deleting the nodes of *region* leaves
        standing: a read inside the region is dropped together with them, so the opaque surfaces
        that tree still carries — those outside the region — are the only ones that could have
        tampered the chain the read consults, and a surface the edit removes never gets to. A
        read outside the region survives the edit and keeps the stock answer, which the
        trusted-global arm keeps in full: it rests on `global_pristine`, a whole-program
        precondition this question does not re-ask per region.
        """
        if not any(member is root or member.is_descendant_of(root) for root in region):
            return self.member_read_getter_free(member)
        return self._getter_free_read(member, region)

Methods

def summary_of(self, func)

The effect summary of a function node (or the script). An unknown node is reported as impure.

Expand source code Browse git
def summary_of(self, func: Node) -> EffectSummary:
    """
    The effect summary of a function node (or the script). An unknown node is reported as impure.
    """
    return self._summaries.get(id(func), EffectSummary(calls_unknown=True))
def mutated_bindings(self, func)

The outer bindings (captured locals and globals) a call to func may write, directly or through any function it transitively calls, each identified by its Binding rather than its name so a caller can ask whether one specific binding is mutated. Empty for a function with no such writes and for an unknown node alike — use summary_of(func).calls_unknown to tell those apart.

Expand source code Browse git
def mutated_bindings(self, func: Node) -> frozenset[Binding]:
    """
    The outer bindings (captured locals and globals) a call to *func* may write, directly or through
    any function it transitively calls, each identified by its `Binding` rather than its name so a
    caller can ask whether one specific binding is mutated. Empty for a function with no such writes
    and for an unknown node alike — use `summary_of(func).calls_unknown` to tell those apart.
    """
    return frozenset(self.summary_of(func).written_bindings)
def function_can_mutate(self, func, binding)

Whether a call to func may write binding, itself or through a transitive callee.

Expand source code Browse git
def function_can_mutate(self, func: Node, binding: Binding) -> bool:
    """
    Whether a call to *func* may write *binding*, itself or through a transitive callee.
    """
    return binding in self.summary_of(func).written_bindings
def function_escapes(self, func)

Whether the body of func may run at a point the surrounding scope cannot enumerate as a call name(…) whose callee static_callee resolves: an anonymous function (an IIFE, a callback, stored and called later), or a named function whose binding is exported, which hands it to every importer, reassigned, redeclared, or referenced anywhere other than as the identifier callee of a direct call (aliased, passed as an argument, f.call(…), or called through the global object as globalThis.f()). A reference inside a dynamic scope — a name a with body resolves at runtime — counts too: the model cannot order or resolve it, so the function may be invoked or aliased there with no static call site.

A body also runs where no reference of its name shows. A generator's body runs at each step of the object a call returns, wherever that object is stepped. The function may be called or handed out without its name, through the caller of another function, its own arguments or new.target (SemanticModel.reached_without_its_name()). The initializer of a field runs at each construction of its class, which no call names. And code outside the file may call it by a name it reaches (SemanticModel.reached_from_outside()): a host calls a declared entry point whenever the script hands it control, and code loading a library calls its functions once the file has run, as an importer calls an export.

A call to such a function can land at a point no call site pins down; a function only ever called directly by name has all its invocations enumerated by those call sites. Memoized per function.

Expand source code Browse git
def function_escapes(self, func: Node) -> bool:
    """
    Whether the body of *func* may run at a point the surrounding scope cannot enumerate as a
    call `name(...)` whose callee `static_callee` resolves: an anonymous function (an IIFE, a
    callback, stored and called later), or a named function whose binding is exported, which
    hands it to every importer, reassigned, redeclared, or referenced anywhere other than as
    the identifier callee of a direct call (aliased, passed as an argument, `f.call(...)`, or
    called through the global object as `globalThis.f()`). A reference inside a dynamic scope —
    a name a `with` body resolves at runtime — counts too: the model cannot order or resolve
    it, so the function may be invoked or aliased there with no static call site.

    A body also runs where no reference of its name shows. A generator's body runs at each step
    of the object a call returns, wherever that object is stepped. The function may be called
    or handed out without its name, through the `caller` of another function, its own
    `arguments` or `new.target`
    (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_without_its_name`). The
    initializer of a field runs at each construction of its class, which no call names. And code
    outside the file may call it by a name it reaches
    (`refinery.lib.scripts.js.analysis.model.SemanticModel.reached_from_outside`): a host calls
    a declared entry point whenever the script hands it control, and code loading a library
    calls its functions once the file has run, as an importer calls an export.

    A call to such a function can land at a point no call site pins down; a function only ever
    called directly by name has all its invocations enumerated by those call sites. Memoized
    per function.
    """
    cached = self._escape_cache.get(func)
    if cached is None:
        cached = self._escape_cache[func] = self._compute_function_escapes(func)
    return cached
def mutators_escape(self, binding)

Whether some function that may write binding — itself or through a transitive callee — escapes (function_escapes), so a write to binding may occur at a point no call site enumerates. When true, the places binding changes cannot be pinned down, and a caller reasoning about where its value survives must treat it as volatile everywhere. Memoized per binding.

Expand source code Browse git
def mutators_escape(self, binding: Binding) -> bool:
    """
    Whether some function that may write *binding* — itself or through a transitive callee — escapes
    (`function_escapes`), so a write to *binding* may occur at a point no call site enumerates. When
    true, the places *binding* changes cannot be pinned down, and a caller reasoning about where its
    value survives must treat it as volatile everywhere. Memoized per binding.
    """
    cached = self._mutators_escape_cache.get(id(binding))
    if cached is None:
        cached = any(
            func is not self.model.root
            and binding in self.summary_of(func).written_bindings
            and self.function_escapes(func)
            for func in self._functions
        )
        self._mutators_escape_cache[id(binding)] = cached
    return cached
def some_function_can_mutate(self, binding)

Whether any function this file writes may write binding, itself or through a transitive callee. The answer a caller needs where a call runs a function it cannot name: not knowing which one runs, it has to reckon with every one that could. Memoized per binding.

Expand source code Browse git
def some_function_can_mutate(self, binding: Binding) -> bool:
    """
    Whether any function this file writes may write *binding*, itself or through a transitive
    callee. The answer a caller needs where a call runs a function it cannot name: not knowing
    which one runs, it has to reckon with every one that could. Memoized per binding.
    """
    cached = self._some_mutator_cache.get(id(binding))
    if cached is None:
        cached = any(
            func is not self.model.root and self.function_can_mutate(func, binding)
            for func in self._functions
        )
        self._some_mutator_cache[id(binding)] = cached
    return cached
def is_pure_call(self, call)

Whether evaluating call has no observable effect: it invokes a trusted pure intrinsic (under the pristine-intrinsics precondition) or a local function whose summary is pure.

Expand source code Browse git
def is_pure_call(self, call: JsCallExpression | JsNewExpression) -> bool:
    """
    Whether evaluating *call* has no observable effect: it invokes a trusted pure intrinsic (under
    the pristine-intrinsics precondition) or a local function whose summary is pure.
    """
    callee = self._resolve_callee(call)
    if callee is _PURE:
        return True
    if isinstance(callee, Node):
        return self.summary_of(callee).is_pure
    return False
def is_pure_call_discarded(self, call)

Whether evaluating call and discarding its result has no observable effect. Like is_pure_call but resolved through EffectSummary.is_effect_free_when_discarded, so a callee whose only residual effect is a write it confines to its returned value qualifies — that write is unobservable once the result is thrown away. A caller may use this only in a position it has proven discards the value.

Expand source code Browse git
def is_pure_call_discarded(self, call: JsCallExpression | JsNewExpression) -> bool:
    """
    Whether evaluating *call* and discarding its result has no observable effect. Like `is_pure_call`
    but resolved through `EffectSummary.is_effect_free_when_discarded`, so a callee whose only residual
    effect is a write it confines to its returned value qualifies — that write is unobservable once the
    result is thrown away. A caller may use this only in a position it has proven discards the value.
    """
    callee = self._resolve_callee(call)
    if callee is _PURE:
        return True
    if isinstance(callee, Node):
        return self.summary_of(callee).is_effect_free_when_discarded
    return False
def call_clearable(self, call, callee_established, binding_established)

Whether call may be cleared with respect to ordering — its callee in place before the call runs, and every outer lexical binding its body may read past that binding's temporal dead zone. Two ordering judgments the caller supplies, since only its layer can make them: callee_established tests the name a named local callee is read through, which holds the function from a point of its own, and binding_established tests a dead_zone_reads binding (whether the call sits after the binding's declaration). A trusted pure intrinsic qualifies unconditionally. Otherwise the callee resolves to a function, and the call is refused unless every binding the callee's summary defers here is binding_established — a dead-zone read would throw a ReferenceError the deletion of the call would silently drop, which no establishment of the callee itself excuses. Past that gate, an inline function-expression callee (defined at the call site, hence always in place) qualifies, and a call resolving to a single named local function qualifies when callee_established accepts it; an unresolved or ambiguous callee does not.

The resolution, the intrinsic case, the dead-zone gate, and the inline-callee case live here so callers supply only the ordering judgments their layer can make. An ordering-free caller passes binding_established = lambda binding: False, which refuses any callee that reads an outer lexical binding. This certifies establishment ONLY, not purity — a caller deciding whether a call may be dropped must conjoin it with is_pure_call, as side_effect_free does, since an established callee may still run an effectful body.

Expand source code Browse git
def call_clearable(
    self,
    call: JsCallExpression | JsNewExpression,
    callee_established: Callable[[Binding], bool],
    binding_established: Callable[[Binding], bool],
) -> bool:
    """
    Whether *call* may be cleared with respect to ordering — its callee in place before the
    call runs, and every outer lexical binding its body may read past that binding's
    temporal dead zone. Two ordering judgments the caller supplies, since only its layer can
    make them: *callee_established* tests the name a named local callee is read through, which
    holds the function from a point of its own, and
    *binding_established* tests a `dead_zone_reads` binding (whether the call sits after the
    binding's declaration). A trusted pure intrinsic qualifies unconditionally. Otherwise
    the callee resolves to a function, and the call is refused unless every binding the
    callee's summary defers here is `binding_established` — a dead-zone read would throw a
    `ReferenceError` the deletion of the call would silently drop, which no establishment of
    the callee itself excuses. Past that gate, an inline function-expression callee (defined
    at the call site, hence always in place) qualifies, and a call resolving to a single
    named local function qualifies when *callee_established* accepts it; an unresolved or
    ambiguous callee does not.

    The resolution, the intrinsic case, the dead-zone gate, and the inline-callee case live
    here so callers supply only the ordering judgments their layer can make. An ordering-free
    caller passes `binding_established = lambda binding: False`, which refuses any callee that
    reads an outer lexical binding. This certifies establishment ONLY, not purity — a caller
    deciding whether a call may be dropped must conjoin it with `is_pure_call`, as
    `side_effect_free` does, since an established callee may still run an effectful body.
    """
    resolved = self._resolve_callee(call)
    if resolved is _PURE:
        return True
    if isinstance(resolved, Node):
        deferred = self.summary_of(resolved).dead_zone_reads
        if deferred and not all(binding_established(binding) for binding in deferred):
            return False
        callee = call.callee
        if isinstance(strip_parens(callee), (JsFunctionExpression, JsArrowFunctionExpression)):
            return True
        name = self.model.resolve(callee) if isinstance(callee, JsIdentifier) else None
        return name is not None and callee_established(name)
    return False
def is_side_effect_free(self, node, defunct=None, member_safe=None, call_established=None, discarded=False, reads_may_throw=False, read_established=None, coercions_may_write=False)

Whether evaluating node can be dropped or reordered without an observable side effect, with the call leaf resolved through this model's is_pure_call: a call to a proven-pure function or trusted intrinsic is free, recursing into its arguments. defunct names bindings being removed, whose calls and property reads are treated as free. This is the model-aware form of the model-free side_effect_free in this module, which clears only calls to a defunct name; unlike it, an identifier read that resolves through a with body's dynamic scope is rejected here — reading the bare name may fire the with object's getter or throw (see SemanticModel.read_has_dynamic_effect()) — while a function value whose body performs such a read stays free, since defining it runs nothing. A caller with control-flow context passes member_safe to also clear a getter-free read through a local global-object alias it can prove established before the read; the default clears only the syntactic global case (_is_trusted_global_read).

With discarded the caller asserts node's own value is thrown away, so a top-level call leaf is cleared through is_pure_call_discarded and a callee that only mutates a local it returns is droppable — the removal contexts of JsUnusedCodeRemoval supply it.

With reads_may_throw the read leaf is rejected not only when it fires a with object's getter but also when it may throw a ReferenceError, which is the throw half of the read_effect contract that read_has_dynamic_effect supplies only the getter half of. A caller asks for it of an expression whose value it drops with nothing left evaluating the read — an allocation discarded to answer its type or truthiness, the store-removal sweep, the discarded-test and dead-branch folds, the string-array dispatcher's dropped trailing arguments, and the reflective inliner's dropped comma-sequence prefix — so the throw that reading it would have raised is not muted. A pass that relocates the read into a position still evaluated — the object fold moving a property value to its use, an inliner substituting an argument into a body — keeps the throw at that position and asks this only of the sub-expression it truly drops (an unaccessed property, an unused parameter, a typeof operand). With read_established a caller passes its establishment proof — in every consumer the query of one shared DefiniteAssignmentModel — and a may-throw read the proof vouches a completed creating write for is free again, which is what lets a sweep still drop X = 5; y = X; whole rather than keep the read of X a kept store establishes. A proof is only ever offered against the throw it excuses, so supplying one asks for that half of the contract as much as reads_may_throw does, and neither is quietly ignored for want of the other.

With coercions_may_write the caller drops the evaluation, so an operator that converts its operand is held to a value that cannot be an object — the conversion asks the operand's valueOf or Symbol.toPrimitive, which is code the operand carries, and the composition alone cannot see it. The same drop-versus-relocation divide as reads_may_throw applies: a pass that relocates the evaluation keeps the conversion where it runs and answers for it with its own gates. A call of a pure intrinsic converts its arguments (calls_a_pure_intrinsic), so it is held to arguments that cannot be objects as well.

Expand source code Browse git
def is_side_effect_free(
    self,
    node: Node,
    defunct: set[str] | None = None,
    member_safe: Callable[[JsMemberExpression], bool] | None = None,
    call_established: Callable[[JsCallExpression | JsNewExpression], bool] | None = None,
    discarded: bool = False,
    reads_may_throw: bool = False,
    read_established: Callable[[JsIdentifier], bool] | None = None,
    coercions_may_write: bool = False,
) -> bool:
    """
    Whether evaluating *node* can be dropped or reordered without an observable side effect, with
    the call leaf resolved through this model's `is_pure_call`: a call to a proven-pure function or
    trusted intrinsic is free, recursing into its arguments. *defunct* names bindings being removed,
    whose calls and property reads are treated as free. This is the model-aware form of the
    model-free `side_effect_free` in this module, which clears only calls to a defunct name; unlike
    it, an identifier read that resolves through a `with` body's dynamic
    scope is rejected here — reading the bare name may fire the `with` object's getter or throw (see
    `refinery.lib.scripts.js.analysis.model.SemanticModel.read_has_dynamic_effect`) — while a
    function value whose body performs such a read stays free, since defining it runs nothing. A
    caller with control-flow context passes *member_safe* to also clear a getter-free read through a
    local global-object alias it can prove established before the read; the default clears only the
    syntactic global case (`_is_trusted_global_read`).

    With *discarded* the caller asserts *node*'s own value is thrown away, so a top-level call leaf is
    cleared through `is_pure_call_discarded` and a callee that only mutates a local it returns is
    droppable — the removal contexts of `JsUnusedCodeRemoval` supply it.

    With *reads_may_throw* the read leaf is rejected not only when it fires a `with` object's getter
    but also when it may throw a `ReferenceError`, which is the throw half of the *read_effect*
    contract that `read_has_dynamic_effect` supplies only the getter half of. A caller asks for
    it of an expression whose value it drops with nothing left evaluating the read — an
    allocation discarded to answer its type or truthiness, the store-removal sweep, the
    discarded-test and dead-branch folds, the string-array dispatcher's dropped trailing
    arguments, and the reflective inliner's dropped comma-sequence prefix — so the throw that
    reading it would have raised is not muted. A pass that relocates the read into a position
    still evaluated — the object fold moving a property value to its use, an inliner
    substituting an argument into a body — keeps the throw at that position and asks this only
    of the sub-expression it truly drops (an unaccessed property, an unused parameter, a
    `typeof` operand). With *read_established* a caller passes its establishment proof — in
    every consumer the query of
    one shared `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel` — and a
    may-throw read the proof vouches a completed creating write for is free again, which is what
    lets a sweep still drop `X = 5; y = X;` whole rather than keep the read of `X` a kept store
    establishes. A proof is only ever offered against the throw it excuses, so supplying one
    asks for that half of the contract as much as *reads_may_throw* does, and neither is quietly
    ignored for want of the other.

    With *coercions_may_write* the caller drops the evaluation, so an operator that converts
    its operand is held to a value that cannot be an object — the conversion asks the operand's
    `valueOf` or `Symbol.toPrimitive`, which is code the operand carries, and the composition
    alone cannot see it. The same drop-versus-relocation divide as *reads_may_throw* applies:
    a pass that relocates the evaluation keeps the conversion where it runs and answers for it
    with its own gates. A call of a pure intrinsic converts its arguments
    (`calls_a_pure_intrinsic`), so it is held to arguments that cannot be objects as well.
    """
    read_effect = self.model.read_has_dynamic_effect
    if reads_may_throw or read_established is not None:
        read_effect = self.throwing_read_effect(read_established)
    return side_effect_free(
        node,
        defunct,
        self.is_pure_call,
        read_effect,
        member_safe or self._getter_free_read,
        call_established or self._established_call_default,
        discarded,
        self.is_pure_call_discarded,
        coercions_may_write,
        self.store_unobserved,
        self.calls_a_pure_intrinsic,
        assumptions=self.assumed_keys,
    )
def throwing_read_effect(self, read_established)

The read leaf for a caller that keeps the throw half of the contract: rejects a read that fires a with object's getter or may throw a ReferenceError, except one read_established vouches a creating write has certainly completed for. is_side_effect_free composes it for its own callers; a pass that reaches the module-level side_effect_free directly passes it as that function's read_effect, so every discarding context asks one read contract.

Expand source code Browse git
def throwing_read_effect(
    self, read_established: Callable[[JsIdentifier], bool] | None,
) -> Callable[[Node], bool]:
    """
    The read leaf for a caller that keeps the throw half of the contract: rejects a read that
    fires a `with` object's getter or may throw a `ReferenceError`, except one *read_established*
    vouches a creating write has certainly completed for. `is_side_effect_free` composes it for
    its own callers; a pass that reaches the module-level `side_effect_free` directly passes it
    as that function's *read_effect*, so every discarding context asks one read contract.
    """
    if read_established is None:
        return self._read_effectful_or_throwing
    return lambda node: self.read_throws(node, read_established)
def read_throws(self, node, read_established)

Whether reading node fires a with object's getter or may throw a ReferenceError read_established does not vouch the read is past — a creating write that has completed, or a lexical binding whose declaration has run, ending its temporal dead zone. A read of a let/const/class binding is a candidate here even though the name resolves, since it throws all the same while the declaration has not run; the establishment proof clears it once the declaration is ordered before the read. The predicate throwing_read_effect composes, exposed directly so a caller that queries one node at a time (the reflective inliner) allocates no closure per read.

Expand source code Browse git
def read_throws(
    self, node: Node, read_established: Callable[[JsIdentifier], bool],
) -> bool:
    """
    Whether reading *node* fires a `with` object's getter or may throw a `ReferenceError`
    *read_established* does not vouch the read is past — a creating write that has completed, or
    a lexical binding whose declaration has run, ending its temporal dead zone. A read of a
    `let`/`const`/`class` binding is a candidate here even though the name resolves, since it
    throws all the same while the declaration has not run; the establishment proof clears it
    once the declaration is ordered before the read. The predicate `throwing_read_effect`
    composes, exposed directly so a caller that queries one node at a time (the reflective
    inliner) allocates no closure per read.
    """
    if self.model.read_has_dynamic_effect(node):
        return True
    if not isinstance(node, JsIdentifier):
        return False
    if not self.model.read_may_raise_reference_error(node):
        return False
    return not read_established(node)
def binding_is_immutable_container(self, binding, *, inert_methods=None, exclude=None, direct_eval_ordered=False)

Whether binding holds a container — an object or array — whose element and property values are stable after construction, so that an access into it may be soundly inlined at its read sites. Every reference must read through the container (obj.k, obj[i]) or plainly rebind the name (obj = ..., whose value the caller resolves by domination); a write through the container (obj.k = v, obj[i]++, delete obj[i], a for-of or destructuring target) makes it mutable. A method invoked on the container (obj.m(…)) may mutate it — an array's sort/push/splice and so on — so by default it too counts as mutable, except a method of Array.prototype that only reads its receiver (_ARRAY_READING_METHODS) called on an array literal without holes while the program leaves that prototype trusted; a caller that knows some of the container's own methods cannot mutate it (an object literal with no this-bound property) names their keys in inert_methods: a call of one of those on the container is permitted, and so is a call on a value it holds (obj.k.split(…)), which such a caller vouches for. A method the container inherits is never one of them: obj.valueOf() hands the container back and obj.__defineGetter__(k, g) replaces a property. A reference that escapes is safe in three cases: it aliases another binding that is itself an immutable container (alias-following the textual predicates this replaces could not do, and the reason a reassigned-and-aliased lookup array stays inlinable), it is passed to a statically known function as an argument whose parameter is itself an immutable container (so the callee neither mutates nor further-escapes it), or it is the value of a store no code observes (store_unobserved). Any other escape — returned, stored as a property, passed to a call that cannot be resolved — is treated conservatively as mutable, and so is an exported binding or an alias of one: an importer holds the container and may change it after the module has run, which no reference in this file records. A mutation through a dynamic scope is modelled: a with body that names the container — a member write, method call, reassignment, or escape — is attributed to it as a dynamic reference and judged by the same role logic, so a with that never names it keeps it foldable, and a direct eval in a local container's own function makes it mutable. The one residual is a script-scope container reached by an opaque global surface — a direct eval, Function, timer, or dynamic global write whose code cannot be read — which cannot be frozen without also freezing the lookup arrays real samples fold, so it is left to the caller's reflection reasoning, the trust an unresolved external call already receives.

The query is over a resolved binding, so it is shadowing-correct, and it descends through alias chains, callee parameters, and nested functions, so a capturing closure that mutates the container is caught. The answer is fixed for the model's lifetime — a binding's reference set does not change — so it is memoized per (binding, inert_methods, direct_eval_ordered). A caller may pass exclude to disregard references within that subtree — asking whether the container is stable across the rest of the program, ignoring a read site about to be relocated into it; such a query is not memoized, since the answer depends on the excluded region.

direct_eval_ordered is the caller's promise that it has ordered every direct-eval site of the container's owning function against the read it is folding — the definition-to-read span ReachingModel.value_preserved proves, whose kills hold those sites — so the eval leg does not refuse here. A caller that cannot order its reads against the sites keeps the default and the refusal; the promise covers the binding asked about and not the aliases its escapes are judged through, which keep the strict answer.

Expand source code Browse git
def binding_is_immutable_container(
    self,
    binding: Binding,
    *,
    inert_methods: frozenset[str] | None = None,
    exclude: Node | None = None,
    direct_eval_ordered: bool = False,
) -> bool:
    """
    Whether *binding* holds a container — an object or array — whose element and property values
    are stable after construction, so that an access into it may be soundly inlined at its read
    sites. Every reference must read through the container (`obj.k`, `obj[i]`) or plainly rebind
    the name (`obj = ...`, whose value the caller resolves by domination); a write through the
    container (`obj.k = v`, `obj[i]++`, `delete obj[i]`, a `for-of` or destructuring target)
    makes it mutable. A method invoked on the container (`obj.m(...)`) may mutate it — an
    array's `sort`/`push`/`splice` and so on — so by default it too counts as mutable, except
    a method of `Array.prototype` that only reads its receiver (`_ARRAY_READING_METHODS`)
    called on an array literal without holes while the program leaves that prototype trusted;
    a caller that knows some of the container's own methods cannot mutate it (an object literal
    with no `this`-bound property) names their keys in *inert_methods*: a call of one of those
    on the container is permitted, and so is a call on a value it holds (`obj.k.split(...)`),
    which such a caller vouches for. A method the container inherits is never one of them:
    `obj.valueOf()` hands the container back and `obj.__defineGetter__(k, g)` replaces a
    property. A reference that escapes is safe in three cases: it aliases another binding that
    is itself an immutable container (alias-following the textual predicates this replaces could
    not do, and the reason a reassigned-and-aliased lookup array stays inlinable), it is passed
    to a statically known function as an argument whose parameter is itself an immutable
    container (so the callee neither mutates nor further-escapes it), or it is the value of a
    store no code observes (`store_unobserved`). Any other escape — returned, stored as a
    property, passed to a call that cannot be resolved — is treated conservatively as mutable,
    and so is an exported binding or an alias of one: an importer holds the container and may
    change it after the module has run, which no reference in this file records. A mutation
    through a dynamic scope is modelled: a `with` body that names the container — a member
    write, method call, reassignment, or escape — is attributed to it as a dynamic reference and
    judged by the same role logic, so a `with` that never names it keeps it foldable, and a
    direct `eval` in a local container's own function makes it mutable. The one residual is a
    script-scope container reached by an opaque global surface — a direct `eval`, `Function`,
    timer, or dynamic global write whose code cannot be read — which cannot be frozen without
    also freezing the lookup arrays real samples fold, so it is left to the caller's reflection
    reasoning, the trust an unresolved external call already receives.

    The query is over a *resolved binding*, so it is shadowing-correct, and it descends through
    alias chains, callee parameters, and nested functions, so a capturing closure that mutates
    the container is caught. The answer is fixed for the model's lifetime — a binding's
    reference set does not change — so it is memoized per
    `(binding, inert_methods, direct_eval_ordered)`. A caller may pass *exclude* to
    disregard references within that subtree — asking whether the container is stable across
    the rest of the program, ignoring a read site about to be relocated into it; such a query is
    not memoized, since the answer depends on the excluded region.

    *direct_eval_ordered* is the caller's promise that it has ordered every direct-`eval` site
    of the container's owning function against the read it is folding — the definition-to-read
    span `ReachingModel.value_preserved` proves, whose kills hold those sites — so the eval
    leg does not refuse here. A caller that cannot order its reads against the sites keeps the
    default and the refusal; the promise covers the binding asked about and not the aliases
    its escapes are judged through, which keep the strict answer.
    """
    if exclude is not None:
        return self._immutable_container(
            binding, set(), inert_methods, exclude, direct_eval_ordered)
    key = (id(binding), inert_methods, direct_eval_ordered)
    cached = self._immutable_cache.get(key)
    if cached is None:
        cached = self._immutable_container(
            binding, set(), inert_methods, None, direct_eval_ordered)
        self._immutable_cache[key] = cached
    return cached
def static_callee(self, call)

The function a call invokes, resolved permissively through function_of: a direct function or arrow expression callee, or an identifier bound to a single function — a declaration, a var/let/const initializer, or the value a name is assigned exactly once. For a name that held a value and was then reassigned this returns the post-reassignment value, which is the running target only where that reassignment is established before the call; a consumer that cannot order the reassignment against the call must use unambiguous_callee instead. None for a method call, a parameter, a redeclared or dynamically-rebindable binding, or an unresolved name.

Expand source code Browse git
def static_callee(
    self, call: JsCallExpression
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
    """
    The function a call invokes, resolved permissively through `function_of`: a direct function or
    arrow expression callee, or an identifier bound to a single function — a declaration, a
    `var`/`let`/`const` initializer, or the value a name is assigned exactly once. For a name that
    held a value and was then reassigned this returns the post-reassignment value, which is the
    running target only where that reassignment is established before the call; a consumer that
    cannot order the reassignment against the call must use `unambiguous_callee` instead. `None` for
    a method call, a parameter, a redeclared or dynamically-rebindable binding, or an unresolved name.
    """
    callee = call.callee
    if isinstance(callee, (JsFunctionExpression, JsArrowFunctionExpression)):
        return callee
    if not isinstance(callee, JsIdentifier):
        return None
    return self.function_of(self.model.resolve(callee))
def a_name_this_file_binds_holds_the_callee(self, call)

Whether call names its callee with an identifier this file binds, while static_callee declines to say which function that binding holds.

A caller reasoning about what a call may have done reads static_callee answering None in two ways, and this tells them apart. Where the callee is a method, a host function, a name nothing here declares, or one holding what the host passed (SemanticModel.supplied_from_outside()), None means the call runs something outside this file's reckoning, which is a standing condition every such caller was written under. Where it is a name this file binds, None means the model saw the binding and would not state its value - one Annex B copies into a block's enclosing scope is such a function - and what it runs may be any of the ones written here, one that writes the very binding being reasoned about included.

Expand source code Browse git
def a_name_this_file_binds_holds_the_callee(self, call: JsCallExpression) -> bool:
    """
    Whether *call* names its callee with an identifier this file binds, while `static_callee`
    declines to say which function that binding holds.

    A caller reasoning about what a call may have done reads `static_callee` answering `None` in
    two ways, and this tells them apart. Where the callee is a method, a host function, a name
    nothing here declares, or one holding what the host passed
    (`refinery.lib.scripts.js.analysis.model.SemanticModel.supplied_from_outside`), `None` means
    the call runs something outside this file's reckoning, which is a standing condition every
    such caller was written under. Where it is a name this file binds, `None` means the model
    saw the binding and would not state its value - one Annex B copies into a block's enclosing
    scope is such a function - and what it runs may be any of the ones written here, one that
    writes the very binding being reasoned about included.
    """
    callee = strip_parens(call.callee)
    if not isinstance(callee, JsIdentifier):
        return False
    if self.model.supplied_from_outside(self.model.resolve(callee)):
        return False
    return self.static_callee(call) is None
def unambiguous_callee(self, call)

The ordering-free twin of static_callee, for a consumer that reasons about a call without knowing where it sits in execution order. Identical except an identifier callee resolves through unambiguous_function, so a name that held a value and was then reassigned — whose running target depends on the call's position relative to the reassignment — yields None rather than the post-reassignment value.

Expand source code Browse git
def unambiguous_callee(
    self, call: JsCallExpression
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
    """
    The ordering-free twin of `static_callee`, for a consumer that reasons about a call without
    knowing where it sits in execution order. Identical except an identifier callee resolves through
    `unambiguous_function`, so a name that held a value and was then reassigned — whose running target
    depends on the call's position relative to the reassignment — yields `None` rather than the
    post-reassignment value.
    """
    return unambiguous_callee(self.model, call)
def store_unobserved(self, member)

Whether the store whose target is member is observed by no code: it is the store module.exports = v through the module the host passed, and nothing reads what the file stores there (SemanticModel.module_exports_unread()). The host made exports a data property of its own on that object, so the store runs no setter and cannot fail; under the reading as an ES module, where the name may be absent, the read of module keeps its own throw.

Expand source code Browse git
def store_unobserved(self, member: JsMemberExpression) -> bool:
    """
    Whether the store whose target is *member* is observed by no code: it is the store
    `module.exports = v` through the `module` the host passed, and nothing reads what the file
    stores there (`SemanticModel.module_exports_unread`). The host made `exports` a data
    property of its own on that object, so the store runs no setter and cannot fail; under the
    reading as an ES module, where the name may be absent, the read of `module` keeps its own
    throw.
    """
    if not is_plain_store_target(member):
        return False
    base = strip_parens(member.object)
    if not isinstance(base, JsIdentifier) or base.name != 'module':
        return False
    if not key_is(member, _EXPORTS, NO_IS_STRICTER):
        return False
    binding = self.model.resolve(base)
    if binding is None or binding is not self.model.root_scope.bindings.get('module'):
        return False
    return self.model.module_exports_unread()
def calls_a_pure_intrinsic(self, call)

Whether call invokes one of the trusted pure intrinsic functions, PURE_INTRINSIC_METHODS and PURE_GLOBAL_FUNCTIONS. Each of them converts the arguments it is handed, and converting an object runs code the program may have installed, as parseInt([5]) runs Array.prototype.join; the purity of the call covers the intrinsic and not that conversion.

Expand source code Browse git
def calls_a_pure_intrinsic(
    self,
    call: JsCallExpression | JsNewExpression | JsTaggedTemplateExpression,
) -> bool:
    """
    Whether *call* invokes one of the trusted pure intrinsic functions, `PURE_INTRINSIC_METHODS`
    and `PURE_GLOBAL_FUNCTIONS`. Each of them converts the arguments it is handed, and converting
    an object runs code the program may have installed, as `parseInt([5])` runs
    `Array.prototype.join`; the purity of the call covers the intrinsic and not that conversion.
    """
    callee = call.tag if isinstance(call, JsTaggedTemplateExpression) else call.callee
    if isinstance(callee, JsMemberExpression) and not callee.computed:
        base = callee.object
        return (
            isinstance(base, JsIdentifier)
            and key_is(callee, _PURE_METHODS_OF.get(base.name, frozenset()), NO_IS_STRICTER)
            and self._is_global_intrinsic(base)
        )
    return (
        isinstance(callee, JsIdentifier)
        and callee.name in PURE_GLOBAL_FUNCTIONS
        and self._is_global_intrinsic(callee)
    )
def function_of(self, binding)

The single function a binding stably resolves to — a sole declaration's function declaration or function/arrow initializer, or a name assigned a function exactly once (f = function(){}, the form namespace flattening leaves) — or None when the binding is absent, redeclared, reassigned to more than one value, dynamically rebindable, or not bound to a function. A lone assignment counts because the name denotes that one function wherever it is not in the value's temporal dead zone; a caller that also needs the value established before a use orders it separately. The binding-level twin of static_callee, and the function-typed specialization of SemanticModel.singular_value(): it filters that value-resolution to a function node.

Expand source code Browse git
def function_of(
    self, binding: Binding | None
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
    """
    The single function a *binding* stably resolves to — a sole declaration's function declaration or
    function/arrow initializer, or a name assigned a function exactly once (`f = function(){}`, the
    form namespace flattening leaves) — or `None` when the binding is absent, redeclared, reassigned
    to more than one value, dynamically rebindable, or not bound to a function. A lone assignment
    counts because the name denotes that one function wherever it is not in the value's temporal dead
    zone; a caller that also needs the value established before a use orders it separately. The
    binding-level twin of `static_callee`, and the function-typed specialization of
    `SemanticModel.singular_value`: it filters that value-resolution to a function node.
    """
    value = self.model.singular_value(binding)
    if isinstance(value, FUNCTION_NODES):
        return value
    return None
def unambiguous_function(self, binding)

The single function binding names for a consumer that resolves calls without execution ordering — the interpreter — or None. A name that carried a value from its declaration and is then reassigned holds two values across its life and is refused; since SemanticModel.singular_value() took the complete-singleton contract, that refusal is the query's own, and this answer equals function_of. The name survives for the intent it documents at its call sites — the ordering-free view the evaluator's visible-functions map applied before interpretation routed resolution through the model.

Expand source code Browse git
def unambiguous_function(
    self, binding: Binding | None
) -> JsFunctionDeclaration | JsFunctionExpression | JsArrowFunctionExpression | None:
    """
    The single function *binding* names for a consumer that resolves calls without execution ordering
    — the interpreter — or `None`. A name that carried a value from its declaration and is then
    reassigned holds two values across its life and is refused; since `SemanticModel.singular_value`
    took the complete-singleton contract, that refusal is the query's own, and this answer equals
    `function_of`. The name survives for the intent it documents at its call sites — the ordering-free
    view the evaluator's visible-functions map applied before interpretation routed resolution
    through the model.
    """
    return unambiguous_function(self.model, binding)
def intrinsic_of(self, node)

The pristine intrinsic value node provably denotes: GLOBAL_OBJECT for the global object, an intrinsic root name ('Array', 'String', …) for a named intrinsic, or None. A name is returned only under intrinsics_pristine and where the identifier is unshadowed at this use site, so the result may be value-trusted — used to construct, to clear a getter-free static read, or to fold A || B. Every value it can return — globalThis and every refinery.lib.scripts.js.analysis.intrinsics.PURE_INTRINSIC_ROOTS member — is truthy, so A || B evaluates to A whenever intrinsic_of(A) is not None; a contributor extending this must preserve that truthiness invariant and never return a falsy name such as NaN/undefined.

It deliberately does NOT follow a local alias through its value — intrinsic_of of an identifier bound to var x = Array is None — because a local's value holds only where it is established, a control-flow fact this flow-insensitive query cannot certify; a consumer that owns dominance resolves the alias itself against singular_value. It likewise does not treat <global-object>.Name as a value: that read's getter-freeness rests on global_pristine, a weaker premise than value trust, so it stays the concern of _is_trusted_global_read.

Expand source code Browse git
def intrinsic_of(self, node: Node | None) -> str | _GlobalObject | None:
    """
    The pristine intrinsic value *node* provably denotes: `GLOBAL_OBJECT` for the global object,
    an intrinsic root name (`'Array'`, `'String'`, …) for a named intrinsic, or `None`. A name
    is returned only under `intrinsics_pristine` and where the identifier is unshadowed at this
    use site, so the result may be *value-trusted* — used to construct, to clear a getter-free
    static read, or to fold `A || B`. Every value it can return — `globalThis` and every
    `refinery.lib.scripts.js.analysis.intrinsics.PURE_INTRINSIC_ROOTS` member — is truthy, so
    `A || B` evaluates to `A` whenever `intrinsic_of(A)` is not `None`; a contributor extending
    this must preserve that truthiness invariant and never return a falsy name such as
    `NaN`/`undefined`.

    It deliberately does NOT follow a local alias through its value — `intrinsic_of` of an identifier
    bound to `var x = Array` is `None` — because a local's value holds only where it is established, a
    control-flow fact this flow-insensitive query cannot certify; a consumer that owns dominance
    resolves the alias itself against `singular_value`. It likewise does not treat `<global-object>.Name`
    as a value: that read's getter-freeness rests on `global_pristine`, a weaker premise than value
    trust, so it stays the concern of `_is_trusted_global_read`.
    """
    node = strip_parens(node)
    if isinstance(node, JsIdentifier):
        if node.name == 'globalThis' and self.model.lookup(node.name, self.model.scope_of(node)) is None:
            return GLOBAL_OBJECT
        if node.name in PURE_INTRINSIC_ROOTS and self._is_global_intrinsic(node):
            return node.name
        return None
    if isinstance(node, JsLogicalExpression) and node.operator == '||':
        return self.intrinsic_of(node.left)
    return None
def trusted_intrinsic(self, node)

The global name node denotes, when that one name is provably still the built-in, or None. A name qualifies when the program never binds it, never assigns to it, never writes or updates a property anywhere on it, and neither exposes a reflection surface through which it could be replaced at runtime nor stores a property on the global object under a runtime key (SemanticModel.has_opaque_global_write()), which may replace that name's value the same way.

This differs from intrinsic_of in scope of the question, not in strictness. intrinsic_of rests on intrinsics_pristine, one program-wide flag over a fixed root set, so a single Object.prototype.x = 1 withdraws trust from every intrinsic in the file — including Math, which that line cannot affect. This query answers per name, so the same program still folds Math.floor while declining Object.keys. Its callers are the constant folds, which need to know whether this built-in is intact; intrinsic_of answers the stronger question of whether a value may be trusted for construction or A || B folding, and keeps its own callers and vocabulary.

The name is not checked against a list of blessed intrinsics: whether a fold knows how to evaluate the call is the caller's question, answered by its own registry lookup. Conflating the two is what let a registry entry exist with no matching trust rule.

Shadowing needs no per-site scope resolution, because a name bound anywhere in the program is already disqualified: _globals_written collects every binding in every scope. That is stricter than JavaScript requires — a shadow inside an unrelated function does not affect this use site — and deliberately so. A name the program binds at all is one an obfuscator may be routing values through, and the price of refusing is an unfolded call rather than a wrong value.

Expand source code Browse git
def trusted_intrinsic(self, node: Node | None) -> str | None:
    """
    The global name *node* denotes, when that one name is provably still the built-in, or `None`. A
    name qualifies when the program never binds it, never assigns to it, never writes or updates a
    property anywhere on it, and neither exposes a reflection surface through which it could be
    replaced at runtime nor stores a property on the global object under a runtime key
    (`SemanticModel.has_opaque_global_write`), which may replace that name's value the same way.

    This differs from `intrinsic_of` in *scope of the question*, not in strictness. `intrinsic_of`
    rests on `intrinsics_pristine`, one program-wide flag over a fixed root set, so a single
    `Object.prototype.x = 1` withdraws trust from every intrinsic in the file — including `Math`,
    which that line cannot affect. This query answers per name, so the same program still folds
    `Math.floor` while declining `Object.keys`. Its callers are the constant folds, which need to know
    whether *this* built-in is intact; `intrinsic_of` answers the stronger question of whether a value
    may be trusted for construction or `A || B` folding, and keeps its own callers and vocabulary.

    The name is not checked against a list of blessed intrinsics: whether a fold *knows how* to
    evaluate the call is the caller's question, answered by its own registry lookup. Conflating the two
    is what let a registry entry exist with no matching trust rule.

    Shadowing needs no per-site scope resolution, because a name bound *anywhere* in the program is
    already disqualified: `_globals_written` collects every binding in every scope. That is stricter
    than JavaScript requires — a shadow inside an unrelated function does not affect this use site —
    and deliberately so. A name the program binds at all is one an obfuscator may be routing values
    through, and the price of refusing is an unfolded call rather than a wrong value.
    """
    node = strip_parens(node)
    if not isinstance(node, JsIdentifier):
        return None
    if not self.trusted_intrinsic_name(node.name):
        return None
    return node.name
def program_trust(self)

The trust questions of a fold no point is given for, each answered for the whole program: the oracle vouches for no anchor, and a load of code anywhere may precede the fold.

Expand source code Browse git
def program_trust(self) -> AnchoredTrust:
    """
    The trust questions of a fold no point is given for, each answered for the whole program:
    the oracle vouches for no anchor, and a load of code anywhere may precede the fold.
    """
    model = self.model
    return AnchoredTrust(self, lambda: False, lambda: model.imports_code() or bool(model.code_load_sites()))
def trusted_intrinsic_name(self, name)

EffectModel.trusted_intrinsic() for a caller that holds a global name rather than a node spelling it: a regular-expression literal builds its object from RegExp without naming it, so a fold deciding what /a/g matches has no identifier to ask about.

Expand source code Browse git
def trusted_intrinsic_name(self, name: str) -> bool:
    """
    `EffectModel.trusted_intrinsic` for a caller that holds a global name rather than a node
    spelling it: a regular-expression literal builds its object from `RegExp` without naming it,
    so a fold deciding what `/a/g` matches has no identifier to ask about.
    """
    if self.model.has_reflection_surface() or self.model.has_opaque_global_write():
        return False
    return name not in self._globals_written
def intrinsic_name_unwritten(self, node)

The per-name arm of trusted_intrinsic: the global name node denotes when the program never binds it, never assigns to it, and never writes or updates a property anywhere on it. The question a consumer that holds an oracle clearance composes with it — the oracle answered the reflection terms at the anchor, so the name question is what remains. A consumer without one takes trusted_intrinsic, which is this plus those terms.

Expand source code Browse git
def intrinsic_name_unwritten(self, node: Node | None) -> str | None:
    """
    The per-name arm of `trusted_intrinsic`: the global name *node* denotes when the program
    never binds it, never assigns to it, and never writes or updates a property anywhere on it.
    The question a consumer that holds an oracle clearance composes with it — the oracle
    answered the reflection terms at the anchor, so the name question is what remains. A
    consumer without one takes `trusted_intrinsic`, which is this plus those terms.
    """
    node = strip_parens(node)
    if not isinstance(node, JsIdentifier):
        return None
    if node.name in self._globals_written:
        return None
    return node.name
def prototype_name_unwritten(self, value_type)

The per-name arm of trusted_prototype: whether the intrinsic owning value_type's prototype was never written. The question a consumer that holds an oracle clearance composes with it; a consumer without one takes trusted_prototype, which is this plus its reflection term.

Expand source code Browse git
def prototype_name_unwritten(self, value_type: type) -> bool:
    """
    The per-name arm of `trusted_prototype`: whether the intrinsic owning *value_type*'s
    prototype was never written. The question a consumer that holds an oracle clearance
    composes with it; a consumer without one takes `trusted_prototype`, which is this plus its
    reflection term.
    """
    owner = PROTOTYPE_OWNERS.get(value_type.__name__)
    return owner is not None and owner not in self._globals_written
def trusted_prototype(self, value_type)

Whether the prototype supplying value_type's methods is provably unmodified, so a method call on a receiver of that type still means what the language says. A method call on a literal receiver names no global, which is exactly why it needs its own question: trusted_intrinsic can only judge names the expression mentions, and 'ab'.toUpperCase() mentions none.

The owning intrinsic is looked up rather than assumed, and then asked the same per-name question a named callee gets — String.prototype.toUpperCase = f and Object.defineProperty(Array.prototype, …) are both already recorded as writes to String and Array by build_intrinsic_writes(). A type with no known owner is never trusted.

Expand source code Browse git
def trusted_prototype(self, value_type: type) -> bool:
    """
    Whether the prototype supplying *value_type*'s methods is provably unmodified, so a method call on
    a receiver of that type still means what the language says. A method call on a literal receiver
    names no global, which is exactly why it needs its own question: `trusted_intrinsic` can only
    judge names the expression mentions, and `'ab'.toUpperCase()` mentions none.

    The owning intrinsic is looked up rather than assumed, and then asked the same per-name question a
    named callee gets — `String.prototype.toUpperCase = f` and
    `Object.defineProperty(Array.prototype, ...)` are both already recorded as writes to `String` and
    `Array` by `refinery.lib.scripts.js.analysis.intrinsics.build_intrinsic_writes`. A type with
    no known owner is never trusted.
    """
    owner = PROTOTYPE_OWNERS.get(value_type.__name__)
    if owner is None:
        return False
    if self.model.has_reflection_surface():
        return False
    return self.prototype_name_unwritten(value_type)
def global_key_written(self, name, key)

Whether the program writes the property key on a chain rooted at the global name: Object.prototype.constructor = C, delete Object.getPrototypeOf, or a descriptor installed for that key. The per-name question _globals_written answers is too coarse for a caller that cares which property was replaced — a file patching Object.prototype.z has written Object, and refusing everything about Object on that basis refuses the very files the question is asked about.

The write is attributed to a name rather than to a receiver, which is what makes it answerable at all: the receiver of Object.prototype.constructor = C is a value no static analysis names, while the chain it is written through is rooted in one that is. A name whose written keys cannot be bounded — one the program binds, hands to code this analysis cannot read, writes a computed key on, or installs a descriptor on from a value it cannot read — answers True for every key, and so does a name outside KEYED_WRITE_ROOTS, which the scan records nothing about at all.

Expand source code Browse git
def global_key_written(self, name: str, key: str) -> bool:
    """
    Whether the program writes the property *key* on a chain rooted at the global *name*:
    `Object.prototype.constructor = C`, `delete Object.getPrototypeOf`, or a descriptor
    installed for that key. The per-name question `_globals_written` answers is too coarse for a
    caller that cares which property was replaced — a file patching `Object.prototype.z` has
    written `Object`, and refusing everything about `Object` on that basis refuses the very
    files the question is asked about.

    The write is attributed to a *name* rather than to a receiver, which is what makes it
    answerable at all: the receiver of `Object.prototype.constructor = C` is a value no static
    analysis names, while the chain it is written through is rooted in one that is. A name whose
    written keys cannot be bounded — one the program binds, hands to code this analysis cannot
    read, writes a computed key on, or installs a descriptor on from a value it cannot read —
    answers `True` for every key, and so does a name outside
    `refinery.lib.scripts.js.analysis.intrinsics.KEYED_WRITE_ROOTS`, which the scan records
    nothing about at all.
    """
    return self.intrinsic_writes.key_written(name, key)
def builtin_writes_followed(self)

Whether every write the program makes on a built-in object is one the questions about names see (IntrinsicWrites.followable). A fold that reads a built-in's chain or converts an object without spelling the owner of either asks this beside read_chain_intact: [].constructor.prototype.join = f replaces the join of every array while writing Array by no name. A no is noted on the run's record (UnfollowedWriteRefusals).

Expand source code Browse git
def builtin_writes_followed(self) -> bool:
    """
    Whether every write the program makes on a built-in object is one the questions about names
    see (`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites.followable`). A fold that
    reads a built-in's chain or converts an object without spelling the owner of either asks
    this beside `read_chain_intact`: `[].constructor.prototype.join = f` replaces the join of
    every array while writing `Array` by no name. A no is noted on the run's record
    (`refinery.lib.scripts.js.analysis.intrinsics.UnfollowedWriteRefusals`).
    """
    followed = self.intrinsic_writes.followable
    if not followed and self._refusals is not None:
        self._refusals.note()
    return followed
def builtin_function_text(self, function)

The string converting the built-in function gives in the engine of the host the model assumes (JsEngine.function_text()), or None where that host has no one engine or the conversion may run code the program installed. It runs Function.prototype.toString unless the program gave a conversion key (CONVERSION_KEYS) to Function.prototype, to Object.prototype, or to the constructor function names; a write to the function through no name is one builtin_writes_followed refuses, and so is a write by a code string a reflection surface may run.

Expand source code Browse git
def builtin_function_text(self, function: BuiltinFunction) -> str | None:
    """
    The string converting the built-in *function* gives in the engine of the host the model
    assumes (`refinery.lib.scripts.js.analysis.environment.JsEngine.function_text`), or `None`
    where that host has no one engine or the conversion may run code the program installed. It
    runs
    `Function.prototype.toString` unless the program gave a conversion key
    (`refinery.lib.scripts.js.analysis.conversions.CONVERSION_KEYS`) to `Function.prototype`,
    to `Object.prototype`, or to the constructor *function* names; a write to the function
    through no name is one `builtin_writes_followed` refuses, and so is a write by a code
    string a reflection surface may run.
    """
    if not self.builtin_writes_followed() or self.model.has_reflection_surface():
        return None
    owners = ['Function', 'Object']
    if function.is_constructor:
        owners.append(function.name)
    if any(self.global_key_written(owner, key) for owner in owners for key in CONVERSION_KEYS):
        return None
    engine = self.model.environment.engine
    return None if engine is None else engine.function_text(function.name)
def array_iterator_conversion_untouched(self)

Whether converting an array iterator (ArrayIterator) runs Object.prototype.toString and gives [object Array Iterator]. The scan has to follow every write on a built-in object, since the prototype of every array iterator is reached through no name; the program may give Object no conversion key (CONVERSION_KEYS) and write no Iterator, whose prototype every array iterator inherits; and no reflection surface may run a code string that does either.

Expand source code Browse git
def array_iterator_conversion_untouched(self) -> bool:
    """
    Whether converting an array iterator
    (`refinery.lib.scripts.js.values.ArrayIterator`) runs `Object.prototype.toString` and gives
    `[object Array Iterator]`. The scan has to follow every write on a built-in object, since the
    prototype of every array iterator is reached through no name; the program may give `Object`
    no conversion key (`refinery.lib.scripts.js.analysis.conversions.CONVERSION_KEYS`) and write
    no `Iterator`, whose prototype every array iterator inherits; and no reflection surface may
    run a code string that does either.
    """
    if not self.builtin_writes_followed() or self.model.has_reflection_surface():
        return False
    if 'Iterator' in self.intrinsic_writes.names:
        return False
    return not any(self.global_key_written('Object', key) for key in CONVERSION_KEYS)
def regexp_text(self, value)

The string converting the regular expression value gives in the engine of the host the model assumes (JsEngine.regexp_text()), or None where that host has no one engine or the conversion may run code the program installed. It runs RegExp.prototype.toString, which reads the getters of _REGEXP_TEXT_KEYS, unless the program writes one of those keys on RegExp or a conversion key on Object; a write through no name is one builtin_writes_followed refuses, and so is a write by a code string a reflection surface may run.

Expand source code Browse git
def regexp_text(self, value: RegExpObject) -> str | None:
    """
    The string converting the regular expression *value* gives in the engine of the host the
    model assumes (`refinery.lib.scripts.js.analysis.environment.JsEngine.regexp_text`), or
    `None` where that host has no one engine or the conversion may run code the program
    installed. It runs `RegExp.prototype.toString`, which reads the getters of
    `_REGEXP_TEXT_KEYS`, unless the program writes one of those keys on `RegExp` or a conversion
    key on `Object`; a write through no name is one `builtin_writes_followed` refuses, and so
    is a write by a code string a reflection surface may run.
    """
    engine = self.model.environment.engine
    if engine is None or not self.builtin_writes_followed() or self.model.has_reflection_surface():
        return None
    if any(self.global_key_written('RegExp', key) for key in _REGEXP_TEXT_KEYS):
        return None
    if any(self.global_key_written('Object', key) for key in CONVERSION_KEYS):
        return None
    return engine.regexp_text(value.source)
def builtin_function_named(self, function)

Whether the built-in function still owns the name the language gave it: the program writes no name key on the constructor function names, and none on any built-in through no name (builtin_writes_followed).

Expand source code Browse git
def builtin_function_named(self, function: BuiltinFunction) -> bool:
    """
    Whether the built-in *function* still owns the `name` the language gave it: the program
    writes no `name` key on the constructor *function* names, and none on any built-in through
    no name (`builtin_writes_followed`).
    """
    if not self.builtin_writes_followed():
        return False
    return not function.is_constructor or not self.global_key_written(function.name, 'name')
def builtin_function_constructor_untouched(self, function)

Whether reading constructor off the built-in function gives Function: a function owns no constructor, so the read reaches Function.prototype, unless the program writes that key there or gives the function a constructor or a prototype of its own. Each is a write on Function or on the constructor function names, or one through no name (builtin_writes_followed).

Expand source code Browse git
def builtin_function_constructor_untouched(self, function: BuiltinFunction) -> bool:
    """
    Whether reading `constructor` off the built-in *function* gives `Function`: a function owns
    no `constructor`, so the read reaches `Function.prototype`, unless the program writes that
    key there or gives the function a `constructor` or a prototype of its own. Each is a write
    on `Function` or on the constructor *function* names, or one through no name
    (`builtin_writes_followed`).
    """
    if not self.builtin_writes_followed() or self.global_key_written('Function', 'constructor'):
        return False
    if not function.is_constructor:
        return True
    return not any(self.global_key_written(function.name, key) for key in SPECIES_KEYS)
def read_chain_intact(self, value_type, region=None)

Whether every prototype a plain property read on a value of value_type consults is unmodified, so the read touches a data slot and runs nothing. Strictly stronger than trusted_prototype, which answers the neighbouring question for a method call, and the two must not be merged: a method resolves on the prototype that owns it, so Array.prototype.join shadows anything installed on Object.prototype and a patch there cannot change what [1, 2].join('-') means. A read of an arbitrary name has no such shadow — Object.prototype roots every chain, so a getter installed there is reached by a read on an array literal, on a primitive, and on Math alike.

Confirmed against Node in both directions rather than reasoned from the specification: patching Object.prototype.join leaves [1, 2].join('-') intact, while patching Object.prototype.zz makes [1, 2].zz run a getter.

Two separate facts have to hold and this is their conjunction: no chain root was written, which is chain_roots_unwritten, and no reflective surface could have written one without saying so. A caller for which the second costs more than it buys takes the first alone — see the note there for what that trade is and where it is made.

Expand source code Browse git
def read_chain_intact(self, value_type: type, region: list[Node] | None = None) -> bool:
    """
    Whether every prototype a plain property read on a value of *value_type* consults is unmodified, so
    the read touches a data slot and runs nothing. Strictly stronger than `trusted_prototype`, which
    answers the neighbouring question for a method *call*, and the two must not be merged: a method
    resolves on the prototype that owns it, so `Array.prototype.join` shadows anything installed on
    `Object.prototype` and a patch there cannot change what `[1, 2].join('-')` means. A read of an
    arbitrary name has no such shadow — `Object.prototype` roots every chain, so a getter installed
    there is reached by a read on an array literal, on a primitive, and on `Math` alike.

    Confirmed against Node in both directions rather than reasoned from the specification: patching
    `Object.prototype.join` leaves `[1, 2].join('-')` intact, while patching `Object.prototype.zz`
    makes `[1, 2].zz` run a getter.

    Two separate facts have to hold and this is their conjunction: no chain root was written,
    which is `chain_roots_unwritten`, and no reflective surface could have written one without
    saying so. A caller for which the second costs more than it buys takes the first alone — see
    the note there for what that trade is and where it is made.
    """
    owner = PROTOTYPE_OWNERS.get(value_type.__name__)
    if owner is None:
        return False
    return self._prototypes_intact(owner, INHERITED_CHAIN_ROOTS, region)
def chain_roots_unwritten(self, value_type)

Whether the program writes no prototype a plain property read on a value of value_type consults. This is read_chain_intact without its reflection term, and the difference is not a weakening of the same question but a different one: read_chain_intact also refuses wherever SemanticModel.has_reflection_surface() holds, and that predicate answers whether code could reference a global by name — true of new Function('return this'), which writes no prototype at all.

The distinction is what the answer costs where it is wrong, and it decides who may ask this rather than being a judgement each caller makes for itself. A caller folding one expression pays an unfolded expression for refusing, so it may as well refuse under a surface, and every one of them asks read_chain_intact. A caller deciding whether a whole pass may run pays the pass, and a reflective surface is exactly what real obfuscated files carry: the surface is present in the input and gone from the finished output, so a pass gated on it never runs and never clears the surface that was gating it. Only those callers ask this — today namespace flattening and the dispatcher unwrapper — and they accept that an unresolvable eval could in principle have written a prototype, which is no worse than the nothing they asked before.

Expand source code Browse git
def chain_roots_unwritten(self, value_type: type) -> bool:
    """
    Whether the program writes no prototype a plain property read on a value of *value_type*
    consults. This is `read_chain_intact` without its reflection term, and the difference is not
    a weakening of the same question but a different one: `read_chain_intact` also refuses
    wherever `SemanticModel.has_reflection_surface` holds, and that predicate answers whether
    code could reference a global *by name* — true of `new Function('return this')`, which
    writes no prototype at all.

    The distinction is what the answer costs where it is wrong, and it decides who may ask this
    rather than being a judgement each caller makes for itself. A caller folding one expression
    pays an unfolded expression for refusing, so it may as well refuse under a surface, and
    every one of them asks `read_chain_intact`. A caller deciding whether a whole *pass* may run
    pays the pass, and a reflective surface is exactly what real obfuscated files carry: the
    surface is present in the input and gone from the finished output, so a pass gated on it
    never runs and never clears the surface that was gating it. Only those callers ask this —
    today namespace flattening and the dispatcher unwrapper — and they accept that an
    unresolvable `eval` could in principle have written a prototype, which is no worse than the
    nothing they asked before.
    """
    return self.intrinsic_writes.chain_roots_unwritten(value_type)
def function_object_untouched(self, function, exempt=())

Whether no code but the reads in exempt can give the function object function a property of its own, or hand it to code that could: whether every use of it is FunctionUse.IN_PLACE. A fold deciding whether the object may carry a property it reads asks this; a conversion asks function_conversion_untouched.

The object is reached only through the names holding it — its declaring name, the one name a declarator or an assignment statement stores it into, a named expression's own name — or as the value of a property of an object literal that one name holds as a read-only container, and a literal no name holds is reached only where it is written. An assignment that is not a statement of its own hands its value on as well, as f(W = function () {}) does, so a function written there is not left in place. Every read of those names, and the position of a literal no name holds, has to leave the object where it is: call it, run call or apply on it, read its toString, name or length, convert, compare or test it, or hand it to String while String is the built-in. A read may also copy it into another name, or hand it to a parameter of a function the call certainly runs, when the reads of that name or parameter answer the same question in turn, up to _FUNCTION_FOLLOW_LIMIT steps. The left of instanceof is handed to a method of the right operand, a tagged template hands its holes to the tag, and new hands the function to its own body as new.target and to the object it builds as its constructor, so none of these leave it in place. Its body may use its own arguments only by index, length, spread or as the second argument of apply, since arguments.callee is the function. A program that spells caller anywhere is refused: g.caller is whichever function called g.

Expand source code Browse git
def function_object_untouched(self, function: Node, exempt: Sequence[Node] = ()) -> bool:
    """
    Whether no code but the reads in *exempt* can give the function object *function* a
    property of its own, or hand it to code that could: whether every use of it is
    `FunctionUse.IN_PLACE`. A fold deciding whether the object may carry a property it reads
    asks this; a conversion asks `function_conversion_untouched`.

    The object is reached only through the names holding it — its declaring name, the one name
    a declarator or an assignment statement stores it into, a named expression's own name — or
    as the value of a property of an object literal that one name holds as a read-only
    container, and a literal no name holds is reached only where it is written. An assignment
    that is not a statement of its own hands its value on as well, as `f(W = function () {})`
    does, so a function written there is not left in place. Every read of those names, and the
    position of a literal no name holds, has to leave the object where it is: call it, run
    `call` or `apply` on it, read its `toString`, `name` or `length`, convert, compare or test
    it, or hand it to `String` while `String` is the built-in. A read may also copy it into
    another name, or hand it to a parameter of a function the call certainly runs, when the
    reads of that name or parameter answer the same question in turn, up to
    `_FUNCTION_FOLLOW_LIMIT` steps. The left of
    `instanceof` is handed to a method of the right operand, a tagged template hands its holes
    to the tag, and `new` hands the function to its own body as `new.target` and to the object
    it builds as its `constructor`, so none of these leave it in place. Its body may use its own
    `arguments` only by index, length, spread or as the second argument of `apply`, since
    `arguments.callee` is the function. A program that spells `caller` anywhere is refused:
    `g.caller` is whichever function called `g`.
    """
    return self._function_use(function, frozenset(exempt)) is FunctionUse.IN_PLACE
def function_conversion_untouched(self, function, exempt=())

Whether no code but the reads in exempt can give the function object function a toString, valueOf or Symbol.toPrimitive of its own, or a new prototype, so that converting it answers the text it was written with as far as the function itself goes. A function function_object_untouched accepts converts so, and a use that may install a conversion (FunctionUse.CONVERSION) refuses.

A function that is only handed on converts so when no install the program makes can reach it (ConversionInstalls), but this is asked only of the tree the run has settled, since a key the run decodes later may yet spell an install. On any other tree the question is refused and the run record notes the tree (ConversionSettlement); a model built without a record refuses it and notes nothing. The tree is read as it stands, not as this model saw it, so a model a pass holds across its own changes refuses as soon as the tree has moved. A function of another tree, such as one in the body of a new Function the interpreter parsed, is judged by its own position alone: this model holds none of its names, and the scan never read that tree.

The run record and the tree are read on every question; only the grade and the scan are remembered.

Expand source code Browse git
def function_conversion_untouched(self, function: Node, exempt: Sequence[Node] = ()) -> bool:
    """
    Whether no code but the reads in *exempt* can give the function object *function* a
    `toString`, `valueOf` or `Symbol.toPrimitive` of its own, or a new prototype, so that
    converting it answers the text it was written with as far as the function itself goes. A
    function `function_object_untouched` accepts converts so, and a use that may install a
    conversion (`FunctionUse.CONVERSION`) refuses.

    A function that is only handed on converts so when no install the program makes can reach
    it (`refinery.lib.scripts.js.analysis.conversions.ConversionInstalls`), but this is asked
    only of the tree the run has settled, since a key the run decodes later may yet spell an
    install. On any other tree the question is refused and the run record notes the tree
    (`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`); a model built without
    a record refuses it and notes nothing. The tree is read as it stands, not as this model saw
    it, so a model a pass holds across its own changes refuses as soon as the tree has moved. A
    function of another tree, such as one in the body of a `new Function` the interpreter
    parsed, is judged by its own position alone: this model holds none of its names, and the
    scan never read that tree.

    The run record and the tree are read on every question; only the grade and the scan are
    remembered.
    """
    use = self._function_use(function, frozenset(exempt))
    if use is FunctionUse.IN_PLACE:
        return True
    if use is FunctionUse.CONVERSION:
        return False
    if tree_root(function) is not self.model.root:
        return False
    if self.conversion_installs.may_reach(function):
        return False
    settlement = self._settlement
    if settlement is None:
        return False
    version = tree_version(self.model.root)
    if settlement.settled(version):
        return True
    settlement.defer(version)
    return False
def call_is_foldable(self, node, *, receiver_type=None, trust=None)

Whether the call node may be evaluated to a constant and have its result replace it. This is the one admission gate every fold shares, so that a rule proven necessary at one site cannot be missing at another — the fold surface acquired its divergent hand-rolled checks precisely because each transform owned its own.

Three questions must all answer yes:

  • The callee is still the built-in it is spelled as. A named callee (parseInt(…), String.fromCharCode(…)) is judged by trusted_intrinsic on its root name; a call on a literal receiver is judged by trusted_prototype on the type that literal's syntax fixes; and a call on another call — a chain link — is judged by asking this whole question of that inner call. receiver_type covers the remaining case, a caller holding an already-evaluated receiver whose type it knows and this model does not.
  • Every function-valued argument writes nothing outside itself. is_effect_free_when_discarded is the right predicate rather than is_pure: a callback that mutates a fresh local and returns it — the reduce accumulator idiom — is not pure, but that mutation is the value being computed and an evaluator that runs the call reproduces it.
  • Nothing in the argument list is itself a call this gate does not also clear, so admitting an outer call cannot smuggle in an inner one. A nested built-in (Math.floor(Math.abs(-1.7))) is fine precisely because the same questions are asked of it.
  • No part of the call stores anything the residual would go on to read. The fold deletes the whole expression, so a store is lost wherever in it the store was written — an argument, the receiver, or the computed key naming the method; see _call_stores_nothing.

Trust is not evaluability, and this answers only the first. trusted_intrinsic says unknownFn is undisturbed — true, and no help, since no such built-in exists to fold. Whether a callee can actually be evaluated is the caller's question, answered by its own registry lookup before it asks this one; conflating the two is what let a registry entry exist with no matching trust rule.

The first question is asked of the whole program, or at a point where trust is given: the AnchoredTrust of that point answers it, for the call and for every call nested in its arguments.

Expand source code Browse git
def call_is_foldable(
    self,
    node: JsCallExpression,
    *,
    receiver_type: type | None = None,
    trust: AnchoredTrust | None = None,
) -> bool:
    """
    Whether the call *node* may be evaluated to a constant and have its result replace it. This is the
    one admission gate every fold shares, so that a rule proven necessary at one site cannot be missing
    at another — the fold surface acquired its divergent hand-rolled checks precisely because each
    transform owned its own.

    Three questions must all answer yes:

    - The callee is still the built-in it is spelled as. A named callee (`parseInt(...)`,
      `String.fromCharCode(...)`) is judged by `trusted_intrinsic` on its root name; a call on a
      literal receiver is judged by `trusted_prototype` on the type that literal's syntax fixes; and a
      call on another call — a chain link — is judged by asking this whole question of that inner call.
      `receiver_type` covers the remaining case, a caller holding an already-evaluated receiver whose
      type it knows and this model does not.
    - Every function-valued argument writes nothing outside itself. `is_effect_free_when_discarded` is
      the right predicate rather than `is_pure`: a callback that mutates a fresh local and returns it —
      the `reduce` accumulator idiom — is not pure, but that mutation is the value being computed and an
      evaluator that runs the call reproduces it.
    - Nothing in the argument list is itself a call this gate does not also clear, so admitting an outer
      call cannot smuggle in an inner one. A nested built-in (`Math.floor(Math.abs(-1.7))`) is fine
      precisely because the same questions are asked of it.
    - No part of the call stores anything the residual would go on to read. The fold deletes the
      whole expression, so a store is lost wherever in it the store was written — an argument, the
      receiver, or the computed key naming the method; see `_call_stores_nothing`.

    Trust is not evaluability, and this answers only the first. `trusted_intrinsic` says `unknownFn` is
    undisturbed — true, and no help, since no such built-in exists to fold. Whether a callee can
    actually be evaluated is the caller's question, answered by its own registry lookup before it asks
    this one; conflating the two is what let a registry entry exist with no matching trust rule.

    The first question is asked of the whole program, or at a point where *trust* is given: the
    `AnchoredTrust` of that point answers it, for the call and for every call nested in its
    arguments.
    """
    if trust is None:
        if not self._callee_is_trusted(node, receiver_type):
            return False
    elif not trust.callee(node, receiver_type):
        return False
    if not self._call_stores_nothing(node):
        return False
    return all(self._argument_is_admissible(arg, trust) for arg in node.arguments)
def call_names_an_unwritten_builtin(self, node, *, receiver_type=None)

call_is_foldable's callee-trust question with the reflection terms lifted: whether the built-in node spells is still that built-in, judged by attributed writes alone. A named callee is asked intrinsic_name_unwritten; a literal receiver's or known receiver type's method is asked prototype_name_unwritten; and a chained call is asked this same question of its inner link. The composition a consumer that holds an oracle clearance makes — the oracle answered the reflection terms at the anchor, so the name questions are what remain. A consumer without one takes call_is_foldable, whose trust leg is this plus those terms.

Expand source code Browse git
def call_names_an_unwritten_builtin(
    self,
    node: JsCallExpression,
    *,
    receiver_type: type | None = None,
) -> bool:
    """
    `call_is_foldable`'s callee-trust question with the reflection terms lifted: whether the
    built-in *node* spells is still that built-in, judged by attributed writes alone. A named
    callee is asked `intrinsic_name_unwritten`; a literal receiver's or known receiver type's
    method is asked `prototype_name_unwritten`; and a chained call is asked this same question
    of its inner link. The composition a consumer that holds an oracle clearance makes — the
    oracle answered the reflection terms at the anchor, so the name questions are what remain.
    A consumer without one takes `call_is_foldable`, whose trust leg is this plus those terms.
    """
    callee = strip_parens(node.callee)
    if isinstance(callee, JsIdentifier):
        return self.intrinsic_name_unwritten(callee) is not None
    if not isinstance(callee, JsMemberExpression):
        return False
    base = strip_parens(callee.object)
    if isinstance(base, JsIdentifier):
        return self.intrinsic_name_unwritten(base) is not None
    if isinstance(base, JsCallExpression):
        return self.call_names_an_unwritten_builtin(base)
    literal_type = _LITERAL_RECEIVER_TYPES.get(type(base))
    if literal_type is not None:
        return self.prototype_name_unwritten(literal_type)
    if receiver_type is None:
        return False
    return self.prototype_name_unwritten(receiver_type)
def member_read_getter_free(self, member, established=None)

Whether reading member runs no user getter, so it carries no observable effect: a getter-free read off a pristine value (a fresh literal or a pristine intrinsic root) or a trusted global data-property read off a syntactic global-object alias — always, since neither is nullish — or off a local single-assigned to the global object, which holds it only from its establishing definition onward, or off a local the model knows holds a function, of a key outside the accessor-poisoned set. The local cases qualify only when established confirms the single definition reaches the read, an ordering this effect model cannot decide on its own (see ReachingModel.value_preserved()).

Expand source code Browse git
def member_read_getter_free(
    self,
    member: JsMemberExpression,
    established: Callable[[Binding, JsMemberExpression], bool] | None = None,
) -> bool:
    """
    Whether reading *member* runs no user getter, so it carries no observable effect: a
    getter-free read off a pristine value (a fresh literal or a pristine intrinsic root) or a
    trusted global data-property read off a syntactic global-object alias — always, since
    neither is nullish — or off a local single-assigned to the global object, which holds it
    only from its establishing definition onward, or off a local the model knows holds a
    function, of a key outside the accessor-poisoned set. The local cases qualify only when
    *established* confirms the single definition reaches the read, an ordering this effect
    model cannot decide on its own (see
    `refinery.lib.scripts.js.analysis.reaching.ReachingModel.value_preserved`).
    """
    if self._getter_free_read(member):
        return True
    if established is None:
        return False
    binding = self._trusted_global_alias_read(member)
    if binding is not None and established(binding, member):
        return True
    return self._typed_local_function_read(member, established)
def getter_free_read_dropped_with(self, member, region)

member_read_getter_free asked of the tree an edit deleting the nodes of region leaves standing: a read inside the region is dropped together with them, so the opaque surfaces that tree still carries — those outside the region — are the only ones that could have tampered the chain the read consults, and a surface the edit removes never gets to. A read outside the region survives the edit and keeps the stock answer, which the trusted-global arm keeps in full: it rests on global_pristine, a whole-program precondition this question does not re-ask per region.

Expand source code Browse git
def getter_free_read_dropped_with(self, member: JsMemberExpression, region: list[Node]) -> bool:
    """
    `member_read_getter_free` asked of the tree an edit deleting the nodes of *region* leaves
    standing: a read inside the region is dropped together with them, so the opaque surfaces
    that tree still carries — those outside the region — are the only ones that could have
    tampered the chain the read consults, and a surface the edit removes never gets to. A
    read outside the region survives the edit and keeps the stock answer, which the
    trusted-global arm keeps in full: it rests on `global_pristine`, a whole-program
    precondition this question does not re-ask per region.
    """
    if not any(member is root or member.is_descendant_of(root) for root in region):
        return self.member_read_getter_free(member)
    return self._getter_free_read(member, region)
class EffectSummary (writes_global=False, throws=False, calls_unknown=False, mutates_returned_local=False, wraps_return=False, captured_writes=<factory>, written_bindings=<factory>, dead_zone_reads=<factory>)

The observable effects one call of a function may have, each field a conservative over-estimate. writes_global covers assignment to a global or to a property of an object reached through one; captured_writes names each binding owned by an enclosing function that a call may assign, a closure mutation visible after the call returns, and writes_captured says whether there is one; throws covers a throw, an operation that may throw on a value the analysis cannot prove safe, or a read of a name that is not certain to denote a binding (SemanticModel.read_may_throw()); calls_unknown covers invoking a callee that cannot be resolved and summarized. A summary with none of these set is is_pure. mutates_returned_local is held apart from those four: it records a write to a fresh local the function owns whose sole route to the caller is the value the call returns. Such a write is a real mutation baked into the returned value, so it blocks is_pure and is_expression_replaceable, but not is_effect_free_when_discarded — a call whose result is thrown away can never expose it — and not is_literal_replaceable, whose replacement is a fresh object at every site. wraps_return is separate: it does not bear on purity but records that a call to the function yields a wrapper (a promise from an async function, an iterator from a generator) rather than the value of its return expression, so the call cannot be replaced by that expression. written_bindings names, by identity, the outer bindings — captured locals and globals — a call may write where the write resolves to one, so a caller can ask which binding a call mutates rather than only whether it mutates some. A binding written but never read anywhere adds nothing, as no read can observe the change; likewise a coarse write with no resolvable binding (a dynamic-scope or globalThis.x = member write) sets writes_global but adds nothing here.

A write the summarized function makes to a binding it owns itself is in neither captured_writes nor written_bindings. A write a callee makes to such a binding is left out of both as well where every function written inside the summarized one runs only within the run that made it (EffectModel._keeps_its_closures): it is then part of the run under way, and no caller sees it once the call returns.

dead_zone_reads mirrors written_bindings for a read that throws only at some call sites: it names, by identity, the outer let/const/class bindings a call to this function may read (directly or through a transitive callee) before that binding is initialized, which is a ReferenceError the function raises iff the call lands in the binding's temporal dead zone. The flow-insensitive summary cannot collapse this to throws — that flag fires at every call, but this read throws only inside the dead zone and is safe after the declaration — so it is deferred per binding to the call site, which owns the ordering judgment. A binding the function itself owns is decided at the summary instead (see _scan): the call site cannot order it, since the read and its declaration both run inside the call, so the summary asks the dominance model whether every read of it runs past its declaration and a read that may land in the zone sets throws, the same ReferenceError the call would raise itself.

Four properties read these flags, and they are not a scale from strict to permissive — each answers a different question about a different rewrite, so a consumer picks by naming the rewrite it is about to perform rather than by how many flags a property excludes:

  • is_pure — may the call be deleted outright, result and all? Nothing may be lost, so every flag blocks.
  • is_effect_free_when_discarded — may the call be deleted when its result is already unused? A mutation confined to the returned value is then unreachable, so only that flag is forgiven.
  • is_literal_replaceable — may the call become a literal denoting its value? Throwing and unknown reads are reproduced by actually evaluating it, and mutates_returned_local is forgiven because a literal is a fresh object at every site. A rewrite that yields something other than a literal may not use this.
  • is_expression_replaceable — may the call become one expression lifted out of its body, with the rest of the body discarded? Everything the literal case needs, plus a refusal of mutates_returned_local, since a lifted expression yields no fresh object.
Expand source code Browse git
@dataclass
class EffectSummary:
    """
    The observable effects one call of a function may have, each field a conservative over-estimate.
    `writes_global` covers assignment to a global or to a property of an object reached through one;
    `captured_writes` names each binding owned by an enclosing function that a call may assign, a
    closure mutation visible after the call returns, and `writes_captured` says whether there is
    one; `throws` covers a `throw`, an operation that may throw on a value the analysis cannot prove
    safe, or a read of a name that is not certain to denote a binding
    (`SemanticModel.read_may_throw`); `calls_unknown` covers invoking a callee that cannot be
    resolved and summarized. A summary with none of these set is `is_pure`. `mutates_returned_local` is
    held apart from those four: it records a write to a fresh local the function owns whose sole route to
    the caller is the value the call returns. Such a write is a real mutation baked into the returned
    value, so it blocks `is_pure` and `is_expression_replaceable`, but not
    `is_effect_free_when_discarded` — a call whose result is thrown away can never expose it — and not
    `is_literal_replaceable`, whose replacement is a fresh object at every site. `wraps_return` is separate:
    it does not bear on purity but records that a call to the function yields a wrapper (a promise from
    an `async` function, an iterator from a generator) rather than the value of its return expression,
    so the call cannot be replaced by that expression. `written_bindings` names, by identity, the outer
    bindings — captured locals and globals — a call may write where the write resolves to one, so a
    caller can ask which binding a call mutates rather than only whether it mutates some. A binding
    written but never read anywhere adds nothing, as no read can observe the change; likewise a
    coarse write with no resolvable binding (a dynamic-scope or `globalThis.x =` member write) sets
    `writes_global` but adds nothing here.

    A write the summarized function makes to a binding it owns itself is in neither
    `captured_writes` nor `written_bindings`. A write a callee makes to such a binding is left out
    of both as well where every function written inside the summarized one runs only within the
    run that made it (`EffectModel._keeps_its_closures`): it is then part of the run under way, and
    no caller sees it once the call returns.

    `dead_zone_reads` mirrors `written_bindings` for a read that throws only at some call sites: it
    names, by identity, the outer `let`/`const`/`class` bindings a call to this function may read
    (directly or through a transitive callee) before that binding is initialized, which is a
    `ReferenceError` the function raises iff the call lands in the binding's temporal dead zone.
    The flow-insensitive summary cannot collapse this to `throws` — that flag fires at *every*
    call, but this read throws only inside the dead zone and is safe after the declaration — so it
    is deferred per binding to the call site, which owns the ordering judgment. A binding the
    function itself owns is decided at the summary instead (see `_scan`): the call site cannot
    order it, since the read and its declaration both run inside the call, so the summary asks the
    dominance model whether every read of it runs past its declaration and a read that may land in
    the zone sets `throws`, the same ReferenceError the call would raise itself.

    Four properties read these flags, and they are not a scale from strict to permissive — each answers a
    different question about a *different rewrite*, so a consumer picks by naming the rewrite it is about to
    perform rather than by how many flags a property excludes:

    - `is_pure` — may the call be deleted outright, result and all? Nothing may be lost, so every flag blocks.
    - `is_effect_free_when_discarded` — may the call be deleted when its result is already unused? A mutation
      confined to the returned value is then unreachable, so only that flag is forgiven.
    - `is_literal_replaceable` — may the call become a literal denoting its value? Throwing and unknown reads
      are reproduced by actually evaluating it, and `mutates_returned_local` is forgiven because a literal is a
      fresh object at every site. A rewrite that yields something other than a literal may not use this.
    - `is_expression_replaceable` — may the call become one expression lifted out of its body, with the rest of
      the body discarded? Everything the literal case needs, plus a refusal of `mutates_returned_local`, since
      a lifted expression yields no fresh object.
    """
    writes_global: bool = False
    throws: bool = False
    calls_unknown: bool = False
    mutates_returned_local: bool = False
    wraps_return: bool = False
    captured_writes: set[Binding] = field(default_factory=set)
    written_bindings: set[Binding] = field(default_factory=set)
    dead_zone_reads: set[Binding] = field(default_factory=set)

    @property
    def writes_captured(self) -> bool:
        """
        Whether a call may assign a binding an enclosing function owns, which outlives the call.
        """
        return bool(self.captured_writes)

    @property
    def is_pure(self) -> bool:
        """
        Whether a call to the summarized function produces no observable effect, so it carries no
        consequence the program can detect (termination aside) whether or not its result is used. A
        mutation the function confines to its returned value (`mutates_returned_local`) disqualifies it
        here, since a caller that uses the result observes that mutation; `is_effect_free_when_discarded`
        is the companion test for a call whose result is thrown away, which tolerates it.

        A `dead_zone_reads` throw is NOT reflected here: it fires only inside a binding's dead
        zone, not at every call, so it cannot set `throws`. A consumer that EVALUATES the call (a
        value fold, a HOF fold) needs no extra guard — the dead-zone read manifests as the
        evaluation throwing, which aborts the rewrite exactly as reading the binding would. A
        consumer that DELETES the call without evaluating it (the dead-store and orphan sweeps, via
        `call_clearable`) must gate on the call-site establishment of every `dead_zone_reads`
        binding; reading `is_pure` alone would drop the throw. A binding the function itself owns
        is the exception the summary decides itself: its dead zone runs inside the call, so a read
        that may land in it sets `throws` and blocks every property alike.
        """
        return not (
            self.writes_global
            or self.writes_captured
            or self.throws
            or self.calls_unknown
            or self.mutates_returned_local
        )

    @property
    def is_effect_free_when_discarded(self) -> bool:
        """
        Whether a call to the summarized function, its result discarded, produces no observable effect.
        Identical to `is_pure` except it tolerates `mutates_returned_local`: a write to a fresh local the
        function owns is observable only through the value the call returns, so once that value is thrown
        away the write can never be seen and the call is free to drop. Every other way such a local — or a
        closure over it — reaches the caller is a distinct effect that independently sets a blocking flag
        (a store to a global, a store to an enclosing capture, a leak into an unknown callee, a throw), so
        excluding only `mutates_returned_local` here stays sound.

        As with `is_pure`, a `dead_zone_reads` throw is not reflected here — the read raises
        regardless of whether the result is used — so a consumer that deletes the call on the
        strength of this property must additionally gate on call-site establishment of every
        `dead_zone_reads` binding.
        """
        return not (self.writes_global or self.writes_captured or self.throws or self.calls_unknown)

    @property
    def is_literal_replaceable(self) -> bool:
        """
        Whether a call to the summarized function may be replaced by a *literal* denoting its computed return
        value. This holds when the call writes no state visible after it returns — neither a global nor a
        captured binding — and returns its value directly rather than wrapped: an `async` function's call is a
        promise and a generator's is an iterator, neither equal to the return expression, so `wraps_return`
        disqualifies it. Unlike `is_pure`, a call that may throw or read unknown state still qualifies: an
        evaluator that actually executes the call to a value reproduces those, and only a *write* would be
        silently lost.

        `mutates_returned_local` is tolerated, and the name of this property is what licenses that. Such a
        mutation is baked into a container the call returns, so the substituted value must be a distinct
        object per call — which a literal is, because `value_to_node` builds a new array or object literal at
        every site it fills. A replacement that is *not* a literal has no such guarantee and must not consult
        this property; see the family note on `EffectSummary`.
        """
        return not (
            self.writes_global
            or self.writes_captured
            or self.wraps_return
        )

    @property
    def is_expression_replaceable(self) -> bool:
        """
        Whether a call to the summarized function may be replaced by a single expression lifted out of its
        body, with everything else the body would have done discarded.

        Everything `is_literal_replaceable` requires is required here, for the same reasons: a write to a
        global or a capture would be lost, and a wrapped return is not the return expression. Throwing and
        unknown reads are tolerated identically — the lifted expression sits at the call site and still
        performs them, so they are reproduced rather than dropped, which is why the discard question is *not*
        the right one to ask even though statements are being discarded.

        What this additionally forbids is `mutates_returned_local`. The literal case tolerates it because
        `value_to_node` builds a new array or object at every site it fills, so the distinct container the
        mutation is baked into survives. A lifted expression is spliced from the body and names whatever the
        body named, guaranteeing nothing about identity, so a mutated container must not travel this way.
        """
        return self.is_literal_replaceable and not self.mutates_returned_local

    def absorb(self, other: EffectSummary):
        """
        Union *other*'s effects into this summary, used to fold a callee's effects into its caller.
        """
        self.writes_global = self.writes_global or other.writes_global
        self.captured_writes |= other.captured_writes
        self.throws = self.throws or other.throws
        self.calls_unknown = self.calls_unknown or other.calls_unknown
        self.mutates_returned_local = self.mutates_returned_local or other.mutates_returned_local
        self.written_bindings |= other.written_bindings
        self.dead_zone_reads |= other.dead_zone_reads

Instance variables

var captured_writes

The type of the None singleton.

var written_bindings

The type of the None singleton.

var dead_zone_reads

The type of the None singleton.

var writes_global

The type of the None singleton.

var throws

The type of the None singleton.

var calls_unknown

The type of the None singleton.

var mutates_returned_local

The type of the None singleton.

var wraps_return

The type of the None singleton.

var writes_captured

Whether a call may assign a binding an enclosing function owns, which outlives the call.

Expand source code Browse git
@property
def writes_captured(self) -> bool:
    """
    Whether a call may assign a binding an enclosing function owns, which outlives the call.
    """
    return bool(self.captured_writes)
var is_pure

Whether a call to the summarized function produces no observable effect, so it carries no consequence the program can detect (termination aside) whether or not its result is used. A mutation the function confines to its returned value (mutates_returned_local) disqualifies it here, since a caller that uses the result observes that mutation; is_effect_free_when_discarded is the companion test for a call whose result is thrown away, which tolerates it.

A dead_zone_reads throw is NOT reflected here: it fires only inside a binding's dead zone, not at every call, so it cannot set throws. A consumer that EVALUATES the call (a value fold, a HOF fold) needs no extra guard — the dead-zone read manifests as the evaluation throwing, which aborts the rewrite exactly as reading the binding would. A consumer that DELETES the call without evaluating it (the dead-store and orphan sweeps, via call_clearable) must gate on the call-site establishment of every dead_zone_reads binding; reading is_pure alone would drop the throw. A binding the function itself owns is the exception the summary decides itself: its dead zone runs inside the call, so a read that may land in it sets throws and blocks every property alike.

Expand source code Browse git
@property
def is_pure(self) -> bool:
    """
    Whether a call to the summarized function produces no observable effect, so it carries no
    consequence the program can detect (termination aside) whether or not its result is used. A
    mutation the function confines to its returned value (`mutates_returned_local`) disqualifies it
    here, since a caller that uses the result observes that mutation; `is_effect_free_when_discarded`
    is the companion test for a call whose result is thrown away, which tolerates it.

    A `dead_zone_reads` throw is NOT reflected here: it fires only inside a binding's dead
    zone, not at every call, so it cannot set `throws`. A consumer that EVALUATES the call (a
    value fold, a HOF fold) needs no extra guard — the dead-zone read manifests as the
    evaluation throwing, which aborts the rewrite exactly as reading the binding would. A
    consumer that DELETES the call without evaluating it (the dead-store and orphan sweeps, via
    `call_clearable`) must gate on the call-site establishment of every `dead_zone_reads`
    binding; reading `is_pure` alone would drop the throw. A binding the function itself owns
    is the exception the summary decides itself: its dead zone runs inside the call, so a read
    that may land in it sets `throws` and blocks every property alike.
    """
    return not (
        self.writes_global
        or self.writes_captured
        or self.throws
        or self.calls_unknown
        or self.mutates_returned_local
    )
var is_effect_free_when_discarded

Whether a call to the summarized function, its result discarded, produces no observable effect. Identical to is_pure except it tolerates mutates_returned_local: a write to a fresh local the function owns is observable only through the value the call returns, so once that value is thrown away the write can never be seen and the call is free to drop. Every other way such a local — or a closure over it — reaches the caller is a distinct effect that independently sets a blocking flag (a store to a global, a store to an enclosing capture, a leak into an unknown callee, a throw), so excluding only mutates_returned_local here stays sound.

As with is_pure, a dead_zone_reads throw is not reflected here — the read raises regardless of whether the result is used — so a consumer that deletes the call on the strength of this property must additionally gate on call-site establishment of every dead_zone_reads binding.

Expand source code Browse git
@property
def is_effect_free_when_discarded(self) -> bool:
    """
    Whether a call to the summarized function, its result discarded, produces no observable effect.
    Identical to `is_pure` except it tolerates `mutates_returned_local`: a write to a fresh local the
    function owns is observable only through the value the call returns, so once that value is thrown
    away the write can never be seen and the call is free to drop. Every other way such a local — or a
    closure over it — reaches the caller is a distinct effect that independently sets a blocking flag
    (a store to a global, a store to an enclosing capture, a leak into an unknown callee, a throw), so
    excluding only `mutates_returned_local` here stays sound.

    As with `is_pure`, a `dead_zone_reads` throw is not reflected here — the read raises
    regardless of whether the result is used — so a consumer that deletes the call on the
    strength of this property must additionally gate on call-site establishment of every
    `dead_zone_reads` binding.
    """
    return not (self.writes_global or self.writes_captured or self.throws or self.calls_unknown)
var is_literal_replaceable

Whether a call to the summarized function may be replaced by a literal denoting its computed return value. This holds when the call writes no state visible after it returns — neither a global nor a captured binding — and returns its value directly rather than wrapped: an async function's call is a promise and a generator's is an iterator, neither equal to the return expression, so wraps_return disqualifies it. Unlike is_pure, a call that may throw or read unknown state still qualifies: an evaluator that actually executes the call to a value reproduces those, and only a write would be silently lost.

mutates_returned_local is tolerated, and the name of this property is what licenses that. Such a mutation is baked into a container the call returns, so the substituted value must be a distinct object per call — which a literal is, because value_to_node builds a new array or object literal at every site it fills. A replacement that is not a literal has no such guarantee and must not consult this property; see the family note on EffectSummary.

Expand source code Browse git
@property
def is_literal_replaceable(self) -> bool:
    """
    Whether a call to the summarized function may be replaced by a *literal* denoting its computed return
    value. This holds when the call writes no state visible after it returns — neither a global nor a
    captured binding — and returns its value directly rather than wrapped: an `async` function's call is a
    promise and a generator's is an iterator, neither equal to the return expression, so `wraps_return`
    disqualifies it. Unlike `is_pure`, a call that may throw or read unknown state still qualifies: an
    evaluator that actually executes the call to a value reproduces those, and only a *write* would be
    silently lost.

    `mutates_returned_local` is tolerated, and the name of this property is what licenses that. Such a
    mutation is baked into a container the call returns, so the substituted value must be a distinct
    object per call — which a literal is, because `value_to_node` builds a new array or object literal at
    every site it fills. A replacement that is *not* a literal has no such guarantee and must not consult
    this property; see the family note on `EffectSummary`.
    """
    return not (
        self.writes_global
        or self.writes_captured
        or self.wraps_return
    )
var is_expression_replaceable

Whether a call to the summarized function may be replaced by a single expression lifted out of its body, with everything else the body would have done discarded.

Everything is_literal_replaceable requires is required here, for the same reasons: a write to a global or a capture would be lost, and a wrapped return is not the return expression. Throwing and unknown reads are tolerated identically — the lifted expression sits at the call site and still performs them, so they are reproduced rather than dropped, which is why the discard question is not the right one to ask even though statements are being discarded.

What this additionally forbids is mutates_returned_local. The literal case tolerates it because value_to_node builds a new array or object at every site it fills, so the distinct container the mutation is baked into survives. A lifted expression is spliced from the body and names whatever the body named, guaranteeing nothing about identity, so a mutated container must not travel this way.

Expand source code Browse git
@property
def is_expression_replaceable(self) -> bool:
    """
    Whether a call to the summarized function may be replaced by a single expression lifted out of its
    body, with everything else the body would have done discarded.

    Everything `is_literal_replaceable` requires is required here, for the same reasons: a write to a
    global or a capture would be lost, and a wrapped return is not the return expression. Throwing and
    unknown reads are tolerated identically — the lifted expression sits at the call site and still
    performs them, so they are reproduced rather than dropped, which is why the discard question is *not*
    the right one to ask even though statements are being discarded.

    What this additionally forbids is `mutates_returned_local`. The literal case tolerates it because
    `value_to_node` builds a new array or object at every site it fills, so the distinct container the
    mutation is baked into survives. A lifted expression is spliced from the body and names whatever the
    body named, guaranteeing nothing about identity, so a mutated container must not travel this way.
    """
    return self.is_literal_replaceable and not self.mutates_returned_local

Methods

def absorb(self, other)

Union other's effects into this summary, used to fold a callee's effects into its caller.

Expand source code Browse git
def absorb(self, other: EffectSummary):
    """
    Union *other*'s effects into this summary, used to fold a callee's effects into its caller.
    """
    self.writes_global = self.writes_global or other.writes_global
    self.captured_writes |= other.captured_writes
    self.throws = self.throws or other.throws
    self.calls_unknown = self.calls_unknown or other.calls_unknown
    self.mutates_returned_local = self.mutates_returned_local or other.mutates_returned_local
    self.written_bindings |= other.written_bindings
    self.dead_zone_reads |= other.dead_zone_reads
class LivenessModel (model, control_flow=None)

Flow-sensitive live-variable sets and dead-store verdicts for one script, built over a SemanticModel. Query a control-flow node's live sets with live_in and live_out, find the node standing for an AST element with node_of, and ask whether a write is dead with is_dead_store. Build through build_liveness().

Expand source code Browse git
class LivenessModel:
    """
    Flow-sensitive live-variable sets and dead-store verdicts for one script, built over a
    `refinery.lib.scripts.js.analysis.model.SemanticModel`. Query a control-flow node's live sets
    with `live_in` and `live_out`, find the node standing for an AST element with `node_of`, and ask
    whether a write is dead with `is_dead_store`. Build through `build_liveness`.
    """

    def __init__(self, model: SemanticModel, control_flow: ControlFlowModel | None = None):
        self.model = model
        self._flow = control_flow if control_flow is not None else build_control_flow_model(model.root)
        self._live_in: dict[int, frozenset[Binding]] = {}
        self._live_out: dict[int, frozenset[Binding]] = {}
        self._pseudo_local_owners: dict[Binding, Node] = {}
        self._index_pseudo_locals()
        for graph in self._flow.graphs.values():
            self._compute_graph(graph)

    def live_in(self, node: CfgNode) -> frozenset[Binding]:
        """
        The bindings live on entry to *node* — those that may be read before being overwritten on some
        path that begins at *node*.
        """
        return self._live_in.get(id(node), frozenset())

    def live_out(self, node: CfgNode) -> frozenset[Binding]:
        """
        The bindings live on exit from *node* — those that may be read on some path that leaves it,
        including the path taken if *node* throws.
        """
        return self._live_out.get(id(node), frozenset())

    def live_around(self, element: Node) -> frozenset[Binding] | None:
        """
        The bindings live on entry to or on exit from the control-flow node that evaluates
        *element*, climbing out of any expression it is nested in, or `None` when no graph places
        it. The graph does not order the parts of one node, so a binding live here may be read
        after *element* is evaluated.
        """
        located = self._flow.locate(element)
        if located is None:
            return None
        node = located[1]
        return self.live_in(node) | self.live_out(node)

    def node_of(self, element: Node) -> CfgNode | None:
        """
        The control-flow node standing for *element* in whichever function graph owns it, or `None` if
        *element* is not a node the graphs represent.
        """
        return self._flow.node_of(element)

    def is_dead_store(self, write: JsIdentifier) -> bool:
        """
        Whether the value written to a binding at *write* is never read on any execution. Only an
        unconditional store to an uncaptured function-local `var`/`let` qualifies; a read, a compound or
        conditional write, a captured or outer binding, or a store whose value may still be read all
        return `False`, the conservative verdict. The verdict concerns the stored *value* alone: a
        caller removing the store must still preserve any side effect of the expression producing it.

        No store is reported while a `with` or direct `eval` lexically inside the owning function could
        read the local by name without a reference the model sees. A reflective surface elsewhere in the
        program runs in the global scope and cannot reach a local, so it does not suppress the report.
        """
        located = self._flow.locate(write)
        if located is None:
            return False
        graph, node = located
        owner_scope = self.model.function_scope(graph.owner)
        binding, construct = self._store_target(write)
        if binding is None or construct is None:
            return False
        if not self._trackable(binding, owner_scope):
            return False
        if self.model.reflection_can_reach(binding):
            return False
        if binding in self.live_out(node):
            return False
        return self._unobserved_within(graph, node, write, binding, construct)

    def dead_stores(self) -> list[JsIdentifier]:
        """
        Every write identifier in the script whose stored value is dead, in source order.
        """
        result: list[JsIdentifier] = []
        for node in self.model.root.walk_in_order():
            if isinstance(node, JsIdentifier) and self._is_candidate_write(node):
                if self.is_dead_store(node):
                    result.append(node)
        return result

    def is_dead_on_entry(self, binding: Binding, function: Node) -> bool:
        """
        Whether *function* writes *binding* before reading it on every path, so no value carried into the
        call — from a previous invocation or from load — is ever observed. Answered from the liveness at
        the function's control-flow entry; a binding not tracked in *function* returns `False`.
        """
        graph = self._flow.graph_of(function)
        if graph is None:
            return False
        if not self._analysable(graph, binding, self.model.function_scope(graph.owner)):
            return False
        return binding not in self.live_in(graph.entry)

    def pseudo_local_owner(self, binding: Binding) -> Node | None:
        """
        The function whose graph tracks *binding*, a script-scope `var`, as one of its locals: the
        one function whose body holds every read of it and every site that stores into it
        (`refinery.lib.scripts.js.analysis.model.Binding.store_sites`), none at script scope, in the
        parameter list or in a function nested below it. `None` for every other binding.
        """
        return self._pseudo_local_owners.get(binding)

    def _index_pseudo_locals(self):
        """
        Record, for each script-scope `var` binding that behaves as a single function's local, that
        function, so the dataflow can track it inside it: the owner `_sole_owning_function` finds.
        """
        for binding in self.model.root_scope.bindings.values():
            if binding.kind is not BindingKind.VAR:
                continue
            function = self._sole_owning_function(binding)
            if function is not None:
                self._pseudo_local_owners[binding] = function

    def _compute_graph(self, graph: ControlFlowGraph):
        owner_scope = self.model.function_scope(graph.owner)

        def node_sets(_graph: ControlFlowGraph, node: CfgNode) -> tuple[set[Binding], set[Binding]]:
            return self._node_sets(_graph, node, owner_scope)

        live_in, live_out = solve_liveness(graph, node_sets)
        self._live_in.update(live_in)
        self._live_out.update(live_out)

    def _node_sets(
        self, graph: ControlFlowGraph, node: CfgNode, owner_scope: Scope | None,
    ) -> tuple[set[Binding], set[Binding]]:
        use: set[Binding] = set()
        kill: set[Binding] = set()
        if node.element is None:
            return use, kill
        for ident in self._shallow_idents(graph, node.element):
            declared = self.model.binding_of(ident)
            if declared is not None:
                if self._trackable(declared, owner_scope) and self._declarator_has_init(ident):
                    kill.add(declared)
                continue
            if not is_use_position(ident):
                continue
            binding = self.model.resolve(ident)
            if binding is None or not self._analysable(graph, binding, owner_scope):
                continue
            role = reference_role(ident)
            if role is not Role.WRITE:
                use.add(binding)
            elif self._is_assignment_kill(ident, node.element):
                kill.add(binding)
        return use, kill

    def _unobserved_within(
        self,
        graph: ControlFlowGraph,
        node: CfgNode,
        write: JsIdentifier,
        binding: Binding,
        construct: Node,
    ) -> bool:
        """
        Whether no reference to *binding* other than *write* within *node* can observe *write*'s value.
        A read nested in *construct* (the assignment or declarator performing the write) consumes the
        prior value, so it is ignored; any other reference — a later read or a second write in the same
        node — is treated conservatively as observing the store, since the graph does not order
        the parts of one node.
        """
        assert node.element is not None
        for ident in self._shallow_idents(graph, node.element):
            if ident is write:
                continue
            if self._reference_binding(ident) is not binding:
                continue
            if self._is_read(ident) and ident.is_descendant_of(construct):
                continue
            return False
        return True

    def _shallow_idents(self, graph: ControlFlowGraph, element: Node) -> Iterator[JsIdentifier]:
        """
        Yield the identifiers belonging to *element*'s own control-flow node: *element* itself and
        those in its subtree that are not in a part evaluated at a time of its own, as the body of a
        nested function or the initializer of a field is, or in a descendant that is itself a
        separate control-flow node (whose identifiers are accounted there). This keeps a loop or
        branch head from double-counting the body that follows it.
        """
        stack: list[Node] = [element]
        while stack:
            current = stack.pop()
            if current is not element and graph.node_of(current) is not None:
                continue
            if isinstance(current, JsIdentifier):
                yield current
            stack.extend(
                child for child in current.children() if not evaluated_later(current, child)
            )

    def _store_target(self, write: JsIdentifier) -> tuple[Binding | None, Node | None]:
        """
        The binding *write* stores into and the construct whose completion performs the store, or
        `(None, None)` if *write* is not an unconditional value store: a `var`/`let`/`const` declarator
        with an initializer, or the target of a plain `=` assignment.
        """
        declared = self.model.binding_of(write)
        if declared is not None:
            if not self._declarator_has_init(write):
                return None, None
            return declared, self._enclosing_declarator(write)
        if not is_use_position(write):
            return None, None
        binding = self.model.resolve(write)
        if binding is None or reference_role(write) is not Role.WRITE:
            return None, None
        governor = self._governor(write)
        if not isinstance(governor, JsAssignmentExpression) or governor.operator != '=':
            return None, None
        return binding, governor

    def _is_candidate_write(self, ident: JsIdentifier) -> bool:
        if self.model.binding_of(ident) is not None:
            return self._declarator_has_init(ident)
        if not is_use_position(ident):
            return False
        return reference_role(ident) is Role.WRITE

    def _is_assignment_kill(self, ident: JsIdentifier, element: Node) -> bool:
        governor = self._governor(ident)
        if not isinstance(governor, JsAssignmentExpression) or governor.operator != '=':
            return False
        return evaluated_whenever_completed(ident, element)

    def _is_read(self, ident: JsIdentifier) -> bool:
        if not self.model.is_reference(ident):
            return False
        return reference_role(ident) is not Role.WRITE

    def _reference_binding(self, ident: JsIdentifier) -> Binding | None:
        declared = self.model.binding_of(ident)
        if declared is not None:
            return declared
        if not is_use_position(ident):
            return None
        return self.model.resolve(ident)

    def _declarator_has_init(self, ident: JsIdentifier) -> bool:
        declarator = self._enclosing_declarator(ident)
        return declarator is not None and declarator.init is not None

    def _enclosing_declarator(self, ident: JsIdentifier) -> JsVariableDeclarator | None:
        governor, target = _governing_target(ident)
        if isinstance(governor, JsVariableDeclarator) and governor.id is target:
            return governor
        return None

    def _governor(self, ident: JsIdentifier) -> Node | None:
        """
        The construct that governs the binding target *ident* sits in — the assignment, declarator, or
        loop head reached by climbing out through any destructuring containers and parentheses around
        it, or `None` past the top of the tree.
        """
        governor, _ = _governing_target(ident)
        return governor

    def _trackable(self, binding: Binding, owner_scope: Scope | None) -> bool:
        """
        Whether *binding* is a local of the function *owner_scope* is the body scope of, so that the
        graph built for that function sees every store to it.

        The parameters of a function whose parameter list holds an expression are bound in a scope
        of their own standing outside the body's, so their variable scope is that one rather than
        the body's; `Scope.closure_home` says the two belong to one call, and is what this asks.
        """
        return (
            binding.kind in _CANDIDATE_KINDS
            and not binding.captured
            and owner_scope is not None
            and owner_scope.kind is ScopeKind.FUNCTION
            and binding.scope.closure_home is owner_scope
        )

    def _analysable(
        self, graph: ControlFlowGraph, binding: Binding, owner_scope: Scope | None,
    ) -> bool:
        """
        Whether *binding* is tracked in *graph*: either an uncaptured function-local of the graph's
        own function (the strict store candidate) or a script-scope `var` that function owns
        (`LivenessModel.pseudo_local_owner`). The second case feeds only the live sets; it never
        reaches `LivenessModel.is_dead_store`, which keeps the strict candidacy.
        """
        if self._trackable(binding, owner_scope):
            return True
        return self._pseudo_local_owners.get(binding) is graph.owner

    def _sole_owning_function(self, binding: Binding) -> Node | None:
        """
        The one function whose body lexically contains every read of *binding* and every site that
        stores into it, or `None` when they span more than one function, include one at script
        scope, or do not exist. A declaration that stores where it runs is such a site, so a
        declarator with an initializer or a loop head at script scope rejects the binding. A site
        inside a function nested below the candidate counts as a separate owner, so a binding
        captured by such a nested closure is rejected, and so is one the candidate's parameter list
        reads or writes: parameter expressions run in a scope of their own, which a `var` the body
        declares does not reach. A site in the initializer of a field rejects the binding as well:
        that initializer runs at each construction, a run of its own that holds no declaration.
        """
        owner: Node | None = None
        body: Node | None = None
        for site in (*binding.reads, *binding.store_sites):
            function = enclosing_run(site)
            if not isinstance(function, FUNCTION_NODES):
                return None
            if owner is None:
                owner = function
                body = getattr(function, 'body', None)
            elif function is not owner:
                return None
            if body is None or not site.is_descendant_of(body):
                return None
        return owner

Methods

def live_in(self, node)

The bindings live on entry to node — those that may be read before being overwritten on some path that begins at node.

Expand source code Browse git
def live_in(self, node: CfgNode) -> frozenset[Binding]:
    """
    The bindings live on entry to *node* — those that may be read before being overwritten on some
    path that begins at *node*.
    """
    return self._live_in.get(id(node), frozenset())
def live_out(self, node)

The bindings live on exit from node — those that may be read on some path that leaves it, including the path taken if node throws.

Expand source code Browse git
def live_out(self, node: CfgNode) -> frozenset[Binding]:
    """
    The bindings live on exit from *node* — those that may be read on some path that leaves it,
    including the path taken if *node* throws.
    """
    return self._live_out.get(id(node), frozenset())
def live_around(self, element)

The bindings live on entry to or on exit from the control-flow node that evaluates element, climbing out of any expression it is nested in, or None when no graph places it. The graph does not order the parts of one node, so a binding live here may be read after element is evaluated.

Expand source code Browse git
def live_around(self, element: Node) -> frozenset[Binding] | None:
    """
    The bindings live on entry to or on exit from the control-flow node that evaluates
    *element*, climbing out of any expression it is nested in, or `None` when no graph places
    it. The graph does not order the parts of one node, so a binding live here may be read
    after *element* is evaluated.
    """
    located = self._flow.locate(element)
    if located is None:
        return None
    node = located[1]
    return self.live_in(node) | self.live_out(node)
def node_of(self, element)

The control-flow node standing for element in whichever function graph owns it, or None if element is not a node the graphs represent.

Expand source code Browse git
def node_of(self, element: Node) -> CfgNode | None:
    """
    The control-flow node standing for *element* in whichever function graph owns it, or `None` if
    *element* is not a node the graphs represent.
    """
    return self._flow.node_of(element)
def is_dead_store(self, write)

Whether the value written to a binding at write is never read on any execution. Only an unconditional store to an uncaptured function-local var/let qualifies; a read, a compound or conditional write, a captured or outer binding, or a store whose value may still be read all return False, the conservative verdict. The verdict concerns the stored value alone: a caller removing the store must still preserve any side effect of the expression producing it.

No store is reported while a with or direct eval lexically inside the owning function could read the local by name without a reference the model sees. A reflective surface elsewhere in the program runs in the global scope and cannot reach a local, so it does not suppress the report.

Expand source code Browse git
def is_dead_store(self, write: JsIdentifier) -> bool:
    """
    Whether the value written to a binding at *write* is never read on any execution. Only an
    unconditional store to an uncaptured function-local `var`/`let` qualifies; a read, a compound or
    conditional write, a captured or outer binding, or a store whose value may still be read all
    return `False`, the conservative verdict. The verdict concerns the stored *value* alone: a
    caller removing the store must still preserve any side effect of the expression producing it.

    No store is reported while a `with` or direct `eval` lexically inside the owning function could
    read the local by name without a reference the model sees. A reflective surface elsewhere in the
    program runs in the global scope and cannot reach a local, so it does not suppress the report.
    """
    located = self._flow.locate(write)
    if located is None:
        return False
    graph, node = located
    owner_scope = self.model.function_scope(graph.owner)
    binding, construct = self._store_target(write)
    if binding is None or construct is None:
        return False
    if not self._trackable(binding, owner_scope):
        return False
    if self.model.reflection_can_reach(binding):
        return False
    if binding in self.live_out(node):
        return False
    return self._unobserved_within(graph, node, write, binding, construct)
def dead_stores(self)

Every write identifier in the script whose stored value is dead, in source order.

Expand source code Browse git
def dead_stores(self) -> list[JsIdentifier]:
    """
    Every write identifier in the script whose stored value is dead, in source order.
    """
    result: list[JsIdentifier] = []
    for node in self.model.root.walk_in_order():
        if isinstance(node, JsIdentifier) and self._is_candidate_write(node):
            if self.is_dead_store(node):
                result.append(node)
    return result
def is_dead_on_entry(self, binding, function)

Whether function writes binding before reading it on every path, so no value carried into the call — from a previous invocation or from load — is ever observed. Answered from the liveness at the function's control-flow entry; a binding not tracked in function returns False.

Expand source code Browse git
def is_dead_on_entry(self, binding: Binding, function: Node) -> bool:
    """
    Whether *function* writes *binding* before reading it on every path, so no value carried into the
    call — from a previous invocation or from load — is ever observed. Answered from the liveness at
    the function's control-flow entry; a binding not tracked in *function* returns `False`.
    """
    graph = self._flow.graph_of(function)
    if graph is None:
        return False
    if not self._analysable(graph, binding, self.model.function_scope(graph.owner)):
        return False
    return binding not in self.live_in(graph.entry)
def pseudo_local_owner(self, binding)

The function whose graph tracks binding, a script-scope var, as one of its locals: the one function whose body holds every read of it and every site that stores into it (Binding.store_sites), none at script scope, in the parameter list or in a function nested below it. None for every other binding.

Expand source code Browse git
def pseudo_local_owner(self, binding: Binding) -> Node | None:
    """
    The function whose graph tracks *binding*, a script-scope `var`, as one of its locals: the
    one function whose body holds every read of it and every site that stores into it
    (`refinery.lib.scripts.js.analysis.model.Binding.store_sites`), none at script scope, in the
    parameter list or in a function nested below it. `None` for every other binding.
    """
    return self._pseudo_local_owners.get(binding)
class Role (*args, **kwds)

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

Color.RED

  • value lookup:

Color(1)

  • name lookup:

Color['RED']

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Expand source code Browse git
class Role(enum.Enum):
    READ      = 'read'        # noqa
    WRITE     = 'write'       # noqa
    READWRITE = 'readwrite'   # noqa

Ancestors

  • enum.Enum

Class variables

var READ

The type of the None singleton.

var WRITE

The type of the None singleton.

var READWRITE

The type of the None singleton.

class Scope (kind, node, parent=None, children=<factory>, bindings=<factory>, is_dynamic=False, function_body=None)

A lexical scope. node is the AST node that introduces it (the script, a function, a block, a catch clause, a class, or a with). is_dynamic marks a scope whose bindings cannot be resolved statically: a with body, whose object supplies them at run time, and a parameter or catch scope declared by a pattern the parser could not read, which spells names this model cannot see (pattern_binds_unread_names). A name that would resolve across either boundary resolves to nothing instead, since the binding it denotes may be one that is not there.

A direct eval is not marked here even though it too can inject a name. It would have to mark the whole enclosing function, which would make every name in a function containing one unresolvable, where what an eval actually does is narrower and is answered by the two queries written for it: local_reachable_by_direct_eval for a binding that already exists, and free_name_reachable_by_direct_eval for one the eval may have declared.

Expand source code Browse git
@dataclass(eq=False)
class Scope:
    """
    A lexical scope. `node` is the AST node that introduces it (the script, a function, a block, a
    catch clause, a class, or a `with`). `is_dynamic` marks a scope whose bindings cannot be
    resolved statically: a `with` body, whose object supplies them at run time, and a parameter or
    catch scope declared by a pattern the parser could not read, which spells names this model
    cannot see (`pattern_binds_unread_names`). A name that would resolve across either boundary
    resolves to nothing instead, since the binding it denotes may be one that is not there.

    A direct `eval` is not marked here even though it too can inject a name. It would have to mark the
    whole enclosing function, which would make every name in a function containing one unresolvable,
    where what an `eval` actually does is narrower and is answered by the two queries written for it:
    `local_reachable_by_direct_eval` for a binding that already exists, and
    `free_name_reachable_by_direct_eval` for one the `eval` may have declared.
    """
    kind: ScopeKind
    node: Node
    parent: Scope | None = None
    children: list[Scope] = field(default_factory=list)
    bindings: dict[str, Binding] = field(default_factory=dict)
    is_dynamic: bool = False
    #: For one of the two scopes a function introduces around its body - the one holding its own
    #: name and the one holding its parameters - the scope holding that body. It is what says the
    #: three are one call rather than three, which `closure_home` reads and nothing else does.
    function_body: Scope | None = None

    @property
    def is_var_scope(self) -> bool:
        """
        Whether this scope is the target of `var`/function-declaration hoisting: a function body, a
        class static block, or the script itself.
        """
        return (
            self.kind is ScopeKind.FUNCTION
            or self.kind is ScopeKind.SCRIPT
            or self.kind is ScopeKind.STATIC_BLOCK
            or self.kind is ScopeKind.PARAMS
        )

    @property
    def var_scope(self) -> Scope | None:
        """
        The function or script scope that governs `var`/function-declaration hoisting for this scope:
        this scope itself when it is already a var-scope, otherwise the nearest enclosing one (the
        boundary a closure crosses).
        """
        scope: Scope | None = self
        while scope is not None and not scope.is_var_scope:
            scope = scope.parent
        return scope

    @property
    def closure_home(self) -> Scope | None:
        """
        The scope that decides whether a reference made from this one crosses a closure boundary: a
        name read from a scope with a different one is read by a function other than the one that
        declares it, and is a capture.

        This is the variable scope for every scope but the two a function introduces around its
        body. A parameter default and the body it belongs to are run by one call and share every
        binding either of them makes, and so does the name a function expression answers to inside
        itself, so no closure boundary runs between the three: all of them answer the body's scope.
        The initializer of a field is run by each construction and not by the code around its class,
        so the scope it is evaluated in (`ScopeKind.FIELD`) is a closure home of its own, although
        nothing can be declared in it.
        """
        if self.function_body is not None:
            return self.function_body
        home: Scope | None = self
        while home is not None and not home.is_var_scope and home.kind is not ScopeKind.FIELD:
            home = home.parent
        if home is not None and home.function_body is not None:
            return home.function_body
        return home

    def contains(self, other: Scope, *, strict: bool = False) -> bool:
        """
        Whether this scope lexically contains *other*: *other* itself or any scope nested below it.
        With *strict*, the reflexive case is excluded, so only a scope nested strictly below this one
        qualifies — the shape of the shadowing test in `SemanticModel.is_shadowed`.
        """
        cursor: Scope | None = other.parent if strict else other
        while cursor is not None:
            if cursor is self:
                return True
            cursor = cursor.parent
        return False

Instance variables

var kind

The type of the None singleton.

var node

The type of the None singleton.

var children

The type of the None singleton.

var bindings

The type of the None singleton.

var parent

The type of the None singleton.

var is_dynamic

The type of the None singleton.

var function_body

For one of the two scopes a function introduces around its body - the one holding its own name and the one holding its parameters - the scope holding that body. It is what says the three are one call rather than three, which closure_home reads and nothing else does.

var is_var_scope

Whether this scope is the target of var/function-declaration hoisting: a function body, a class static block, or the script itself.

Expand source code Browse git
@property
def is_var_scope(self) -> bool:
    """
    Whether this scope is the target of `var`/function-declaration hoisting: a function body, a
    class static block, or the script itself.
    """
    return (
        self.kind is ScopeKind.FUNCTION
        or self.kind is ScopeKind.SCRIPT
        or self.kind is ScopeKind.STATIC_BLOCK
        or self.kind is ScopeKind.PARAMS
    )
var var_scope

The function or script scope that governs var/function-declaration hoisting for this scope: this scope itself when it is already a var-scope, otherwise the nearest enclosing one (the boundary a closure crosses).

Expand source code Browse git
@property
def var_scope(self) -> Scope | None:
    """
    The function or script scope that governs `var`/function-declaration hoisting for this scope:
    this scope itself when it is already a var-scope, otherwise the nearest enclosing one (the
    boundary a closure crosses).
    """
    scope: Scope | None = self
    while scope is not None and not scope.is_var_scope:
        scope = scope.parent
    return scope
var closure_home

The scope that decides whether a reference made from this one crosses a closure boundary: a name read from a scope with a different one is read by a function other than the one that declares it, and is a capture.

This is the variable scope for every scope but the two a function introduces around its body. A parameter default and the body it belongs to are run by one call and share every binding either of them makes, and so does the name a function expression answers to inside itself, so no closure boundary runs between the three: all of them answer the body's scope. The initializer of a field is run by each construction and not by the code around its class, so the scope it is evaluated in (ScopeKind.FIELD) is a closure home of its own, although nothing can be declared in it.

Expand source code Browse git
@property
def closure_home(self) -> Scope | None:
    """
    The scope that decides whether a reference made from this one crosses a closure boundary: a
    name read from a scope with a different one is read by a function other than the one that
    declares it, and is a capture.

    This is the variable scope for every scope but the two a function introduces around its
    body. A parameter default and the body it belongs to are run by one call and share every
    binding either of them makes, and so does the name a function expression answers to inside
    itself, so no closure boundary runs between the three: all of them answer the body's scope.
    The initializer of a field is run by each construction and not by the code around its class,
    so the scope it is evaluated in (`ScopeKind.FIELD`) is a closure home of its own, although
    nothing can be declared in it.
    """
    if self.function_body is not None:
        return self.function_body
    home: Scope | None = self
    while home is not None and not home.is_var_scope and home.kind is not ScopeKind.FIELD:
        home = home.parent
    if home is not None and home.function_body is not None:
        return home.function_body
    return home

Methods

def contains(self, other, *, strict=False)

Whether this scope lexically contains other: other itself or any scope nested below it. With strict, the reflexive case is excluded, so only a scope nested strictly below this one qualifies — the shape of the shadowing test in SemanticModel.is_shadowed().

Expand source code Browse git
def contains(self, other: Scope, *, strict: bool = False) -> bool:
    """
    Whether this scope lexically contains *other*: *other* itself or any scope nested below it.
    With *strict*, the reflexive case is excluded, so only a scope nested strictly below this one
    qualifies — the shape of the shadowing test in `SemanticModel.is_shadowed`.
    """
    cursor: Scope | None = other.parent if strict else other
    while cursor is not None:
        if cursor is self:
            return True
        cursor = cursor.parent
    return False
class ScopeKind (*args, **kwds)

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

Color.RED

  • value lookup:

Color(1)

  • name lookup:

Color['RED']

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Expand source code Browse git
class ScopeKind(enum.Enum):
    SCRIPT   = 'script'    # noqa
    FUNCTION = 'function'  # noqa
    NAME     = 'name'      # noqa  the own name of a named function expression
    PARAMS   = 'params'    # noqa  a parameter list holding an expression
    BLOCK    = 'block'     # noqa
    CATCH    = 'catch'     # noqa
    CLASS    = 'class'     # noqa
    WITH     = 'with'      # noqa
    STATIC_BLOCK = 'static-block'  # noqa
    FIELD    = 'field'     # noqa

Ancestors

  • enum.Enum

Class variables

var SCRIPT

The type of the None singleton.

var FUNCTION

The type of the None singleton.

var NAME

The type of the None singleton.

var PARAMS

The type of the None singleton.

var BLOCK

The type of the None singleton.

var CATCH

The type of the None singleton.

var CLASS

The type of the None singleton.

var WITH

The type of the None singleton.

var STATIC_BLOCK

The type of the None singleton.

var FIELD

The type of the None singleton.

class SemanticModel (root, environment=HostEnvironment.universal, trust_eval=False, file_kind=FileKind.script, exports_read_outside=True, host_entrypoint=None)

The resolved scope/binding/def-use model for one script. Build it with build_semantic_model() and query it through resolve, scope_of, binding_of, references, is_shadowed, would_capture, and has_reflection_surface.

Expand source code Browse git
class SemanticModel:
    """
    The resolved scope/binding/def-use model for one script. Build it with `build_semantic_model` and
    query it through `resolve`, `scope_of`, `binding_of`, `references`, `is_shadowed`,
    `would_capture`, and `has_reflection_surface`.
    """

    def __init__(
        self,
        root: JsScript,
        environment: HostEnvironment = HostEnvironment.universal,
        trust_eval: bool = False,
        file_kind: FileKind = FileKind.script,
        exports_read_outside: bool = True,
        host_entrypoint: Callable[[str], bool] | None = None,
    ):
        self.root = root
        self.assumed_keys = KeyAssumptions()
        self.environment = environment
        self.trust_eval = trust_eval
        self.file_kind = file_kind
        self.exports_read_outside = exports_read_outside
        self._host_entrypoint = host_entrypoint
        self._module_exports_unread: bool | None = None
        self._code_load_sites: list[Node] | None = None
        self._node_scope: dict[Node, Scope] = {}
        self._binding_of: dict[Node, Binding] = {}
        self._reflection_surface: bool | None = None
        self._opaque_surface_sites: list[Node] | None = None
        self._opaque_global_write: bool | None = None
        self._opaque_global_write_sites: list[JsMemberExpression] | None = None
        self._opaque_global_write_sites_known = False
        self._recording_def_use = False
        self._dispatch_surface_reached: bool | None = None
        self._function_direct_eval_sites: dict[int, list[Node]] = {}
        self._function_unread_source_sites: dict[int, list[Node]] = {}
        self._dynamic_rebind_sites: dict[Binding, list[Node] | None] = {}
        self._built = False
        self._spells_caller: bool | None = None
        self._reached_unnamed: dict[Node, bool] = {}
        self._constructors_escape: bool | None = None
        self._constructions_hidden: bool | None = None
        self._declares_only_functions: bool | None = None
        self._values_through_calls: dict[Binding, tuple[list[Node], bool]] = {}
        self._shown_calls: dict[tuple[Node, bool], tuple[list[ShownCall], bool]] = {}
        self._table_ways: dict[JsObjectExpression, _Way | None] = {}
        self._module_may_be_reentered = False
        self._annex_b_copies: list[AnnexBCopy] = []
        self.root_scope: Scope = _ScopeBuilder(self).build(root)
        self._build_def_use()
        self._deleted_host_globals: frozenset[str] = self._scan_deleted_host_globals()
        self._built = True

    def scope_of(self, node: Node) -> Scope | None:
        """
        The innermost scope that lexically contains *node*, or `None` if the node was not part of the
        script the model was built from.
        """
        return self._node_scope.get(node)

    def knows(self, node: Node) -> bool:
        """
        Whether the model was built over *node*. The model holds every node it maps, so a node made
        after the build is never taken for one it mapped.
        """
        return node in self._node_scope

    def function_scope(self, func: Node) -> Scope | None:
        """
        The scope a function (or the script) introduces for its body: the script's `root_scope`, the
        body block's scope for a function node, the scope of its run for a field whose initializer
        runs at each construction, and `None` when *func* has none of these.
        """
        if isinstance(func, JsScript):
            return self.root_scope
        body = getattr(func, 'body', None)
        if body is not None:
            return self.scope_of(body)
        for part in func.children():
            if evaluated_later(func, part):
                return self.scope_of(part)
        return None

    def parameter_scope(self, func: Node) -> Scope | None:
        """
        The scope holding *func*'s parameters and the `arguments` object a call gives it, which is
        its body's scope but for a function whose parameter list holds an expression: that one binds
        them in a scope of its own standing between the body and what encloses the function.

        A consumer reading a parameter binding out of a scope's own `bindings` asks for this one.
        `function_scope` answers the body's, which for such a function holds neither.
        """
        scope = self.function_scope(func)
        if scope is None:
            return None
        parent = scope.parent
        if parent is not None and parent.kind is ScopeKind.PARAMS and parent.node is scope.node:
            return parent
        return scope

    def binding_of(self, decl_id: JsIdentifier) -> Binding | None:
        """
        The binding introduced by a binding-site identifier (a declarator id, parameter, function or
        class name, catch parameter, or import local), or `None` if the identifier is not a binding
        site. The name of a function declared in a block introduces the block's binding, which this
        answers; where Annex B copies it, the `var` the copy writes lists the name among its own
        declarations as well (`AnnexBCopy`).
        """
        return self._binding_of.get(decl_id)

    def lookup(
        self,
        name: str,
        scope: Scope | None,
        *,
        cross_dynamic: bool = False,
        exclude: Collection[Binding] = (),
    ) -> Binding | None:
        """
        Resolve *name* from *scope* outward through enclosing scopes, stopping at a dynamically-scoped
        region where the name could be injected at runtime. Returns `None` for a free name. With
        *cross_dynamic*, the walk does not stop at a dynamic boundary but continues outward to the binding
        the name would denote if the `with` object lacked the property — the lexical binding a dynamic
        scope could still reach at runtime — which is how a `with`-body reference is attributed to the
        binding it may touch. The default keeps the definite-resolution semantics every other caller
        relies on.

        A binding in *exclude* is passed over as though its scope did not bind the name: the
        simulated post-rewrite resolution an atomic fold admits against, where one edit splices a
        body and deletes the bindings the splice consumes together. What the name denotes without
        them is what the tree after that edit reads.
        """
        while scope is not None:
            binding = scope.bindings.get(name)
            if binding is not None and binding not in exclude:
                return binding
            if scope.is_dynamic and not cross_dynamic:
                return None
            scope = scope.parent
        return None

    def is_reference(self, node: JsIdentifier) -> bool:
        """
        Whether *node* is a referencing occurrence of a name: it occupies a use position and is not a
        binding site, so it reads or writes an existing binding rather than declaring one or naming a
        property, key, label, or import/export specifier. The binding-aware companion to the syntactic
        `is_use_position`; `resolve` resolves exactly the identifiers for which this holds.
        """
        return is_use_position(node) and node not in self._binding_of

    def resolve(self, ref: JsIdentifier) -> Binding | None:
        """
        The binding a referencing identifier reads or writes, found by walking outward from its scope.
        Returns `None` when the name is free (an external global the program never assigns), when the
        identifier is not a reference (a property name, key, or label), or when resolution crosses a
        dynamically-scoped region where the name could be injected at runtime.
        """
        if not self.is_reference(ref):
            return None
        return self.lookup(ref.name, self._node_scope.get(ref))

    def references(
        self, binding: Binding, *, exclude: Node | None = None,
    ) -> list[ReferenceNode]:
        """
        Every reference (read or write) bound to *binding*, optionally omitting those that lie within
        the subtree of *exclude*. Each is a referencing identifier except where an object aliasing the
        binding stands in for one (see `Binding`).
        """
        nodes = binding.reads + binding.writes
        if exclude is None:
            return nodes
        return [n for n in nodes if n is not exclude and not n.is_descendant_of(exclude)]

    def dynamic_references(
        self, binding: Binding, *, exclude: Node | None = None,
    ) -> list[JsIdentifier]:
        """
        Every reference to *binding* that a dynamic scope resolves at runtime — an identifier inside a
        `with` body that could denote *binding* (it may instead denote a property of the `with` object,
        which is why the static `references` set omits it) — optionally omitting those within the subtree
        of *exclude*. Each is classified on demand by `reference_role` or `container_reference_role`, the
        same oracles the definite references use, so a consumer applies one role logic to both; only the
        ordering and alias-following a resolved reference permits do not carry to an uncertain one.
        """
        nodes = binding.dynamic_refs
        if exclude is None:
            return list(nodes)
        return [n for n in nodes if n is not exclude and not n.is_descendant_of(exclude)]

    def read_has_dynamic_effect(self, node: Node) -> bool:
        """
        Whether reading *node* as a value resolves through a dynamic scope — a bare identifier inside a
        `with` body — so that evaluating it is not a pure, droppable, or reorderable operand. Reading the
        bare name consults the `with` object first: a matching property fires the object's getter (or a
        proxy trap), an observable side effect; a missing one falls through to the lexical binding, or,
        failing that, throws a `ReferenceError`. Neither the getter nor the throw can be proved absent for
        an unknown object, so any reference that crosses a dynamic scope is effectful regardless of a
        lexical fallback. False for a statically resolved reference and any non-reference node.
        """
        if not isinstance(node, JsIdentifier) or not self.is_reference(node):
            return False
        return crosses_dynamic_scope(self._node_scope.get(node))

    def read_may_throw(self, node: JsIdentifier) -> bool:
        """
        Whether evaluating *node* as a read may throw a `ReferenceError` because the name it spells
        is not certain to denote a binding. The companion to `read_has_dynamic_effect`, which asks
        what else a read may do; this asks whether it may not happen at all. A caller that treats an
        unresolved read as free is asserting the host defines the name, which for a name the program
        neither declares nor assigns is an assertion about someone else's global object.

        A name resolves for certain when a declaration binds it, or when the pinned host environment
        guarantees it on the global object (`HostEnvironment.provides`). The default `universal`
        environment provides exactly `GUARANTEED_GLOBALS`, the existence allowlist the language
        mandates everywhere, so `globalThis` resolves while the other `GLOBAL_OBJECT_ALIASES`
        spellings (`window`, `self`, `top`, `frames`, `global`) are a *host* assumption rather than
        a language one, and are not admitted: no host defines all of them, so a bare `window` throws
        under Node exactly as a bare `global` throws in a browser. An analyst who knows the host
        pins it with the `djs` unit's `-e` switch, and the names that host guarantees become certain
        here, recovering the reading the sound default refuses — except a host-conditional global
        the program `delete`s off a same-realm alias, which `_scan_deleted_host_globals` withholds
        program-wide so the bare read keeps its throw. Unpinned, the read is answered may-throw so
        no pass drops the `ReferenceError` the absent host raises, and `_base_is_safe` agrees,
        refusing to clear a property access on such an alias. Everything else may not be there:

        - a free name, which reaches the host and may simply not exist
        - a name whose only binding is an `IMPLICIT_GLOBAL`, which the assignment that creates it
          brings into existence, so a read that runs first — or whose creating assignment sits in a
          function nobody calls — throws exactly as a free name does
        - a name resolved through a `with` body whose object may not carry it and which has no
          lexical binding to fall through to, which the `cross_dynamic` lookup is what distinguishes
        - a binding one way the file may run has and another does not (`Binding.may_be_absent`)

        A reference that is written and not read answers `False`, as do the two operator positions
        `tolerates_unresolvable` names. The write case is a scope boundary, not a claim that
        writing is safe: sloppy code assigning to a name nothing binds creates a property of the
        global object, while strict code throws the same `ReferenceError` a read does, which is a
        separate defect with its own pin
        (`test_unfixed_defects.A_STRICT_REGION_ASSIGNING_TO_NO_BINDING`).
        """
        if not self.is_reference(node) or reference_role(node) is Role.WRITE:
            return False
        if self._certainly_resolves(node.name):
            return False
        if tolerates_unresolvable(node):
            return False
        scope = self._node_scope.get(node)
        binding = self.lookup(node.name, scope, cross_dynamic=True)
        return (
            binding is None
            or binding.kind is BindingKind.IMPLICIT_GLOBAL
            or binding.may_be_absent
        )

    def _certainly_resolves(self, name: str) -> bool:
        """
        Whether a bare read of *name* is guaranteed to find a global the host defines, so it cannot
        raise a `ReferenceError`. The pinned `environment` answers which names the host provides; a
        host-conditional global the program deletes off a same-realm global alias is withheld, since
        after that delete the bare read throws. A language-mandated name is never withheld, so the
        default `universal` environment answers exactly as its `GUARANTEED_GLOBALS` membership did.
        """
        if not self.environment.provides(name):
            return False
        return not (
            name in self._deleted_host_globals
            and self.environment.withholds_on_delete(name)
        )

    def _scan_deleted_host_globals(self) -> frozenset[str]:
        """
        The host-conditional global names the program deletes off a global-object alias anywhere
        (`delete globalThis.Buffer`, `delete window['setTimeout']`), whose presence the pinned host
        would otherwise assert. The scan is flow-insensitive: a delete on any path, reachable or not,
        withholds the name program-wide, which over-keeps a read's throw and so stays sound. It keys on
        the wide `GLOBAL_OBJECT_ALIASES`, not the same-realm subset, because `top` and `frames` name
        this realm's own global object in an unframed document, so a delete spelled through one there
        removes the name here too; withholding on a framed document's cross-realm delete only over-keeps
        a throw, which stays sound. It is the empty set under the default `universal` environment, which
        withholds nothing and so needs no scan, keeping an unpinned run free of the walk. A delete
        reached through a variable holding the global object rather than through an alias spelling is not
        modelled, the limit the rest of the global-object analysis shares.
        """
        if self.environment is HostEnvironment.universal:
            return frozenset()
        deleted: set[str] = set()
        for node in self.root.walk():
            if not (isinstance(node, JsUnaryExpression) and node.operator == 'delete'):
                continue
            target = strip_parens(node.operand)
            if not isinstance(target, JsMemberExpression):
                continue
            base = strip_parens(target.object)
            if not isinstance(base, JsIdentifier):
                continue
            if base.name not in GLOBAL_OBJECT_ALIASES:
                continue
            if self.lookup(base.name, self._node_scope.get(base)) is not None:
                continue
            name = key_named(target, self.assumed_keys)
            if name is not None:
                deleted.add(name)
        return frozenset(deleted)

    def lexical_binding_read(self, node: JsIdentifier) -> Binding | None:
        """
        The `let`/`const`/`class` binding *node* reads, or `None` when *node* is not a read of
        one. A read of such a binding resolves for certain, yet may still raise a
        `ReferenceError` when it runs before the declaration that ends the binding's temporal
        dead zone; a caller that needs the binding — to defer it to a call site
        (`EffectSummary.dead_zone_reads`) — takes it from here rather than resolving a second
        time. Whether the read is in fact in the dead zone is an ordering question this layer
        does not answer — a caller's establishment proof (`ModelCache.read_established`) decides
        it against the dominance model.
        """
        if not self.is_reference(node) or reference_role(node) is Role.WRITE:
            return None
        binding = self.resolve(node)
        return binding if binding is not None and binding.is_lexical else None

    def reads_lexical_binding(self, node: JsIdentifier) -> bool:
        """
        Whether *node* is a read of a `let`, `const`, or `class` binding. The companion flag to
        `read_may_throw`, which instead flags a name that may denote no binding at all; together
        they are the complete set of reads a discarding context must not drop without a proof the
        read is past its establishing point. The binding itself, when a caller needs it, comes from
        `lexical_binding_read`.
        """
        return self.lexical_binding_read(node) is not None

    def read_may_raise_reference_error(self, node: JsIdentifier) -> bool:
        """
        Whether evaluating *node* as a read may raise a `ReferenceError`: it may denote no binding at
        all (`read_may_throw`) or it reads a `let`/`const`/`class` binding that may still be in its
        temporal dead zone (`reads_lexical_binding`). The one flag a discarding context tests before
        dropping a read; a context holding an ordering proof clears the dead-zone case through it
        (`EffectModel.read_throws`), a context holding none fails closed and keeps the read
        (`EffectModel._read_effectful_or_throwing`).
        """
        return self.read_may_throw(node) or self.reads_lexical_binding(node)

    def statement_cannot_throw(
        self,
        statement: Node | None,
        store_cannot_throw: Callable[[JsAssignmentExpression], bool] | None = None,
    ) -> bool:
        """
        Whether evaluating *statement* throws on no run at all, so that a run leaving it for a
        handler never leaves it half done. Only a declaration of plain names, a function declaration
        and an expression statement qualify, the first and the last when `evaluation_cannot_throw`
        holds for everything they evaluate. Binding a destructuring pattern can throw after its
        initializer ran, and a `var` declared inside a `with` body stores through the object, whose
        setter may throw, so a declaration with either does not qualify. Reaching a function
        declaration does nothing, since its function is in place before the statements of its scope
        run, except where Annex B copies it out of its block: that copy stores the function into the
        name of the enclosing scope, which is ruled as every other store into a declared name is. A
        declaration standing as the target of a `for-in` or `for-of` head is asked as the store of
        the value the head hands it on every iteration. *store_cannot_throw* is handed on to
        `evaluation_cannot_throw`.
        """
        if isinstance(statement, JsVariableDeclarator):
            return self._declarator_cannot_throw(statement, store_cannot_throw)
        if isinstance(statement, JsVariableDeclaration):
            return all(
                isinstance(declarator, JsVariableDeclarator)
                and self._declarator_cannot_throw(declarator, store_cannot_throw)
                for declarator in statement.declarations
            )
        if isinstance(statement, JsFunctionDeclaration):
            binding = None if statement.id is None else self.binding_of(statement.id)
            if binding is None:
                return False
            copy = _copy_made_by(binding, statement)
            if copy is None:
                return True
            return self._stores_into_without_throwing(copy.var, statement)
        if isinstance(statement, JsExpressionStatement):
            return self.evaluation_cannot_throw(statement.expression, store_cannot_throw)
        return False

    def _declarator_cannot_throw(
        self,
        declarator: JsVariableDeclarator,
        store_cannot_throw: Callable[[JsAssignmentExpression], bool] | None,
    ) -> bool:
        """
        Whether evaluating *declarator* throws on no run at all, as `statement_cannot_throw` asks
        it for every declarator of a declaration, and for one the control-flow graph gives a node of
        its own.
        """
        target = declarator.id
        if not isinstance(target, JsIdentifier):
            return False
        binding = self.binding_of(target)
        if binding is None or crosses_dynamic_scope(self._node_scope.get(target)):
            return False
        init = declarator.init
        each_iteration = _loop_head_assigns(declarator)
        if init is not None and each_iteration:
            return False
        if (
            (init is not None or each_iteration)
            and not binding.is_lexical
            and not self._stores_into_without_throwing(binding, declarator)
        ):
            return False
        return init is None or self.evaluation_cannot_throw(init, store_cannot_throw)

    def evaluation_cannot_throw(
        self,
        node: Node | None,
        store_cannot_throw: Callable[[JsAssignmentExpression], bool] | None = None,
    ) -> bool:
        """
        Whether evaluating the expression *node* throws on no run at all. Deliberately minimal: a
        primitive literal, a function value, a read that neither `read_may_raise_reference_error`
        nor `read_has_dynamic_effect` flags, and a plain `=` store of such a value to a name, joined
        by parentheses and the comma operator.

        A store to a name cannot throw when the name resolves, through no `with` body, to a `var`,
        a function, a parameter or a catch binding, unless it is a strict store to a script-level
        `var` or function. Such a binding is a property of the global object, whose attributes are
        the host's: the language makes `undefined`, `NaN` and `Infinity` read-only, and a browser
        gives `window`, `document`, `top`, `navigator` and many more a getter without a setter,
        which a strict store refuses and a sloppy one ignores. A `let` may still be in its temporal
        dead zone, a `const` refuses every store, and a function expression's own name and a name
        nothing declares refuse a strict one. *store_cannot_throw* vouches for any other store its
        caller holds a proof for.
        """
        if node is None:
            return False
        if isinstance(node, (
            JsStringLiteral,
            JsNumericLiteral,
            JsBooleanLiteral,
            JsNullLiteral,
        )):
            return True
        if isinstance(node, FUNCTION_NODES):
            return True
        if isinstance(node, JsParenthesizedExpression):
            return self.evaluation_cannot_throw(node.expression, store_cannot_throw)
        if isinstance(node, JsSequenceExpression):
            return all(
                self.evaluation_cannot_throw(part, store_cannot_throw) for part in node.expressions
            )
        if isinstance(node, JsIdentifier):
            return (
                self.is_reference(node)
                and not self.read_has_dynamic_effect(node)
                and not self.read_may_raise_reference_error(node)
            )
        if isinstance(node, JsAssignmentExpression):
            if node.operator != '=':
                return False
            target = strip_parens(node.left)
            if not isinstance(target, JsIdentifier):
                return False
            binding = self.resolve(target)
            vouched = binding is not None and self._stores_into_without_throwing(binding, node)
            if not vouched and (store_cannot_throw is None or not store_cannot_throw(node)):
                return False
            return self.evaluation_cannot_throw(node.right, store_cannot_throw)
        return False

    def _stores_into_without_throwing(self, binding: Binding, store: Node) -> bool:
        if binding.kind not in _BINDINGS_A_STORE_CANNOT_REFUSE or binding.may_be_absent:
            return False
        return binding.scope is not self.root_scope or not strict_mode_at(store)

    def _naming_binding(self, function: Node) -> Binding | None:
        """
        The binding that gives *function* a name through which it can be invoked: the declared name of a
        named function declaration, or the single `var`/`let`/`const` declarator a function or arrow
        expression is the initializer of. `None` for an anonymous function whose invocation point cannot
        be pinned to a name — an IIFE, a callback, a function stored through any other expression.
        """
        if isinstance(function, JsFunctionDeclaration) and function.id is not None:
            return self.binding_of(function.id)
        parent = function.parent
        if (
            isinstance(parent, JsVariableDeclarator)
            and parent.init is function
            and isinstance(parent.id, JsIdentifier)
        ):
            return self.binding_of(parent.id)
        return None

    def function_names(self, function: Node) -> FunctionNames | None:
        """
        The names *function* is called and observed through, or `None` for a function no name
        holds: an IIFE, a callback, a function stored through anything but a name, which a consumer
        reads as running where it stands or wherever what holds it hands it on. The own name is the
        declared name of a function declaration, the one declarator a function or arrow expression
        initializes, or the name a lone assignment installs it in (`f = function(){}`), so that a
        function held in a hoisted `var` assigned once is ordered by its calls rather than by its
        creation. A function declared in a block has the `var` Annex B copies it into as a second
        name wherever something observes that copy (`AnnexBCopy.observed`).

        A caller confirms that each name is singly declared, `binding_pinned_to` *function*, and
        free of dynamic references before trusting its reads to enumerate every invocation.
        """
        own = self._invocation_binding(function)
        if own is None:
            return None
        copy = None
        if isinstance(function, JsFunctionDeclaration):
            copy = _copy_made_by(own, function)
        if copy is not None and not copy.observed:
            copy = None
        return FunctionNames(own, copy)

    def _invocation_binding(self, function: Node) -> Binding | None:
        """
        The own name of `function_names`.
        """
        binding = self._naming_binding(function)
        if binding is not None:
            return binding
        parent = function.parent
        if (
            isinstance(parent, JsAssignmentExpression)
            and parent.operator == '='
            and parent.right is function
        ):
            target = strip_parens(parent.left)
            if isinstance(target, JsIdentifier):
                return self.resolve(target)
        return None

    def name_holding(self, read: Node) -> Binding | None:
        """
        The one name the value read at *read* is stored into, when *read* is the whole value a
        declarator or an assignment whose own value is discarded stores into a name. An assignment
        whose value is used hands it on as well, so its target is not the one name holding the
        value: `f(x = ref)` and `var y = (x = ref)`.
        """
        store = enclosing_operator(read)
        if isinstance(store, JsVariableDeclarator):
            if strip_parens(store.init) is not read or not isinstance(store.id, JsIdentifier):
                return None
            return self.binding_of(store.id)
        if not isinstance(store, JsAssignmentExpression):
            return None
        if store.operator != '=' or strip_parens(store.right) is not read:
            return None
        if not value_is_discarded(store):
            return None
        target = strip_parens(store.left)
        if not isinstance(target, JsIdentifier):
            return None
        return self.resolve(target)

    def binding_pinned_to(self, binding: Binding, function: Node) -> bool:
        """
        Whether *binding* holds *function* as its one assigned value, so every read of it outside the
        value's temporal dead zone denotes *function* and its reads enumerate *function*'s invocations.
        True when the binding's only write is the assignment that establishes *function* — a bare
        `name = function(){}` records that target as its sole write — and false once any other write could
        give the name a different value. A named function declaration or a declarator initializer installs
        the value with no recorded write, so any write at all is a reassignment that unpins it. The
        single-declaration and dynamic-reference checks a caller also needs are left to the caller; this
        answers only the reassignment question — the whole of it, so a write that leaves no `writes`
        entry because nothing says what it stored (`has_indefinite_write`) unpins the name as much
        as one that does. A copy Annex B makes into the name is a write too, and it unpins the name
        unless it stores the function its declaration declares (`copy_stores_the_function`).
        """
        parent = function.parent
        establishing = None
        if (
            isinstance(parent, JsAssignmentExpression)
            and parent.operator == '='
            and parent.right is function
        ):
            establishing = strip_parens(parent.left)
        if binding.has_indefinite_write:
            return False
        if not all(self.copy_stores_the_function(copy) for copy in binding.copies_received):
            return False
        return all(write is establishing for write in binding.writes)

    def copy_stores_the_function(self, copy: AnnexBCopy) -> bool:
        """
        Whether *copy* stores the function its declaration declares, and nothing else can come of
        the function through the block binding: the block binding has that one declaration, no write
        of any kind and nothing that could rebind it at run time, so it holds the function wherever
        the copy runs; and every other read of it calls or constructs it, so the one function object
        the two names share is not handed on, stored or given properties through the name the `var`
        cannot see.
        """
        block = copy.block
        if (
            len(block.declarations) != 1
            or block.writes
            or block.has_indefinite_write
            or block.dynamic_refs
            or self.binding_maybe_reassigned_dynamically(block)
        ):
            return False
        return all(
            isinstance(read, JsFunctionDeclaration) or is_constructed_or_invoked(read)
            for read in block.reads
        )

    def binding_values(
        self, binding: Binding | None, *, ignore_dynamic_rebinds: bool = False,
    ) -> tuple[list[Node], bool]:
        """
        Every value expression the text stores under *binding* through a channel that spells its stored
        value, in no promised order, and whether that list is complete — whether no other channel can
        give the name a value.
        The readable channels are a declarator's initializer, the function or class of a declaration,
        and the right side of a plain `=` written through the referencing identifier. Every other way a
        value can arrive makes the answer incomplete without contributing a value: **every** parameter
        is incomplete, because the call site is a value channel this model cannot see, and so are a
        catch or import binding and a function expression's own name; a compound assignment, an update,
        a `for-in`/`for-of` head, and a destructuring target store a value they do not spell; a write
        recorded with no value and a dynamic rebinding (`binding_maybe_reassigned_dynamically`) say a
        value arrived without saying which. A write through a member access on a global-object alias
        is left unread and breaks completeness too: which member accesses are such writes depends on
        the names holding the global object, and those are found from these values
        (`_bindings_holding_the_global_object`), so a value read out of such a write would feed the
        very set that decides whether it is one. A binding with no declaration — an implicit global,
        and the binding `_ensure_implicit_global_from_alias_write` mints — contributes nothing and
        is never complete, even where the file assigns it by name. With *ignore_dynamic_rebinds* the
        dynamic-rebind conjunct is left out of the completeness verdict: the values answer what the
        text spells, and whether a rebind crosses a given read is the caller's ordering question
        over `binding_dynamic_rebind_sites`.

        The values hold wherever the name is not in their temporal dead zone; a bare declarator
        contributes no value even though the name reads `undefined` there, and a consumer that needs a
        value established before a use orders it separately (`binding_establishment_sites`). A
        recognizer whose safe direction is admitting decides on ANY value and ignores completeness; a
        consumer whose rewrite needs the binding to hold nothing else requires completeness first, and
        must also require a value, since a complete empty list answers every universal question
        vacuously. `refinery.lib.scripts.js.analysis.intrinsics._binding_value_roots` is the
        may-side sibling that over-approximates where this list refuses, and `Binding.store_sites`
        the one that lists where a value is stored rather than which.
        """
        channels, complete = self._binding_value_channels(
            binding, ignore_dynamic_rebinds=ignore_dynamic_rebinds)
        return [value for _, value in channels], complete

    def values_at_call(
        self,
        binding: Binding | None,
        arguments: dict[Binding | None, Node | None],
    ) -> tuple[list[Node], bool]:
        """
        `binding_values` read at one call site: *arguments* maps each parameter binding of the
        called function to the argument that call supplies for it (`_argument_parameter_map`), and
        for a binding it covers, the mapped argument is the entry channel `binding_values` cannot
        see — so the answer can be complete where the plain query never is. A parameter the
        call supplies no argument for maps to `None` and stays incomplete, unless the parameter list
        gives it a default; the default is then one of its values, as it is beside an argument that
        may be `undefined`. Every other rule is
        `binding_values`' own: a write the text spells no value for, and any dynamic rebinding —
        a direct `eval` in the function, and a write through its own `arguments` object — still
        poison the answer. For a binding *arguments* does not cover, the answer is exactly
        `binding_values`.
        """
        channels, complete = self._binding_value_channels(binding, arguments)
        return [value for _, value in channels], complete

    def values_through_calls(self, binding: Binding | None) -> tuple[list[Node], bool]:
        """
        `binding_values`, with a parameter read through the calls of its function the text shows:
        its values are its own writes, its default, and the argument each such call passes at its
        position (`values_at_call`). A call shown is one `_calls_shown` lists: an invocation of a
        name holding the function, its own name included, of a table entry it may select, or of
        the function where it is written; for a class constructor, a `new` of its class the same
        way, and a `super` call in the constructor of a class extending it. A value one of these
        calls passes is listed whatever other calls exist and whatever the other parameters are;
        only a spread at or before the position, or arguments the call does not write out, hide
        which argument the call passes there.

        The list is complete only when no call can come another way (`_calls_shown`) and every
        shown call passes an argument at the position, or the parameter has a default. Every
        consumer reads a parameter through here, so none can forget a way a call comes. Memoized
        per binding.
        """
        if binding is None or not binding.declarations:
            return self.binding_values(binding)
        declared = _declared_parameter(binding.declarations[0])
        if declared is None:
            return self.binding_values(binding)
        cached = self._values_through_calls.get(binding)
        if cached is not None:
            return cached
        function, position, default = declared
        values, _ = self.binding_values(binding)
        listed = {id(value) for value in values}

        def list_value(value: Node) -> None:
            if id(value) not in listed:
                listed.add(id(value))
                values.append(value)

        calls, complete = self._calls_shown(function)
        for call in calls:
            argument, hidden = _passed_at(call, position)
            if hidden:
                complete = False
                continue
            received, received_all = self.values_at_call(binding, {binding: argument})
            complete = complete and received_all
            for value in received:
                list_value(value)
        if default is not None:
            list_value(default)
        cached = self._values_through_calls[binding] = values, complete
        return cached

    def arguments_through_calls(self, function: JsFunctionNode, position: int | None) -> tuple[list[Node], bool]:
        """
        What an element of the `arguments` of *function* holds: the arguments the calls of the
        function the text shows (`values_through_calls`) pass at *position*, or at any position
        when it is `None`. The list is complete only when no call can come another way
        (`_calls_shown`) and every shown call passes an argument there that no spread hides.
        """
        calls, complete = self._calls_shown(function)
        passed: list[Node] = []
        for call in calls:
            if position is None:
                if call.arguments is None:
                    complete = False
                    continue
                for argument in call.arguments:
                    argument = strip_parens(argument)
                    if isinstance(argument, JsSpreadElement):
                        complete = False
                    elif argument is not None:
                        passed.append(argument)
                continue
            argument, _ = _passed_at(call, position)
            if argument is None:
                complete = False
            else:
                passed.append(argument)
        return passed, complete

    def parameter_never_passed(self, binding: Binding, *, own_keys: bool = False) -> bool:
        """
        Whether the function of the parameter *binding* is called, and no call can pass an argument
        to the parameter, which then holds `undefined` or its default whenever the function starts:
        it is a name of a parameter list, plain or with a default, every way to its function is a
        call the text shows (`_calls_shown`, with *own_keys*), there is one at least, and none of
        those passes an argument at its position or hides which argument it passes there.
        """
        if len(binding.declarations) != 1:
            return False
        declared = _declared_parameter(binding.declarations[0])
        if declared is None:
            return False
        function, position, _ = declared
        calls, complete = self._calls_shown(function, own_keys=own_keys)
        if not complete or not calls:
            return False
        for call in calls:
            argument, hidden = _passed_at(call, position)
            if argument is not None or hidden:
                return False
        return True

    def _names_calling(self, held: Node) -> list[_Way | None]:
        """
        The names the function or class *held* is called or constructed through: its declared
        names (`function_names`), the one name a declarator or an assignment statement stores it
        into (`name_holding`), or the name of a table holding it (`_table_holding`), and the own
        name of a function or class expression. `None` stands for a holder that is no name. One
        invoked where it is written (`_invocation_of`) is held by none.
        """
        names: list[_Way | None] = []
        if isinstance(held, JsFunctionDeclaration):
            declared = self.function_names(held)
            if declared is None:
                names.append(None)
            else:
                names.extend(_Way(name, declared.reads_of(name), held) for name in declared.bindings)
        elif isinstance(held, JsClassDeclaration):
            own = self.binding_of(held.id) if isinstance(held.id, JsIdentifier) else None
            names.append(None if own is None else _Way(own, own.reads, held))
        elif self._invocation_of(held) is None:
            holder = self.name_holding(held)
            if holder is None:
                names.append(self._table_holding(held))
            else:
                names.append(_Way(holder, holder.reads, held))
        if isinstance(held, (JsFunctionExpression, JsClassExpression)) and isinstance(held.id, JsIdentifier):
            own = self.binding_of(held.id)
            names.append(None if own is None else _Way(own, own.reads, None))
        return names

    def _table_holding(self, held: Node) -> _Way | None:
        """
        The way to a function written as the value of a property of an object literal
        (`_table_way`), or `None` where *held* is written anywhere else. Memoized per literal.
        """
        entry = enclosing_operator(held)
        if not isinstance(entry, JsProperty) or strip_parens(entry.value) is not held:
            return None
        table = entry.parent
        if not isinstance(table, JsObjectExpression):
            return None
        if table not in self._table_ways:
            self._table_ways[table] = None
            self._table_ways[table] = self._table_way(table)
        return self._table_ways[table]

    def _table_way(self, table: JsObjectExpression) -> _Way | None:
        """
        The way to a function written as the value of a property of the object literal *table*:
        the one name the literal is stored into (`name_holding`), with a node for each read of it,
        the member access it is the object of where it is one, which may select the function, and
        the read itself otherwise. `None` where the table can reach code that is not shown:
        an invocation through the table hands it over as `this`, so every property has to be a
        plain one (`key: value`, no accessor, method or spread, which may read it through `super`)
        whose value is a function reading no `this` of its own (`_function_observes_its_this`);
        and a key the text spells, or a string a call shown passes for it
        (`_keys_a_call_spells`), may not select what the table inherits, where `valueOf` hands it
        back: where the literal spells every key, it has to be one of them, and where some key of
        the literal is one only the run knows, it may not be a member every object inherits
        (`refinery.lib.scripts.js.builtins.OBJECT_PROTOTYPE_MEMBERS`). A key only the run knows may
        name such a member too; the way lists the accesses under one as unkeyed, which
        `_calls_shown` takes for an entry's call only where its caller assumes such a key is one of
        the literal's own.
        """
        holder = self.name_holding(table)
        if holder is None:
            return None
        keys: set[str] = set()
        every_key_spelled = True
        for prop in table.properties:
            if (
                not isinstance(prop, JsProperty)
                or prop.kind is not JsPropertyKind.INIT
                or prop.method
                or not isinstance(value := strip_parens(prop.value), (JsFunctionExpression, JsArrowFunctionExpression))
                or self._function_observes_its_this(value, set(), 0)
            ):
                return None
            if (key := key_text(prop)) is not None:
                keys.add(key)
            else:
                every_key_spelled = False
        nodes: list[Node] = []
        unkeyed: set[Node] = set()
        for read in holder.reads:
            access = _enclosing_member_access(read)
            if access is None:
                nodes.append(read)
                continue
            key = key_text(access)
            spelled, known = ({key}, True) if key is not None else self._keys_a_call_spells(access)
            foreign = spelled - keys
            if foreign and (every_key_spelled or foreign & OBJECT_PROTOTYPE_MEMBERS):
                return None
            if not known:
                unkeyed.add(access)
            nodes.append(access)
        return _Way(holder, nodes, None, frozenset(unkeyed))

    def _keys_a_call_spells(self, access: JsMemberExpression) -> tuple[set[str], bool]:
        """
        The strings the computed key of *access* may be by what the text spells, and whether those
        are all it may be: where the key is a name, each string among the values it may hold
        (`values_through_calls`), what a call shown passes for a parameter among them, all of them
        where the list is complete and every value is a string the text spells. Any other value,
        and any other key, is one only the run knows.
        """
        key = strip_parens(access.property)
        if not access.computed or not isinstance(key, JsIdentifier):
            return set(), False
        values, complete = self.values_through_calls(self.resolve(key))
        spelled = [expression_key(value) for value in values]
        known = complete and bool(spelled) and None not in spelled
        return {string for string in spelled if string is not None}, known

    def shown_calls(
        self,
        function: JsFunctionNode,
    ) -> list[JsCallExpression | JsNewExpression] | None:
        """
        Every call and construction that runs *function*, which are the expressions its return
        value becomes (`_calls_shown`), or `None` where one may come another way.
        """
        calls, complete = self._calls_shown(function)
        return [call.node for call in calls] if complete else None

    def reached_only_by_shown_calls(self, function: JsFunctionNode, *, own_keys: bool = False) -> bool:
        """
        Whether every way to *function* is a call the text shows or a test of its names
        (`SemanticModel._calls_shown`, with *own_keys*), and no class extends it, so that nothing
        reads a property of it: the static code of a class extending it reads its properties
        through `super`.
        """
        if not self._calls_shown(function, own_keys=own_keys)[1]:
            return False
        return not any(
            _extended_by(read) is not None
            for _, reads in self._shown_names(function)
            for read in reads
        )

    def _shown_names(self, function: JsFunctionNode) -> list[tuple[Binding, Sequence[Node]]]:
        """
        The names `_calls_shown` follows to *function* or to the class it constructs, each with the
        nodes through which it may reach it.
        """
        held = constructed_class(function) or function
        return [(way.binding, way.nodes) for way in self._names_calling(held) if way is not None]

    def _calls_shown(
        self,
        function: JsFunctionNode,
        *,
        own_keys: bool = False,
    ) -> tuple[list[ShownCall], bool]:
        """
        The calls of *function* the text shows, in the sense of `values_through_calls`, and whether
        no call can come another way: every node through which a name it is called through may
        reach it (`_names_calling`) is invoked (`_invocation_of`), only tested
        (`read_is_only_tested`), or the `extends` clause of a class whose own constructor makes the
        `super` calls shown, or which has none, so that its own calls are shown in turn; the names
        of such a class are read the same way, since its prototype is the one it extends; each name
        holds what it has to hold alone (`_Way`), and nothing reaches it where no read of it stands
        (`reached_without_a_read`): it is neither exported nor written at entry nor reached through
        a handed object, by reflection, or by code outside the file; no class among them can be
        constructed without its name
        (`_constructed_without_its_name`); nothing reaches the function without its name
        (`reached_without_its_name`); no class among them extends a constructor the program does
        not declare (`_extends_beyond_the_program`), whose methods construct it; and where a shown
        call constructs it, no construction is hidden (`constructions_may_be_hidden`), through an
        escaping `constructor` property, a `new` of one, or `new.target`. A table read under a key
        only the run knows (`_Way.unkeyed`) may select what the table inherits, `valueOf` handing it
        back, so it is a way no call shows, unless *own_keys* has such a key taken for one of the
        literal's own: an assumption only a caller that has argued it may make. Memoized per
        function and assumption.
        """
        cached = self._shown_calls.get((function, own_keys))
        if cached is not None:
            return cached
        calls: list[ShownCall] = []
        complete = not self.reached_without_its_name(function)
        root = constructed_class(function) or function
        pending: list[Node] = [root]
        seen = {root}
        while pending:
            held = pending.pop()
            cls = held if isinstance(held, (JsClassDeclaration, JsClassExpression)) else None
            forwards = held is root or (cls is not None and _super_calls(cls) is None)
            if cls is not None and (
                _constructed_without_its_name(cls)
                or self._extends_beyond_the_program(cls)
            ):
                complete = False
            direct = self._invocation_of(held)
            if direct is not None and forwards:
                calls.append(direct)
            for way in self._names_calling(held):
                if way is None:
                    complete = False
                    continue
                if self.reached_without_a_read(way.binding):
                    complete = False
                if way.alone is not None and strip_parens(self.singular_value(way.binding)) is not way.alone:
                    complete = False
                for read in way.nodes:
                    if read in way.unkeyed and not own_keys:
                        complete = False
                        continue
                    call = self._invocation_of(read)
                    subclass = _extended_by(read)
                    if call is not None:
                        if forwards:
                            calls.append(call)
                    elif subclass is not None:
                        supers = _super_calls(subclass) or ()
                        calls.extend(ShownCall(call, call.arguments) for call in supers)
                        if subclass not in seen:
                            seen.add(subclass)
                            pending.append(subclass)
                    elif not read_is_only_tested(read):
                        complete = False
        if (
            complete
            and any(isinstance(call.node, JsNewExpression) for call in calls)
            and self.constructions_may_be_hidden()
        ):
            complete = False
        cached = self._shown_calls[function, own_keys] = calls, complete
        return cached

    def _binding_value_channels(
        self,
        binding: Binding | None,
        arguments: dict[Binding | None, Node | None] | None = None,
        *,
        ignore_dynamic_rebinds: bool = False,
    ) -> tuple[list[tuple[Node, Node]], bool]:
        """
        The readable value channels of *binding* as `(site, value)` pairs — the node whose execution
        installs the value, and the value expression — plus the completeness verdict `binding_values`
        documents. One derivation feeds `binding_values`, `values_at_call`, `singular_value`, and
        `binding_establishment_sites`, so they can never disagree about which channels a binding
        has. With *arguments* — a call's parameter-to-argument map — a parameter declaration of a
        covered binding is a readable channel carrying the mapped argument, the `values_at_call`
        reading; without it, a parameter declaration is an unseen channel. With
        *ignore_dynamic_rebinds* the dynamic-rebind conjunct is left out of the completeness
        verdict: the channels answer the one value the text spells, and whether a rebind crosses a
        given read stays the caller's ordering question over `binding_dynamic_rebind_sites`. The
        value of a lone-assignment channel is returned with its parentheses stripped, the
        normalization every consumer of `singular_value` has always received there.
        """
        if binding is None or not binding.declarations:
            return [], False
        channels: list[tuple[Node, Node]] = []
        complete = (
            ignore_dynamic_rebinds
            or not self.binding_maybe_reassigned_dynamically(binding)
        )
        if binding.supplied_by_host and not any(
            isinstance(declaration.parent, JsFunctionDeclaration)
            and declaration.parent.id is declaration
            for declaration in binding.declarations
        ):
            complete = False
        for declaration in binding.declarations:
            parent = declaration.parent
            if (
                isinstance(parent, (JsFunctionDeclaration, JsClassDeclaration))
                and parent.id is declaration
            ):
                copy = _copy_received_by(binding, parent)
                if copy is None or (not copy.conditional and self.copy_stores_the_function(copy)):
                    channels.append((parent, parent))
                else:
                    complete = False
            elif isinstance(parent, JsVariableDeclarator) and parent.id is declaration:
                if _loop_head_assigns(parent):
                    complete = False
                if parent.init is not None:
                    channels.append((parent, parent.init))
            elif (
                arguments is not None
                and binding in arguments
                and (parameter := _declared_parameter(declaration)) is not None
            ):
                argument = arguments[binding]
                default = parameter[2]
                if argument is not None:
                    channels.append((declaration, argument))
                if default is not None:
                    channels.append((declaration, default))
                elif argument is None:
                    complete = False
            else:
                complete = False
        for write in binding.writes:
            assignment = write.parent
            stored = None
            if (
                isinstance(write, JsIdentifier)
                and isinstance(assignment, JsAssignmentExpression)
                and assignment.operator == '='
                and strip_parens(assignment.left) is write
            ):
                stored = strip_parens(assignment.right)
            if stored is None:
                complete = False
            else:
                channels.append((write, stored))
        return channels, complete

    def binding_value_sites(self, binding: Binding | None) -> list[ReferenceNode] | None:
        """
        Every node whose execution stores into *binding* (`Binding.store_sites`), or `None` where a
        value may arrive at no such node: the call writes the name before any statement runs
        (`Binding.written_at_entry`, a parameter, a catch or import binding, a function expression's
        own name), some access writes it without saying where (`Binding.has_indefinite_write`, a
        dynamic rebinding), the host supplies it, or a function it declares is in place before any
        statement of its scope runs. Ordering the sites against a point is the caller's, since that
        needs the dominance model this layer must not depend on.
        """
        if (
            binding is None
            or not binding.declarations
            or binding.written_at_entry
            or binding.has_indefinite_write
            or binding.supplied_by_host
            or self.binding_maybe_reassigned_dynamically(binding)
            or any(
                _where_it_stores(binding, declaration) is _DeclarationStore.ON_ENTRY
                for declaration in binding.declarations
            )
        ):
            return None
        return binding.store_sites

    def singular_value(self, binding: Binding | None) -> Node | None:
        """
        The single value node a *binding* provably holds: the sole entry of a complete
        `binding_values` answer. `None` when the binding is absent, stores more than one value, or has
        any channel the text does not spell — a name whose declaration carries a value and is then
        assigned holds two values across its life and is refused, as is every parameter. The value is
        what the name denotes wherever it is not in the value's temporal dead zone; a consumer that also
        needs the value established before a use orders it separately, since a bare-assignment binding
        reads `undefined` before its write. `EffectModel.function_of` is the function-typed specialization
        of this query, and it is the value-resolution the bare-assignment recognition sites route through
        instead of re-deriving binding shapes.
        """
        channels, complete = self._binding_value_channels(binding)
        if not complete or len(channels) != 1:
            return None
        return channels[0][1]

    def binding_establishment_sites(
        self, binding: Binding | None, *, ignore_dynamic_rebinds: bool = False,
    ) -> list[Node] | None:
        """
        The nodes that must all have executed before *binding*'s `singular_value` is installed, for a
        consumer that gates a use on execution order. An empty list when the value is hoisted into
        place before any statement runs — a function declaration — so no ordering is required; the
        declaration when the value is a function a copy Annex B makes stores there (`AnnexBCopy`);
        the declarator when the value is a `var`/`let`/`const` initializer, which is absent until that
        declarator runs; the class declaration when the value is a class, which is in its temporal
        dead zone until it runs; the recorded write when a lone assignment installs it
        (`f = function(){}`, the form namespace flattening leaves). `None` when the binding holds no
        single such value, so its presence cannot be ordered and the caller declines — decided by the
        same complete-singleton `binding_values` answer `singular_value` requires, so the two queries
        can never disagree about which bindings have an orderable value: one returns the value and
        the other the node that establishes it. With *ignore_dynamic_rebinds* that answer is read on
        the view `binding_values` documents, the one a positioned consumer orders rebind hazards
        against itself. Ordering the returned nodes against the use is the caller's job, since that
        needs the dominance model this layer must not depend on.
        """
        if binding is None:
            return None
        channels, complete = self._binding_value_channels(
            binding, ignore_dynamic_rebinds=ignore_dynamic_rebinds)
        if not complete or len(channels) != 1:
            return None
        site, _ = channels[0]
        if isinstance(site, JsFunctionDeclaration):
            return [site] if _copy_received_by(binding, site) is not None else []
        return [site]

    def is_shadowed(self, name: str, at: Node, outer: Scope) -> bool:
        """
        Whether *name*, referenced at *at*, resolves to a binding declared strictly inside *outer*
        rather than in *outer* itself or an enclosing scope. This replaces the various hand-rolled
        shadowing checks: a name shadowed below *outer* does not refer to *outer*'s binding.
        """
        binding = None
        if isinstance(at, JsIdentifier) and at.name == name:
            binding = self._copy_written_at(at)
        if binding is None:
            binding = self.lookup(name, self._node_scope.get(at))
        if binding is None:
            return False
        return outer.contains(binding.scope, strict=True)

    def _copy_written_at(self, node: Node) -> Binding | None:
        """
        The `var` Annex B copies the function into where *node* is the name of a function declared
        in a block: the declaration writes that name as well as declaring its own.
        """
        if not isinstance(node, JsIdentifier) or (own := self._binding_of.get(node)) is None:
            return None
        copy = _copy_made_by(own, node.parent)
        return None if copy is None else copy.var

    def may_run_as_strict(self, node: Node) -> bool:
        """
        Whether the code at *node* may run as strict code: the file may be an ES module, which is
        strict code throughout whatever its text says, or the text makes it strict.
        """
        return self.file_kind.may_be_an_es_module or strict_mode_at(node)

    def annex_b_copy_observed(self, declaration: JsFunctionDeclaration) -> bool:
        """
        Whether anything may observe the copy Annex B makes of the function *declaration*, declared
        in a block, or the `var` that copy declares: the `var` is read, written, named in a `with`
        body, exported, carried by an object handed to a call, or reachable by reflection. A write
        observes the `var` because without it the same assignment lands on whatever the name means
        outside the function, or creates a global. A declaration this model was not built over may
        be observed.
        """
        if declaration.id is None:
            return False
        block = self.binding_of(declaration.id)
        if block is None:
            return True
        return any(
            copy.observed
            or copy.var.writes
            or copy.var.has_indefinite_write
            or self.reflection_can_reach(copy.var)
            for copy in block.copies_made
            if copy.declaration is declaration
        )

    def declaration_may_land(
        self,
        names: set[str],
        scope: Scope,
        *,
        exclude: Collection[Binding] = (),
    ) -> bool:
        """
        Whether declarations of *names* may be introduced directly in *scope*: none of them captures
        a use already meaningful there (`would_capture`), and none lands on a binding the host
        supplies (`Binding.supplied_by_host`), which no identifier of the text spells for that walk
        to find. At the top of a CommonJS file a `let` of a parameter's name makes Node refuse the
        file as CommonJS, and a `var` of it starts out holding what the host passed rather than
        `undefined`.

        None lands at the top of a script under a name a host reaches (`host_reaches_global`)
        either: there a `var` is the property of the global object the host calls, and a `let`
        shadows it, where the declaration it came from bound a name of its own body. A `Function`
        body declaring `var onload` and spliced at the top would install a load handler the window
        then calls once more.
        """
        if (
            scope is self.root_scope
            and not self.file_kind.scopes_declarations_to_the_file
            and any(self.host_reaches_global(name) for name in names)
        ):
            return False
        for name in names:
            binding = scope.bindings.get(name)
            if binding is not None and binding.supplied_by_host:
                return False
        return not self.would_capture(names, scope, exclude=exclude)

    def would_capture(
        self, names: set[str], scope: Scope, *, exclude: Collection[Binding] = (),
    ) -> bool:
        """
        Whether introducing a binding for any of *names* directly in *scope* would capture an
        identifier already meaningful there. Every use-position occurrence of one of *names* within
        *scope*, including in a nested function that would close over the new binding, must already
        resolve to a binding strictly nested below *scope* (see `is_shadowed`); otherwise that
        occurrence — free, inherited from an enclosing scope, or bound in *scope* itself — would be
        rebound by the introduced declaration.

        A use resolving to a binding in *exclude* is skipped: the edit the caller asks this for
        deletes that binding with the same splice that introduces the declaration, so the use is
        carried off rather than captured. The natural binding is what decides membership — a
        resolution walked past an excluded binding answers where the name would land *after* the
        edit, which is the question the callers below `_admit_reflected_body` ask of their free
        names, not the one this capture rule asks of a use the tree still holds.
        """
        return next(self._capturing_uses(names, scope, exclude), None) is not None

    def captured_names(
        self, names: set[str], scope: Scope, *, exclude: Collection[Binding] = (),
    ) -> set[str]:
        """
        Those of *names* for which `would_capture` holds, read in one walk of *scope*.
        """
        return {node.name for node in self._capturing_uses(names, scope, exclude)}

    def _capturing_uses(
        self, names: set[str], scope: Scope, exclude: Collection[Binding],
    ) -> Iterator[JsIdentifier]:
        for node in name_uses_in_scope(names, scope):
            binding = (
                self._copy_written_at(node)
                or self.lookup(node.name, self._node_scope.get(node))
            )
            if binding in exclude:
                continue
            if binding is None or not scope.contains(binding.scope, strict=True):
                yield node

    def has_reflection_surface(self) -> bool:
        """
        Whether the program still contains a construct through which code could reference a global
        by name at runtime: a value-read of the `eval` or `Function` intrinsic in any form — a
        direct or indirect call, an alias (`var e = eval`), a comma sequence (`(0, eval)`), or a
        member access (`window.eval`, `g['Function']`) — a string-valued timer, a dynamic property
        read on the global object (`window[expr]`), a `with` statement, or a span of source this
        model never read, which may spell a name nothing here records. Computed conservatively
        (over-reporting is safe): while any such surface remains, a dead global must not be removed,
        because reflective code may read it. A computed global *write* names no global
        (`has_opaque_global_write` owns that question) and is not counted here.
        """
        self._ensure_reflection_detected()
        assert self._reflection_surface is not None
        return self._reflection_surface

    def reached_without_its_name(self, function: Node) -> bool:
        """
        Whether *function* may be called, constructed or handed out where no reference of a name
        holding it stands: the program may read the `caller` of a function (`spells_caller`), which
        is whichever function called the one it is read on; the body may reach the function through
        its own `arguments` (`arguments_name_their_function`); or the body reads `new.target`
        (`references_new_target`), which is the function itself wherever `new` runs it. The one
        answer every reader asking how a function may be invoked or obtained shares, memoized per
        function.
        """
        cached = self._reached_unnamed.get(function)
        if cached is None:
            cached = self._reached_unnamed[function] = (
                self.spells_caller()
                or arguments_name_their_function(function)
                or references_new_target(function)
            )
        return cached

    def spells_caller(self) -> bool:
        """
        Whether the program may read the `caller` of a function: it spells the name as a property
        it reads or destructures, in a string, or as a name that no binding of the program holds,
        which a `with` body may resolve to a property. In sloppy mode `g.caller` is whichever
        function called `g`, so such a program can reach every function that calls another one
        without naming it. A variable named `caller` and a key of that name in an object literal or
        a class read no such property. Memoized.
        """
        if self._spells_caller is None:
            self._spells_caller = any(self._may_read_caller_at(node) for node in self.root.walk())
        return self._spells_caller

    def _may_read_caller_at(self, node: Node) -> bool:
        """
        Whether *node* may read the `caller` of a function, in the sense of `spells_caller`.
        """
        if isinstance(node, (JsStringLiteral, JsTemplateLiteral)):
            return string_spells(node, 'caller')
        if isinstance(node, JsMemberExpression) and node.computed:
            return key_spells(node, 'caller')
        if not isinstance(node, JsIdentifier) or node.name != 'caller':
            return False
        parent = node.parent
        if isinstance(parent, JsProperty) and isinstance(parent.parent, JsObjectPattern):
            return True
        if names_a_property(node):
            return isinstance(parent, JsMemberExpression)
        return node not in self._binding_of and self.resolve(node) is None

    def host_reaches_global(self, name: str) -> bool:
        """
        Whether a host reaches the property *name* of the global object from outside the file: the
        host *environment* calls the event handler stored there
        (`refinery.lib.scripts.js.analysis.environment.HostEnvironment.calls_handler`), or the
        analyst declared the name one it reaches (*host_entrypoint*).
        """
        if self.environment.calls_handler(name):
            return True
        return self._host_entrypoint is not None and self._host_entrypoint(name)

    def named_by_the_host(self, binding: Binding) -> bool:
        """
        Whether a host may reach *binding* by name: it reaches the property of the global object
        under that name (`host_reaches_global`), and the binding is that property
        (`reaches_global_object`), itself or through a `var` Annex B copies it into. The name alone
        does not decide it: a nested binding, a `let`, or anything a module scopes to the file never
        becomes a property of the global object. A declared object is reached as much as a declared
        function, and the host may call a method installed on it whenever the script hands it
        control.
        """
        if not self.host_reaches_global(binding.name):
            return False
        if any(self.reaches_global_object(copy.var) for copy in binding.copies_made):
            return True
        return self.reaches_global_object(binding)

    def reached_from_outside(self, binding: Binding) -> bool:
        """
        Whether code outside the file may reach *binding* by name at some point through the global
        object: a host names it (`named_by_the_host`), or the file is a library, holding nothing but
        function declarations at its top (`declares_only_functions`), and *binding* names one of
        them as a property of the global object, which code that loads the library calls once the
        file has run. An importer reaching an export is not among them: that is
        `Binding.exported`, which a caller asks beside this. A pass that could delete such a
        declaration, fold a read of one into its value, or relocate one out of the global scope
        asks here before doing so, so that the answer is one predicate's rather than each pass's
        own.
        """
        if self.named_by_the_host(binding):
            return True
        return (
            self.declares_only_functions()
            and self.reaches_global_object(binding)
            and any(
                isinstance(function := declaration.parent, JsFunctionDeclaration)
                and function.parent is self.root
                for declaration in binding.declarations
            )
        )

    def reached_without_a_read(self, binding: Binding) -> bool:
        """
        Whether code may reach *binding* where no read of it in the file stands: an importer reads
        it as an export, the call writes it on entry (`Binding.written_at_entry`), a body the file
        does not read is handed the object carrying it
        (`Binding.reachable_through_a_handed_object`), reflection reaches it
        (`reflection_can_reach`), or code outside the file reaches it by name
        (`reached_from_outside`). A reader asking whether the reads of a name are every way to what
        it holds asks here.
        """
        return (
            binding.exported
            or binding.written_at_entry
            or binding.reachable_through_a_handed_object
            or self.reflection_can_reach(binding)
            or self.reached_from_outside(binding)
        )

    def importer_may_run_first(self, binding: Binding) -> bool:
        """
        Whether *binding* is exported from a module an importer can run in before its body has
        finished (`module_may_be_reentered`), so the importer can read it at a point the file's
        text does not order.
        """
        return binding.exported and self.module_may_be_reentered()

    def callable_from_outside(self, function: Node) -> bool:
        """
        Whether code outside the file can call *function* while the file's own text still runs, at
        a point that text does not order: a host calls a function it names (`named_by_the_host`)
        whenever the script hands it control, and an importer in an import cycle calls an export,
        one exported by name or an anonymous `export default function`, before the module's body
        has finished (`importer_may_run_first`). The functions of a library
        (`reached_from_outside`) are not among them, and neither are the exports an importer
        outside every cycle reaches: code that loads the file calls those once it has run.
        """
        names = self.function_names(function)
        for binding in () if names is None else names.bindings:
            if self.importer_may_run_first(binding) or self.named_by_the_host(binding):
                return True
        return (
            isinstance(function.parent, JsExportDefaultDeclaration)
            and self.module_may_be_reentered()
        )

    def declares_only_functions(self) -> bool:
        """
        Whether the top level of the file holds function declarations and nothing else. Memoized.
        """
        if self._declares_only_functions is None:
            body = self.root.body
            self._declares_only_functions = bool(body) and all(
                isinstance(statement, JsFunctionDeclaration) for statement in body
            )
        return self._declares_only_functions

    def constructions_may_be_hidden(self) -> bool:
        """
        Whether a function the program constructs may be constructed with arguments no call the
        text shows passes it: a constructor may escape (`constructors_may_escape`), or the program
        constructs one without naming it (`_constructs_unseen`), through a `constructor` property or
        through `new.target`. Memoized.
        """
        if self._constructions_hidden is None:
            self._constructions_hidden = self.constructors_may_escape() or any(
                _constructs_unseen(node, self.assumed_keys) for node in self.root.walk()
            )
        return self._constructions_hidden

    def _extends_beyond_the_program(self, cls: JsClassDeclaration | JsClassExpression) -> bool:
        """
        Whether the class *cls* inherits from a constructor the program does not declare, a built-in
        such as `Array`, `Promise` or `RegExp` among them, whose methods construct a new instance of
        the class an instance has (`Symbol.species`) with arguments of their own: its `extends`
        clause, followed through the classes of the program it names, ends at anything but a class
        without one or a function of the program.
        """
        seen: set[int] = set()
        while id(cls) not in seen:
            seen.add(id(cls))
            heritage = strip_parens(cls.super_class)
            if heritage is None:
                return False
            if not isinstance(heritage, JsIdentifier):
                return True
            binding = self.resolve(heritage)
            if binding is None:
                return True
            value = strip_parens(self.singular_value(binding))
            if isinstance(value, (JsFunctionDeclaration, JsFunctionExpression)):
                return False
            if not isinstance(value, (JsClassDeclaration, JsClassExpression)):
                return True
            cls = value
        return True

    def constructors_may_escape(self) -> bool:
        """
        Whether a constructor may run on a receiver or with arguments of the program's choice: the
        program spells `"constructor"` in a string or in a concatenation of constant strings, reads
        a `constructor` property other than to construct with it, compare it, read its name or test
        it, or destructures one. An instance reaches the function that built it through that
        property, and so does its prototype. Memoized.
        """
        if self._constructors_escape is None:
            concatenations: list[JsBinaryExpression] = []
            escapes = False
            for node in self.root.walk():
                if isinstance(node, JsBinaryExpression):
                    if node.operator == '+':
                        concatenations.append(node)
                elif _lets_a_constructor_escape(node):
                    escapes = True
                    break
            self._constructors_escape = escapes or concatenation_spells(concatenations, 'constructor')
        return self._constructors_escape

    def strings_still_unseen(self) -> frozenset[str]:
        """
        The names of `refinery.lib.scripts.js.keys.STRINGS_READ_ANYWHERE` whose readers answer no
        for this program, so that a string spelling one anywhere would change an answer: one
        spelling `constructor` lets a constructor escape (`constructors_may_escape`), and one
        spelling `caller` may read the function that called another (`spells_caller`).
        """
        unseen: set[str] = set()
        if not self.constructors_may_escape():
            unseen.add('constructor')
        if not self.spells_caller():
            unseen.add('caller')
        return frozenset(unseen)

    def has_opaque_global_write(self) -> bool:
        """
        Whether the program stores a property on the global object under a key only the runtime
        resolves (`window[expr] = x`), or installs one there under such a key
        (`opaque_global_write`), so an intrinsic or a script-scope name may hold something else
        than what the text spells once the program runs. The read-naming question
        `has_reflection_surface` answers is unaffected by such a write — storing a property names
        nothing and runs nothing — but the *replacement* questions are not: a written key may be
        `Math`, `String`, or the name a top-level `var` carries, so a consumer that trusts an
        intrinsic by name, or that a script-scope binding keeps its spelled value, refuses while
        this holds.

        Detection is model-aware, distinct from the spelling-level exemption
        `_is_reflective_member` grants the same sites: a base may be the global object here
        (`may_be_the_global_object`), so a local holding the object (`var g = globalThis;
        g[k] = 1`) and the receiver a sloppy call supplies a write through (`this[k] = 1`) are
        both counted, not only its spelled names — the alias would otherwise store a global under
        a key the spelling never saw, and the receiver is the one spelling a callee can choose
        freely. Every store form counts
        (`is_member_write_target`: plain and compound assignment, update, `delete`, `for-in`/`for-of`
        heads, destructuring patterns), so the fact stands on its own wherever a consumer consults it.
        Computed lazily and memoized,
        but never while `_build_def_use` is still recording references: which member accesses reach
        the global object depends on the names holding it, which that step finds from the writes it
        records, so an answer taken inside it would depend on how far it had got; asked there, the
        conservative `True` is answered instead of a partial fact.
        """
        if self._recording_def_use:
            return True
        if not self._opaque_global_write_sites_known:
            self._compute_opaque_global_write_sites()
        return self._opaque_global_write is True

    def _compute_opaque_global_write_sites(self) -> None:
        sites = [
            member for member in self.root.walk()
            if isinstance(member, JsMemberExpression)
            and opaque_global_write(member, self.may_be_the_global_object)
        ]
        self._opaque_global_write_sites = sites
        self._opaque_global_write_sites_known = True
        if sites and self._opaque_global_write is None:
            self._opaque_global_write = True

    def opaque_global_write_sites(self) -> list[JsMemberExpression] | None:
        """
        The member expressions storing a property on the global object under a key only the runtime
        resolves — the located form of the fact `has_opaque_global_write` reports, for a consumer
        that orders the fact's consequences rather than refusing on it. `None` when the fact holds
        without a site to order: an observed hand-over of the object to a callee that may write it,
        or an answer taken while the reference-recording walk is still running, where the boolean
        answers `True` for the same reason. A consumer that turns sites into kills treats `None` as
        volatility it cannot locate.
        """
        if self._recording_def_use:
            return None
        if not self._opaque_global_write_sites_known:
            self._compute_opaque_global_write_sites()
        sites = self._opaque_global_write_sites
        assert sites is not None
        if sites or self._opaque_global_write is not True:
            return sites
        return None

    def opaque_global_write_replacement_sites(self, binding: Binding) -> list[JsMemberExpression] | None:
        """
        The opaque global writes that could replace the value *binding* holds, or `None` when that
        question cannot be answered in sites. Only a script-scope name is replaceable — it is a
        property of the global object under the script execution model the write stores to — so a
        binding in any other scope answers `None`: its reflection hazards, if any, are not this
        write's. A script-scope name under a reflection surface answers `None` too, since any surface
        could write the name from anywhere, and so does the fact when it holds without a site
        (`opaque_global_write_sites`). `None` therefore means the binding is reflection-reachable for
        reasons no located site spells, and a consumer that turned sites into kills keeps the value
        volatile instead.
        """
        owner = binding.scope.var_scope
        if owner is None or self._named_by_global_code(binding):
            if self.has_reflection_surface():
                return None
            return self.opaque_global_write_sites()
        return None

    def _named_by_global_code(self, binding: Binding) -> bool:
        """
        Whether code running in the global scope, which every reflective surface but a direct `eval`
        runs, can name *binding*: an implicit global, and where the file is a script, every binding
        of its top scope. In a module of either kind a top-level declaration is scoped to the file,
        but a binding of its top scope that may be absent leaves its name to a global in the reading
        without it (`Binding.may_be_absent`).
        """
        owner = binding.scope.var_scope
        if owner is None or owner.kind is not ScopeKind.SCRIPT:
            return False
        return (
            binding.kind is BindingKind.IMPLICIT_GLOBAL
            or binding.may_be_absent
            or not self.file_kind.scopes_declarations_to_the_file
        )

    def _named_by_code_of_its_function(self, binding: Binding) -> bool:
        """
        Whether *binding* belongs to its function, or to the function a CommonJS host wraps around
        the file, in some way the file may run, so that a direct `eval` there can name it: every
        binding global code alone names is left out (`_named_by_global_code`). A binding of the top
        scope that may be absent is both, the file's own where it exists and a global's name where
        it does not (`Binding.may_be_absent`).
        """
        return binding.may_be_absent or not self._named_by_global_code(binding)

    def reflection_can_reach(self, binding: Binding) -> bool:
        """
        Whether a runtime name lookup could read or write *binding* without a reference this model
        records. Derived over the precise dynamic-scope facts. A global is reachable through any
        reflective surface — `eval`, `Function`, a string timer, dynamic global access, `with` — all
        of which run in the global scope, so it defers to the whole-program
        `has_reflection_surface`, and by an opaque global write rebinding its name
        (`has_opaque_global_write`), which no reference records either. A function-local is
        reachable only from within its own function and only by name: a `with` body that names
        it (a `dynamic_references` entry), a direct `eval` in the function
        (`local_reachable_by_direct_eval`), or a span of the function this model never read
        (`unread_source_can_reach`), which may spell the name where nothing records that it does.
        A `with` that never names it cannot reach it, and reflective code in the global scope
        cannot name a local — so the local answer is exact, while the global one stays
        conservative (any surface).

        Where the file is a module of either kind a declaration of the top-level scope is scoped to
        the file: global-scope code cannot name it, so it is reached the way a function-local is,
        from the file's own text only (`reflection_can_reach_from_its_scope`). An implicit global
        stays a global.
        """
        if self._named_by_global_code(binding):
            return self.has_reflection_surface() or self.has_opaque_global_write()
        return self.reflection_can_reach_from_its_scope(binding)

    def reflection_can_reach_from_its_scope(self, binding: Binding) -> bool:
        """
        Whether code standing in the scope of *binding* that this model cannot read could name it: a
        `with` body that names it, a direct `eval` in its function, or a span of that function this
        model never read. The scope of an implicit global is the global scope, where every
        reflective surface runs. Code in the global scope outside the file is not asked about, which
        is the whole question for a binding scoped to a function or to a module and one half of it
        for a declaration of a script's top scope (`reflection_can_reach`).
        """
        owner = binding.scope.var_scope
        if owner is None or binding.kind is BindingKind.IMPLICIT_GLOBAL:
            return self.has_reflection_surface() or self.has_opaque_global_write()
        return (
            bool(binding.dynamic_refs)
            or self._function_has_direct_eval(owner.node)
            or bool(self._unread_source_sites(owner.node))
        )

    def reachable_by_opaque_reflection(self, binding: Binding) -> bool:
        """
        Whether an opaque reflective surface — a value-read of `eval` or `Function`, a string timer,
        a dynamic access on the global object, or a span of source this model never read — could
        name *binding* at runtime with no reference this model records. Unlike
        `reflection_can_reach`, a `with` body is not counted: a `with` that names the binding is
        attributed precisely as a `dynamic_references` entry, so a caller that already consults
        `dynamic_refs` needs only the opaque surfaces here, the ones that leave no attributable
        reference. A global is reachable through any such surface, all of which run in the global
        scope; a function-local only through a direct `eval` or an unread span in its own function,
        since a surface running in the global scope cannot name a local. The boolean companion of
        `reflection_surface_sites` — true exactly when that site list is non-empty.
        """
        return bool(self.reflection_surface_sites(binding))

    def reflection_surface_sites(self, binding: Binding) -> list[Node]:
        """
        The AST nodes of the opaque reflective surfaces that could name *binding* at runtime with no
        reference this model records — the points no reflected invocation of it can precede. A
        caller ranks a definition against these to prove it runs before every such invocation, the
        site-level companion of `reachable_by_opaque_reflection`. For a global (script-scope)
        binding they are the whole-program opaque surfaces (`opaque_reflection_sites`), each
        running in the global scope and able to name any global; for a function-local, the direct
        `eval` sites in its owning function (`_direct_eval_sites`) and the spans of that function
        this model never read (`_unread_source_sites`), the only opaque surfaces that stand in the
        local's own scope and can name it. Empty exactly when the binding is not opaque-reflection
        reachable. A `with` surface is not included — a `with` that names the binding is attributed
        as a `dynamic_references` entry a caller consults separately.
        """
        owner = binding.scope.var_scope
        if owner is None or self._named_by_global_code(binding):
            return self.opaque_reflection_sites()
        return self._direct_eval_sites(owner.node) + self._unread_source_sites(owner.node)

    def local_reachable_by_direct_eval(self, binding: Binding) -> bool:
        """
        Whether a direct `eval` positioned to name *binding* could read or write it with no reference this
        model records. True only for a function-local whose owning function — or a closure nested inside
        it, which inherits its scope — contains a direct `eval`, the one reflective surface that runs in
        the caller's own scope and can therefore name a local. False for a global: an opaque global-scope
        surface can name any global, but that is what the whole-program `reflection_can_reach` answers, and
        freezing every global on it is an over-approximation the caller must choose to accept, not a fact
        this query asserts. The `with` surface is not counted — a `with` body's accesses are attributed
        precisely as `dynamic_references`, so only the opaque `eval` case needs this per-function answer.
        A binding of the top scope that may be absent is a local of the CommonJS wrapper where it
        exists, and counts as one (`_named_by_code_of_its_function`).
        """
        owner = binding.scope.var_scope
        if owner is None or not self._named_by_code_of_its_function(binding):
            return False
        return self._function_has_direct_eval(owner.node)

    def unread_source_can_reach(self, binding: Binding) -> bool:
        """
        Whether a span of source this model never read stands where it could name *binding*. Such a
        span is text the file holds at a definite position, and nothing says what it references, so
        everything in scope where it stands may be read or written by it with no reference this
        model records. A binding of the script is within reach of every span in the file; a
        function-local only of one inside its own function, since no span outside it can name a
        local.

        This is not folded into the `eval` answers, even though both surfaces are opaque, because
        the two are known to different degrees. Whether an `eval` anywhere in a file rebinds a given
        global is a question about text no one has, and freezing every global on it is the
        over-approximation `local_reachable_by_direct_eval` documents as refused; an unread span is
        the file's own text, standing in one place, and refusing to count it is what drops the write
        that text spells.
        """
        owner = binding.scope.var_scope
        if owner is None or self._named_by_global_code(binding):
            return bool(self._unread_source_sites(self.root))
        return bool(self._unread_source_sites(owner.node))

    def free_name_reachable_by_direct_eval(self, node: Node) -> bool:
        """
        Whether a direct `eval` could have installed a binding that a free name at *node* reads instead
        of the global one. `resolve` answering `None` means this model saw no declaration of the name,
        which is not the same as there being none: `eval('var undefined = 4')` declares one that no
        reference here records, and a read of that name afterwards is the binding, not the global.

        Only `var` and function declarations escape an `eval` — a `let` inside one lives in a scope
        discarded with the call — so a binding it installs lands in the var scope the call itself stands
        in, and is visible at *node* exactly when that var scope contains *node*'s scope. This is the
        mirror of `local_reachable_by_direct_eval`, which asks whether an `eval` can name a binding that
        already exists and therefore counts one nested *below* the binding's owner; a nested `eval`
        declares into its own function and so is not counted here.

        An `eval` whose own argument contains *node* is excluded, and that exclusion is about order
        rather than scope: the arguments of a call are evaluated before the call runs, so the code the
        `eval` is about to execute cannot have declared anything the argument reads. Without it,
        `eval(atob('...'))` — the shape most of this tool's corpus is written in — would refuse to read
        `atob` on the strength of the very `eval` it is decoding the body of.
        """
        scope = self.scope_of(node)
        if scope is None:
            return True
        return any(
            owner is None or owner.contains(scope)
            for owner in self._var_scopes_of_evals_before(node)
        )

    def direct_eval_declares_below(self, node: Node, outer: Scope) -> bool:
        """
        Whether a direct `eval` could have installed a `var` that a name written at *node* finds
        before its lookup reaches the scope *outer*: one whose var scope contains the scope of
        *node* and lies strictly inside *outer*. The bounded form of
        `free_name_reachable_by_direct_eval`, for a caller about to declare the name in *outer*.
        """
        scope = self.scope_of(node)
        if scope is None:
            return True
        return any(
            owner is None or owner.contains(scope) and outer.contains(owner, strict=True)
            for owner in self._var_scopes_of_evals_before(node)
        )

    def _var_scopes_of_evals_before(self, node: Node) -> Iterator[Scope | None]:
        """
        The var scope of every direct `eval` that can have run before *node* is evaluated, or `None`
        where the scope of one is unknown: all of them but those whose own argument holds *node*,
        which is evaluated before its `eval` runs (`free_name_reachable_by_direct_eval`).
        """
        enclosing = {id(node)}
        cursor = node.parent
        while cursor is not None:
            enclosing.add(id(cursor))
            cursor = cursor.parent
        for site in self._direct_eval_sites(self.root):
            if any(id(argument) in enclosing for argument in getattr(site, 'arguments', ())):
                continue
            site_scope = self.scope_of(site)
            yield site_scope.var_scope if site_scope is not None else None

    def bare_name_reaches_the_host(self, name: str, at: Node) -> bool:
        """
        Whether a bare *name* read at *at* denotes what the host supplies under that name: nothing
        binds it where *at* stands, not even an assignment the model records as an
        `IMPLICIT_GLOBAL`, no `with` body between *at* and the global scope may supply it from its
        object, and no direct `eval` may have declared it (`free_name_reachable_by_direct_eval`).
        *at* need not be an identifier, so a pass can ask before it writes the name there, as one
        that rewrites a `this` to `globalThis` must.
        """
        scope = self.scope_of(at)
        return (
            scope is not None
            and self.lookup(name, scope) is None
            and not crosses_dynamic_scope(scope)
            and not self.free_name_reachable_by_direct_eval(at)
        )

    def binding_dynamic_rebind_sites(self, binding: Binding) -> list[Node] | None:
        """
        The AST nodes at which a dynamic scope could rebind *binding*, or `None` for the one such
        rebind that holds no node to order — the write a call makes on entry (`written_at_entry`),
        which the text does not spell. The located form of
        `binding_maybe_reassigned_dynamically`, which is re-derived from this answer, so the two
        can never disagree about which bindings are volatile: a consumer that gets a list holds a
        hazard per node, and one that gets `None` holds the nodeless kill. Each leg of the boolean
        contributes its nodes — a write through an object that aliases the binding
        (`indefinite_writes`), a `with`-body reference whose role is not a plain read, a direct
        `eval` in the owning function, and a span of source the model never read — with the two
        scope lines the boolean draws drawn identically: the eval leg is a function-local's only
        (a global is not frozen on a global-scope surface, and a binding of the top scope that may
        be absent is a local of the CommonJS wrapper where it exists), and the unread-source leg
        reaches a global from anywhere in the file but a local only from its own function. An
        empty list is a binding no dynamic scope can rebind. Memoized per binding once the model is
        built: while it is, the references made through objects aliasing a binding are still being
        recorded, and an answer taken then would depend on how far that had got.
        """
        if binding in self._dynamic_rebind_sites:
            return self._dynamic_rebind_sites[binding]
        sites = self._dynamic_rebind_sites_of(binding)
        if self._built:
            self._dynamic_rebind_sites[binding] = sites
        return sites

    def _dynamic_rebind_sites_of(self, binding: Binding) -> list[Node] | None:
        if binding.written_at_entry:
            return None
        owner = binding.scope.var_scope
        sites = list(binding.indefinite_writes)
        if owner is not None and self._named_by_code_of_its_function(binding):
            sites.extend(self._direct_eval_sites(owner.node))
        if owner is None or self._named_by_global_code(binding):
            sites.extend(self._unread_source_sites(self.root))
        else:
            sites.extend(self._unread_source_sites(owner.node))
        sites.extend(
            ref for ref in self.dynamic_references(binding)
            if reference_role(ref) is not Role.READ
        )
        return sites

    def binding_maybe_reassigned_dynamically(self, binding: Binding) -> bool:
        """
        Whether a dynamic scope could rebind *binding* — give the name a new value through a surface
        the static `writes` set does not record. A `with` body that names it as an assignment target
        may rebind it (the target may instead be a property of the `with` object, but may equally be
        this binding, so it is treated as a possible rebind), a direct `eval` in its owning function
        can rebind it opaquely, and so can a span of source this model never read
        (`unread_source_can_reach`), whose text may spell an assignment to the name. A member write
        or method call through the name does not rebind it — the name keeps its value — so only a
        dynamic reference whose role is not a plain read counts. A write through an object that
        aliases the binding — `indefinite_writes` — is counted here too: it replaces the value under
        the name while leaving no entry that says with what. A consumer that judges a binding's
        value stable from `writes` alone must also consult this, since none of these reassignments
        leaves a `writes` entry; a script-scope binding reassigned only through an opaque `eval`
        stays the documented residual, as `local_reachable_by_direct_eval` reports it false there.
        The boolean form of `binding_dynamic_rebind_sites`: true exactly when that answer is
        `None` or holds a node.
        """
        sites = self.binding_dynamic_rebind_sites(binding)
        return sites is None or bool(sites)

    def binding_reflection_kill_sites(self, binding: Binding) -> list[Node] | None:
        """
        The AST nodes at which a reflective surface could change the value *binding* holds — the
        located kills a flow query orders a read against — or `None` when the question has no
        located answer.

        A script-scope binding is replaceable only by an opaque global write, and the existing
        `opaque_global_write_replacement_sites` answer is the whole of it: `None` under any
        whole-program reflective surface, which could write the name from anywhere, and the member
        sites otherwise. A function-local can change value only through a surface standing in its
        own scope: a direct `eval` in its owning function or a span of that function this model
        never read (`reflection_surface_sites`), or a reference a `with` body resolves at runtime
        (`dynamic_references`). Every `with`-governed reference counts, read or not — reading the
        bare name consults the `with` object first, and a getter there runs code that can rebind
        the local — so only a located answer keeps that hazard orderable rather than refusing on
        it. Each site is a node a consumer that has ordered a definition against a use can also
        order the kill against; `None` is the volatility it cannot.
        """
        owner = binding.scope.var_scope
        if owner is None or self._named_by_global_code(binding):
            return self.opaque_global_write_replacement_sites(binding)
        return self.reflection_surface_sites(binding) + self.dynamic_references(binding)

    def holds_what_the_host_passed(self, binding: Binding) -> bool:
        """
        Whether *binding* is supplied by the host and still holds what the host passed wherever it
        is read: nothing in the file declares or writes it, and no direct `eval` or span of source
        this model never read stands where it could. Where the ES-module reading leaves the name to
        a global, no reflective surface may reach it either.
        """
        return (
            binding.supplied_by_host
            and not binding.declarations
            and not binding.writes
            and self.binding_dynamic_rebind_sites(binding) == []
            and not (binding.may_be_absent and self.reflection_can_reach(binding))
        )

    def module_exports_unread(self) -> bool:
        """
        Whether nothing reads what the file stores in `module.exports`. Outside the file, only code
        that loads it reads its exports, and the run assumes none does unless told otherwise
        (`exports_read_outside`). Node's module registry is part of loading: a file reaching itself
        through `require.cache`, `require.main`, `process.mainModule` or a `require` or `import()`
        of its own path reads its exports as a loader does. Inside the file, the object the store
        replaces is read through the `module` the host passed, which is therefore named only as the
        target of a store `module.exports = v`, as the operand of `typeof`, or compared with `null`,
        and through the `arguments` of the function the host wraps around the file, which the file
        does not name, nor reaches through a function's `caller`. Memoized.
        """
        if self._module_exports_unread is None:
            self._module_exports_unread = self._compute_module_exports_unread()
        return self._module_exports_unread

    def _compute_module_exports_unread(self) -> bool:
        if self.exports_read_outside or not self.file_kind.may_be_commonjs:
            return False
        bindings = self.root_scope.bindings
        module = bindings.get('module')
        if module is None or not self.holds_what_the_host_passed(module):
            return False
        if self.reflection_can_reach(module) or self.local_reachable_by_direct_eval(module):
            return False
        if self.dynamic_references(module):
            return False
        if not all(
            _tests_or_replaces_the_exports(reference)
            for reference in self.references(module)
        ):
            return False
        arguments = bindings.get('arguments')
        if arguments is not None and arguments.supplied_by_host and any(True for _ in self.references(arguments)):
            return False
        return not self.spells_caller()

    def supplied_from_outside(self, binding: Binding | None) -> bool:
        """
        Whether a name resolving to *binding* holds something the file does not supply: it resolves
        to no binding, so to a global the host may define, or to one the host passed and the file
        never replaces (`holds_what_the_host_passed`). A callee read through such a name runs
        nothing the file declares.
        """
        return binding is None or self.holds_what_the_host_passed(binding)

    def binding_never_reassigned(self, binding: Binding) -> bool:
        """
        Whether *binding* holds one value for its whole lifetime: it is never written after its
        declaration, statically (`writes`) or through a dynamic scope
        (`binding_maybe_reassigned_dynamically`). This is the value-stability contract a caller needs
        before treating the binding's initializer as its value everywhere — distinct from the
        orderability contract `dynamic_refs` expresses (whether every reference can be ranked), which a
        `with`-body read violates while a stable value does not. It does not itself require a single
        declaration; a caller that needs one checks `declarations` alongside.
        """
        return not binding.writes and not self.binding_maybe_reassigned_dynamically(binding)

    def reaches_global_object(self, binding: Binding) -> bool:
        """
        Whether *binding* is a property of the global object at runtime — the global a free name in
        global-scope reflected code (a `Function` body, an indirect `eval`, a string timer) resolves to.
        An implicit global always is. A top-level `var`/function declaration is where the file is a
        script; in a module of either kind it is scoped to the file and never reaches the global. A
        top-level `let`/`const`/`class`, or any binding nested below the script, is a distinct
        lexical binding that global-scope code cannot see.
        """
        if binding.kind is BindingKind.IMPLICIT_GLOBAL:
            return True
        if self.file_kind.scopes_declarations_to_the_file:
            return False
        return (
            binding.scope is self.root_scope
            and binding.is_hoisted
        )

    def module_may_be_reentered(self) -> bool:
        """
        Whether code of another module can run after this module is linked and before its body has
        finished (`_lets_another_module_run`). A module in an import cycle with this one can then
        call an exported function before the declarations after its definition have run. A module
        that imports nothing is in no cycle and runs its whole body before any importer does, so
        every importer calls into it after its last statement.
        """
        return self._module_may_be_reentered

    def _direct_eval_sites(self, function: Node) -> list[Node]:
        """
        The direct `eval` call sites within *function* — every call whose callee, once parentheses are
        stripped, is the bare identifier `eval` (see `is_direct_eval_call`), the one reflective surface
        that runs in the function's own scope and can therefore name its locals. Nested functions are
        included, since a direct `eval` in a closure inherits the enclosing locals. The `with` surface is
        not scanned — a `with` body's accesses are attributed precisely as dynamic references — so only
        direct eval needs a per-function answer. Filed for every scope owning variables while the
        model is built (`_record_def_use_references`), and computed once and memoized for any other
        node.

        The trusting model (`trust_eval`) answers no sites: a direct `eval` whose argument cannot be
        resolved is the one surface that model assumes inert, so every consumer that freezes a local
        on it — the rebind hazards, the reflection surfaces, the install question a free name asks —
        loses the eval leg alone, and the other legs a `with` body or an unread span carries keep
        refusing.
        """
        if self.trust_eval:
            return []
        cached = self._function_direct_eval_sites.get(id(function))
        if cached is None:
            cached = [node for node in function.walk() if is_direct_eval_call(node)]
            self._function_direct_eval_sites[id(function)] = cached
        return cached

    def _function_has_direct_eval(self, function: Node) -> bool:
        return bool(self._direct_eval_sites(function))

    def _unread_source_sites(self, function: Node) -> list[Node]:
        """
        The spans within *function* that this model never read — text the parser could not read, and
        a construct the file ended inside (see `is_unread_source`). Each stands where the function's
        own locals are in scope and says nothing about what it references, so it can name any of
        them with no reference this model records, exactly as a direct `eval` can. Nested functions
        are included, since a span inside one names the enclosing locals too. Filed and memoized the
        way `_direct_eval_sites` is.
        """
        cached = self._function_unread_source_sites.get(id(function))
        if cached is None:
            cached = [node for node in function.walk() if is_unread_source(node)]
            self._function_unread_source_sites[id(function)] = cached
        return cached

    def _reads_reflective_intrinsic(self, node: JsIdentifier) -> bool:
        """
        Whether *node* obtains the genuine `eval`/`Function` intrinsic as a value: a read of the bare name
        in a use position that resolves to no binding, so it denotes the intrinsic rather than a local
        shadow. Naming the intrinsic as a value is itself the reflective surface — once obtained it can be
        aliased, sequenced (`(0, eval)(...)`), or passed on, all beyond what this model tracks — so the read
        alone is conclusive, with no need to follow where the value flows. A binding site that declares the
        name (`function eval(){}`, `var Function`) introduces a shadow rather than reading the intrinsic,
        and a name that resolves to such a shadow is not the intrinsic, so neither is a surface.
        """
        if node.name not in REFLECTIVE_INTRINSICS:
            return False
        if not self.is_reference(node):
            return False
        if reference_role(node) is not Role.READ:
            return False
        return self.lookup(node.name, self._node_scope.get(node)) is None

    def _reads_a_wrapper_name_as_global_code(self, node: JsIdentifier) -> bool:
        """
        Whether the read of `eval` at *node* is the callee of an indirect `eval` reading a name the
        CommonJS wrapper binds as global code, `(0, eval)('name')` or `(0, eval)('typeof name')`:
        the spelling into which the reflection pass turns such a read of a packed body, which global
        code reads as the global object's property of that name, or as no name at all. The call does
        nothing else, so it reaches nothing the text does not show, and it is no reflective surface.
        The name has to be one the host binds around the file
        (`Binding.supplied_by_host`) that global code does not see and that no access through the
        global object reaches: under a module reading, a store `globalThis.name = v` is recorded
        against the file's binding of the name, and the call would read what it stored. Any other
        name read this way may be the global object or an intrinsic, whose value goes on unseen.
        """
        if node.name != 'eval':
            return False
        sequence = node.parent
        if not isinstance(sequence, JsSequenceExpression) or sequence.expressions[-1] is not node:
            return False
        if not all(isinstance(e, (JsNumericLiteral, JsStringLiteral)) for e in sequence.expressions[:-1]):
            return False
        callee: Node = sequence
        while isinstance(callee.parent, JsParenthesizedExpression):
            callee = callee.parent
        call = callee.parent
        if not isinstance(call, JsCallExpression) or call.callee is not callee or len(call.arguments) != 1:
            return False
        text = call.arguments[0]
        if not isinstance(text, JsStringLiteral) or text.value is None:
            return False
        name = text.value.removeprefix('typeof ')
        if not is_identifier_name(name) or name in RESERVED_WORD_NAMES:
            return False
        binding = self.root_scope.bindings.get(name)
        return (
            binding is not None
            and binding.supplied_by_host
            and not self.reaches_global_object(binding)
            and not binding.indefinite_writes
            and not binding.reachable_through_a_handed_object
            and all(isinstance(reference, JsIdentifier) for reference in self.references(binding))
        )

    def code_load_sites(self) -> list[Node]:
        """
        The calls by which the program has Node's `require` load code its text does not hold
        (`_loads_code_from_outside`): a file, a package, or a built-in module that runs code. What
        such code does is unknown; what a loaded module commonly does is set up globals, such as a
        `window` for code written for a page or an AMD `define`. A question that orders a point
        against code the text does not hold takes these as sites
        (`refinery.lib.scripts.js.analysis.tampering.TamperingModel`, and the `typeof` of a global
        name through
        `refinery.lib.scripts.js.analysis.tampering.TamperingModel.code_load_may_precede`). They
        are no reflection surface: that would withdraw every trust the whole program is asked for
        from a file that holds one in a function it never calls. Memoized.
        """
        if self._code_load_sites is None:
            self._code_load_sites = [
                site for node in self.root.walk()
                if isinstance(node, (JsIdentifier, JsMemberExpression))
                and (site := self._loads_code_from_outside(node)) is not None
            ]
        return self._code_load_sites

    def imports_code(self) -> bool:
        """
        Whether the file has the host load code its text does not hold before its body runs: an
        `import` or an `export … from` declaration (`_lets_another_module_run`) naming a module
        that is not a built-in one running no code (`_specifier_loads_code`). The host links and
        evaluates every such module before the first statement of the file, wherever the
        declaration stands, so no point of the file is ordered before the load.
        """
        for statement in self.root.body:
            if not isinstance(statement, (JsImportDeclaration, JsExportAllDeclaration, JsExportNamedDeclaration)):
                continue
            if not _lets_another_module_run(statement):
                continue
            source = statement.source
            if self._specifier_loads_code(None if source is None else source.value):
                return True
        return False

    def _loads_code_from_outside(self, node: JsIdentifier | JsMemberExpression) -> Node | None:
        """
        The call `require(m)` *node* is the callee of, where it calls the host's `require` and may
        load code the text does not hold: *m* is not a string naming a built-in module that runs no
        code (`_specifier_loads_code`). The callee is the host's `require` where it is the name
        `require` resolving to no binding or to the one the host passed that the file does not
        declare or assign, or the member `module.require` of such a `module`, which is the same
        function. `None` where *node* is no such callee. A `require` read for anything but a call
        is not followed: real programs hold it under another name (`global.r = require`) and call
        it with built-in names, and a site at every such read would order nothing after it.
        """
        if isinstance(node, JsMemberExpression):
            base = strip_parens(node.object)
            if (
                not isinstance(base, JsIdentifier)
                or base.name != 'module'
                or not self._may_hold_what_the_host_passed(base)
                or not key_is(node, _REQUIRE, self.assumed_keys)
            ):
                return None
        elif node.name != 'require' or not self.is_reference(node) or not self._may_hold_what_the_host_passed(node):
            return None
        call = enclosing_operator(node)
        if not isinstance(call, JsCallExpression) or strip_parens(call.callee) is not node:
            return None
        arguments = call.arguments
        if len(arguments) == 1 and isinstance(module := strip_parens(arguments[0]), JsStringLiteral):
            if not self._specifier_loads_code(module.value):
                return None
        return call

    def _may_hold_what_the_host_passed(self, name: JsIdentifier) -> bool:
        """
        Whether the name *name* may read what the CommonJS host passed under it: it resolves to no
        binding, or to the one the host passed that the file does not declare or assign.
        """
        binding = self.resolve(name)
        return binding is None or (binding.supplied_by_host and not binding.declarations and not binding.writes)

    def _specifier_loads_code(self, specifier: str | None) -> bool:
        """
        Whether loading the module *specifier* names runs code the file's text does not hold: it is
        not a built-in module that runs none (`node_require_runs_no_code`), nor `vm` under the
        trusting model, which assumes the code it runs inert since the program hands it as data.
        """
        if specifier is None:
            return True
        if node_require_runs_no_code(specifier):
            return False
        return not (self.trust_eval and specifier in ('vm', 'node:vm'))

    def _computed_read_aliases_a_global(self, member: JsMemberExpression) -> bool:
        """
        Whether *member* reads an unknown global under a runtime key through a name the file gives
        the global object — `g[k]` where `var g = globalThis`. `_is_reflective_member` recognizes
        the same read on a base spelled as the global object (`globalThis[k]`); an alias holds the
        object under another name, so a read of it under a key only the runtime resolves names any
        global just as the spelled base does, and is the same surface. A plain write (`g[k] = x`)
        stores a property and reads nothing, and is left to `has_opaque_global_write` as on the
        spelled base.
        """
        if key_text(member) is not None or is_simple_assignment_target(member):
            return False
        return self.names_the_global_object(strip_parens(member.object))

    def _ensure_reflection_detected(self) -> None:
        """
        Populate the reflection-surface memos in a single AST walk. A `with` statement contributes
        only to the whole-program surface; every other surface — a span of source this model never
        read, an `import()`, a value-read of the `eval`/`Function` intrinsic, a reflective
        global-object member, or a string-valued timer — is opaque, and its node is collected so a
        caller can order a definition against the site. The whole-program surface is present when
        any opaque site exists or a `with` statement is seen.

        The unread span is tested before the shapes are, because a construct the file ended inside
        is one of those shapes: an unterminated call is a call, and what matters about it is the
        source that never followed it rather than what it would compute.

        The trusting model (`trust_eval`) declines to count the surfaces of the kinds it assumes
        inert — the question `_surface_is_trusted` answers per node — so the whole-program surface
        and the site list lose exactly those and keep the rest.
        """
        if self._reflection_surface is not None:
            return
        sites: list[Node] = []
        saw_with = False
        for node in self.root.walk():
            if is_unread_source(node):
                sites.append(node)
            elif isinstance(node, JsWithStatement):
                saw_with = True
            elif isinstance(node, JsImportExpression):
                sites.append(node)
            elif isinstance(node, JsIdentifier):
                if (
                    self._reads_reflective_intrinsic(node)
                    and not self._reads_a_wrapper_name_as_global_code(node)
                    and not self._surface_is_trusted(node)
                ):
                    sites.append(node)
            elif isinstance(node, JsMemberExpression):
                surface = (
                    _is_reflective_member(node, self.assumed_keys, self._trusted_surface_keys())
                    or self._computed_read_aliases_a_global(node)
                )
                if surface and not self._surface_is_trusted(node):
                    sites.append(node)
            elif isinstance(node, JsCallExpression):
                if not self._surface_is_trusted(node) and _is_string_timer(node, self.assumed_keys):
                    sites.append(node)
            elif isinstance(node, (JsVariableDeclarator, JsAssignmentExpression)):
                if (
                    self._destructures_a_reflective_intrinsic(node)
                    and not self._surface_is_trusted(node)
                ):
                    sites.append(node)
        self._opaque_surface_sites = sites
        self._reflection_surface = saw_with or bool(sites)

    def _trusted_surface_keys(self) -> frozenset[str]:
        """
        The keys of a member read the model takes for no reflective surface
        (`_surface_is_trusted`).
        """
        return _TRUSTED_SURFACE_KEYS if self.trust_eval else frozenset()

    def _surface_is_trusted(self, node: Node) -> bool:
        """
        Whether the reflective surface at *node* is of a kind the trusting model assumes inert, so
        the surface walk declines to count it. The covered kinds are the ones that run code
        supplied as data this model cannot read: the callee read that spells a direct `eval` call,
        the `Function` intrinsic obtained as a value — a bare read of the name, a member key
        naming it, a `constructor` key whose yield flows onward, or a destructuring of it out of
        the global object — and a string timer whose first argument is not a function literal.

        What is never trusted is a spelling of `eval` other than a direct call's callee — a value
        read of the bare name, an `eval` member key, a destructuring, or a destructuring key only
        the runtime resolves (`{[k]: e}`), which may extract `eval` — because that is indirect eval,
        which runs in the global scope; and a computed read of an unknown global consults no code
        this model cannot read. A span of source this model never read, a `with` body, and an
        `import()` reach this walk through their own branches and stay kept under both models.
        """
        if not self.trust_eval:
            return False
        if isinstance(node, JsCallExpression):
            return True
        if isinstance(node, JsIdentifier):
            if node.name == 'Function':
                return True
            return node.name == 'eval' and self._is_the_callee_of_a_direct_eval_call(node)
        if isinstance(node, JsMemberExpression):
            return key_is(node, _TRUSTED_SURFACE_KEYS, NO_IS_STRICTER)
        if isinstance(node, (JsVariableDeclarator, JsAssignmentExpression)):
            pattern = node.id if isinstance(node, JsVariableDeclarator) else node.left
            if not isinstance(pattern, JsObjectPattern):
                return False
            exposure = _pattern_reflective_exposure(pattern)
            return 'eval' not in exposure.named and not exposure.dynamic_key
        return False

    @staticmethod
    def _is_the_callee_of_a_direct_eval_call(node: JsIdentifier) -> bool:
        """
        Whether the read of `eval` at *node* is the callee that spells a direct `eval` call — the
        one position of the name the trusting model counts as the covered kind. Every other read of
        the name hands the intrinsic out for an indirect call, which no model trusts.
        """
        cursor: Node | None = node
        while isinstance(cursor.parent, JsParenthesizedExpression):
            cursor = cursor.parent
        call = cursor.parent
        return (
            isinstance(call, JsCallExpression)
            and call.callee is cursor
            and is_direct_eval_call(call)
        )

    def _destructures_a_reflective_intrinsic(
        self, node: JsVariableDeclarator | JsAssignmentExpression,
    ) -> bool:
        """
        Whether *node* binds one of the reflective intrinsics out of the global object: an object
        pattern whose source may be the object (`may_be_the_global_object`) and that names `eval` or
        `Function` among its keys, or reads one under a key only the runtime resolves. `const {eval}
        = globalThis` is the same value-read of the intrinsic that the bare name spells — the pattern
        reads the property off the object and binds its value — so it is a reflection surface just as
        the bare spelling is, and `const {[k]: e} = globalThis` is the destructuring counterpart of
        the computed member read `globalThis[k]` (`_is_reflective_member`): its key may resolve to
        `eval` at runtime, so it is a surface too. A pattern destructuring anything else, or the same
        names out of any other object, binds a value the program chose and is no surface.
        """
        if isinstance(node, JsVariableDeclarator):
            pattern, source = node.id, node.init
        else:
            pattern, source = node.left, node.right
        if not isinstance(pattern, JsObjectPattern) or not self.may_be_the_global_object(source):
            return False
        exposure = _pattern_reflective_exposure(pattern)
        return bool(exposure.named) or exposure.dynamic_key

    def opaque_reflection_sites(self) -> list[Node]:
        """
        The AST nodes of the whole-program opaque reflective surfaces — a value-read of the
        `eval`/`Function` intrinsic, a reflective global-object member, a string-valued timer, an
        `import()`, or a span of source this model never read. A `with` statement is not opaque (its
        body's accesses are attributed as dynamic references) and is excluded. Computed once and
        memoized; empty exactly when the program has no opaque surface, which
        `_has_opaque_reflection_surface` reports as its non-emptiness.
        """
        self._ensure_reflection_detected()
        assert self._opaque_surface_sites is not None
        return self._opaque_surface_sites

    def _has_opaque_reflection_surface(self) -> bool:
        return bool(self.opaque_reflection_sites())

    def _build_def_use(self):
        """
        Record every reference of the program against the binding it reaches. The identifier
        references come first, the names holding the global object are found from the values those
        write, and only then is each member access classified: whether `w.g = 2` writes the global
        `g` depends on whether `w` holds the object, wherever in the program `w` is given it.
        """
        self._recording_def_use = True
        self._create_implicit_globals()
        accesses = self._record_def_use_references()
        self._global_object_holders = self._bindings_holding_the_global_object()
        for access in accesses:
            self._record_global_alias_member_reference(access)
        self._recording_def_use = False
        self._record_arguments_alias_references()
        self._record_global_object_alias_references()
        self._record_copy_reads()

    def _record_copy_reads(self):
        """
        Record, against the block binding of each function declared in a block, the read the copy
        Annex B makes of it where the declaration runs, wherever something observes the copy. The
        declaration stands in for the read (`ReferenceNode`). It is recorded last because whether
        the copy is observed is read off every other reference the `var` has.
        """
        for copy in self._annex_b_copies:
            if copy.observed:
                self._record_alias_reference(copy.block, copy.declaration, Role.READ)

    def _mark_export_declaration(self, declaration: JsExportNamedDeclaration | JsExportDefaultDeclaration):
        """
        Flag the bindings one `export` ties to the outside as `Binding.exported`. A declaration written
        under an export (`export var a`, `export function`/`class`, and `export default` of a named
        function or class) exports the binding it declares; a sourceless list (`export { a }`,
        `export { a as q }`) exports the binding each specifier's local half names. A list carrying a
        `from` clause and a re-export name a binding of the module the clause spells, nothing local,
        and are passed over here.
        """
        if isinstance(declaration, JsExportDefaultDeclaration):
            self._mark_declaration_exported(declaration.declaration)
        elif declaration.declaration is not None:
            self._mark_declaration_exported(declaration.declaration)
        elif declaration.source is None:
            for specifier in declaration.specifiers:
                if isinstance(specifier, JsErrorNode):
                    continue
                if isinstance(specifier.local, JsIdentifier):
                    self._mark_binding_exported(self.resolve(specifier.local))

    def _mark_declaration_exported(self, declaration: Node | None):
        """
        Flag the bindings a declaration written under an export declares. A `var`/`let`/`const`
        exports every name its declarators bind, descending through destructuring; a function or
        class declaration exports its own name. An expression under `export default` declares no
        binding and is read like any other value.
        """
        if isinstance(declaration, JsVariableDeclaration):
            for declarator in declaration.declarations:
                if isinstance(declarator, JsVariableDeclarator):
                    for ident in pattern_identifiers(declarator.id):
                        self._mark_binding_exported(self.binding_of(ident))
        elif isinstance(declaration, (JsFunctionDeclaration, JsClassDeclaration)):
            if isinstance(declaration.id, JsIdentifier):
                self._mark_binding_exported(self.binding_of(declaration.id))

    @staticmethod
    def _mark_binding_exported(binding: Binding | None):
        if binding is not None:
            binding.exported = True

    def _record_def_use_references(self) -> list[JsMemberExpression]:
        """
        Record every identifier reference, and return the member accesses the walk passes in the
        order it meets them, for `_build_def_use` to classify once the names holding the global
        object are known. One record per reference node: the walk reaches a node once per slot
        holding it, and the one identifier of `{ a }` or of `export { a };` fills two, so without
        the dedup a read's multiplicity would follow its spelling rather than the program. The
        export-marking rider shares the walk: it consults only scope-builder state, so riding along
        changes nothing the remaining construction walks observe. So does the rider answering
        `module_may_be_reentered`, which consults nothing but the node, and the one filing the
        direct `eval` calls and unread spans of every scope owning variables, which
        `_direct_eval_sites` and `_unread_source_sites` answer from. Those lists are facts of the
        tree the model is built from like every other: a pass holding the model across its own edits
        would otherwise read them off the edited tree, where an `eval` it spliced away no longer
        stands while the writes the splice made are missing from the model.
        """
        pending = [self.root_scope]
        while pending:
            scope = pending.pop()
            if scope.is_var_scope:
                self._function_direct_eval_sites[id(scope.node)] = []
                self._function_unread_source_sites[id(scope.node)] = []
            pending.extend(scope.children)
        seen: set[int] = set()
        accesses: list[JsMemberExpression] = []
        for node in self.root.walk():
            if isinstance(node, (JsExportNamedDeclaration, JsExportDefaultDeclaration)):
                self._mark_export_declaration(node)
            if _lets_another_module_run(node):
                self._module_may_be_reentered = True
            if is_direct_eval_call(node):
                self._file_reflective_site(node, self._function_direct_eval_sites)
            elif is_unread_source(node):
                self._file_reflective_site(node, self._function_unread_source_sites)
            if isinstance(node, JsMemberExpression):
                accesses.append(node)
                continue
            if not isinstance(node, JsIdentifier):
                continue
            if id(node) in seen:
                continue
            seen.add(id(node))
            if not self.is_reference(node):
                continue
            ref_scope = self._node_scope.get(node)
            binding = self.lookup(node.name, ref_scope)
            if binding is None:
                self._attribute_dynamic_reference(node, ref_scope)
                continue
            role = reference_role(node)
            if role is not Role.WRITE:
                binding.reads.append(node)
            if role is not Role.READ:
                binding.writes.append(node)
            binding.note_reference_from(ref_scope)
            if binding.may_be_absent:
                self._lend_to_the_binding_outside(node, binding)
        return accesses

    def _lend_to_the_binding_outside(self, node: JsIdentifier, binding: Binding):
        """
        Record *node*, a reference to a binding that may be absent, as a dynamic reference of what
        its name means outside the scope of *binding*, which the reading without *binding* resolves
        it to (`Binding.may_be_absent`).
        """
        if binding.scope.parent is None:
            return
        outside = self.lookup(node.name, binding.scope.parent, cross_dynamic=True)
        if outside is not None:
            outside.dynamic_refs.append(node)

    @staticmethod
    def _file_reflective_site(site: Node, sites_of: dict[int, list[Node]]):
        """
        Add *site* to the list *sites_of* keeps for every scope node enclosing it, which is the list
        of each scope owning variables that the site can name a variable of.
        """
        cursor: Node | None = site
        while cursor is not None:
            sites = sites_of.get(id(cursor))
            if sites is not None:
                sites.append(site)
            cursor = cursor.parent

    def _attribute_dynamic_reference(self, node: JsIdentifier, scope: Scope | None):
        """
        Attribute a reference that did not resolve statically to the binding it could reach across a
        dynamic scope. A name inside a `with` body resolves to `None` — it may denote a property of the
        `with` object or a lexical binding — so the def-use walk would otherwise drop it. Only a name that
        crosses a dynamic scope is a candidate; continuing the lookup past that boundary finds the lexical
        binding it may touch, and the reference is recorded on that binding's `dynamic_refs`. A genuinely
        free name that crosses no dynamic scope (an external global the program never declares) is left
        untouched, as is one whose cross-boundary lookup still finds no binding.
        """
        if not crosses_dynamic_scope(scope):
            return
        binding = self.lookup(node.name, scope, cross_dynamic=True)
        if binding is not None:
            binding.dynamic_refs.append(node)

    def _create_implicit_globals(self):
        """
        Give every implicitly-declared global a binding at script scope, so that the def-use pass that
        follows resolves its references to it like any other binding. A name becomes an implicit global
        when the program writes it — an assignment, update, or `for-in`/`for-of` target — without it
        resolving to any lexical binding, which in sloppy mode creates a property on the global object.
        A write through a member access on a global-object alias (`globalThis.g = ...`) likewise creates
        the named global; the reference itself — the alias write, and any alias read — is recorded
        against the binding by `_build_def_use` like any other reference, so this pass establishes
        existence only. A write that resolves through a dynamic scope is skipped: inside a `with` body
        the target may be a property of the `with` object rather than a global, so the model cannot
        claim a global binding.
        """
        for node in self.root.walk():
            if isinstance(node, JsMemberExpression):
                self._ensure_implicit_global_from_alias_write(node)
                continue
            if not isinstance(node, JsIdentifier) or not self.is_reference(node):
                continue
            scope = self._node_scope.get(node)
            if reference_role(node) is Role.READ:
                continue
            if self.lookup(node.name, scope) is not None or crosses_dynamic_scope(scope):
                continue
            self.root_scope.bindings.setdefault(
                node.name, Binding(node.name, BindingKind.IMPLICIT_GLOBAL, self.root_scope))

    def global_alias_member_name(self, member: JsMemberExpression) -> str | None:
        """
        The name of the global that a member access on a global-object alias references
        (`globalThis.g`, `window['g']` → `g`), or `None` when *member* is not such an access. The alias
        must be an unshadowed `GLOBAL_OBJECT_ALIASES` identifier (a local `window` names an ordinary
        object, not the global) with a statically known property name, and the access must not cross a
        dynamic scope, where the alias could be rebound or the target could be a `with`-object property —
        in either case the model cannot claim the reference denotes a global.

        A `this` where the top level holds one denotes the global object only where the file is a
        script (`file_kind`): in an ES module it denotes nothing, and in a CommonJS file that file's
        exports.
        """
        if (
            self.file_kind.scopes_declarations_to_the_file
            and isinstance(strip_parens(member.object), JsThisExpression)
        ):
            return None
        return self._global_member_name(member, self._base_is_the_global_object)

    def may_name_a_global(self, member: JsMemberExpression) -> str | None:
        """
        The name of the global that a member access *may* reference once the program runs, read
        through `may_be_global_object_base` rather than through the spelling alone, or `None`.

        The reading half of `global_alias_member_name`, and separate from it because the two answers
        are spent on opposite things. This one is recorded as a reference, where admitting an access
        whose receiver turns out to be another object keeps a declaration nothing reaches. That one
        drives a rewrite, where the same admission renames a method's own property to a global:
        `refinery.lib.scripts.js.deobfuscation.reflection` resolves a member callee through it, and
        a `this.eval(...)` answered as the global `eval` rewrites a call to an ordinary method.

        No binding is minted from this answer. `_ensure_implicit_global_from_alias_write` keeps the
        spelling question, because a minted global is a name every intrinsic-trust and reflection
        reader then sees, and one minted from a receiver that was some other object withdraws trust
        the file never gave up.
        """
        return self._global_member_name(member, self._base_may_be_the_global_object)

    def _global_member_name(
        self,
        member: JsMemberExpression,
        base_is_the_global_object: Callable[[Node | None], bool],
    ) -> str | None:
        base = strip_parens(member.object)
        if not base_is_the_global_object(base):
            return None
        if crosses_dynamic_scope(self._node_scope.get(member)):
            return None
        return key_named(member, self.assumed_keys)

    def _base_is_the_global_object(self, base: Node | None) -> bool:
        """
        Whether *base* is the global object under the narrow reading: the spelling says so, and the
        name it is spelled with is not bound to anything else. A local `window` names an ordinary
        object, so the two questions are one answer here, and every reader that drives a rewrite
        gets that answer.
        """
        return is_global_object_base(base) and not self._is_bound_here(base)

    def _holds_the_global_object(self, node: Node | None) -> bool:
        """
        Whether *node* is the global object: spelled as one, or a name the file gives it to. A
        program meant to run in a browser and in something else names it once — `var w = window ||
        {}` — and every read through that name afterwards reads a global property, which
        `_base_is_the_global_object` cannot see, because the name it is asked about is `w`.
        """
        return self._base_is_the_global_object(node) or self.names_the_global_object(node)

    def _base_may_be_the_global_object(self, base: Node | None) -> bool:
        """
        Whether *base* may be the global object once the program runs: `_holds_the_global_object`
        widened by the receiver a call supplies, which `may_be_global_object_base` states. Only a
        reader recording a reference asks this, and the argument for admitting a receiver that turns
        out to be another object is written there.
        """
        return (
            may_be_global_object_base(base) and not self._is_bound_here(base)
        ) or self.names_the_global_object(base)

    def may_be_the_global_object(self, node: Node | None) -> bool:
        """
        Whether *node* may be the global object once the program runs, asked of the node alone:
        spelled as one and not bound to something else, a receiver any call may supply (`this`), or
        a name any value of which the file gives the object. The node-level form of the base
        question `may_name_a_global` asks of a member access, so a consumer deciding whether a
        write, an install, or a hand-over reached the global object shares this one reading rather
        than each spelling a narrower one — the miss of one narrower spelling is how a written
        global went unrecorded while the fold kept trusting it.
        """
        return self._base_may_be_the_global_object(node)

    def _is_bound_here(self, node: Node | None) -> bool:
        return (
            isinstance(node, JsIdentifier)
            and self.lookup(node.name, self._node_scope.get(node)) is not None
        )

    def names_the_global_object(self, node: Node | None) -> bool:
        """
        Whether *node* is a name the file gives the global object, so a property read on it may be a
        read of a global: one of the names `_bindings_holding_the_global_object` finds. ANY value of
        `binding_values` being the object is enough, and completeness is not asked: the callers
        record a reference, where one admission too many keeps a declaration and one refusal too
        many deletes one, so admitting is this answer's safe direction — a name that held the object
        on one branch of its life records the reads made through it even where another branch gave
        it something else. Parentheses around the name are looked through, as they are for a name
        spelled as the object (`is_global_object_base`).

        A name the file never declares answers nothing, since `binding_values` declines for a
        binding with no declaration: an implicit global the file assigns by name, and the binding
        `_ensure_implicit_global_from_alias_write` mints for a name the file installs on the global
        object, whose value only a member access gives.

        The values hold wherever the name is not in their temporal dead zone, and nothing here orders
        an establishing definition before the read. A caller driving a rewrite has to — and needs the
        complete-singleton reading `singular_value` gives, not this one.
        """
        if not self._global_object_holders:
            return False
        node = strip_parens(node)
        return isinstance(node, JsIdentifier) and self.resolve(node) in self._global_object_holders

    def _bindings_holding_the_global_object(self) -> frozenset[Binding]:
        """
        Every binding the text gives the global object, directly or through any number of other
        names, read once per build from `binding_values`. A value is the object when it is spelled
        as one, and `A || B` is it whenever `A` is: every spelling of the object is truthy, so the
        guard a program writes to survive a host lacking the name it prefers evaluates to the object
        wherever that name exists. A value that is a name passes on whatever that name holds.
        """
        holders: set[Binding] = set()
        copies: dict[Binding, list[Binding]] = {}
        for binding in dict.fromkeys(self._binding_of.values()):
            values, _ = self.binding_values(binding, ignore_dynamic_rebinds=True)
            for value in values:
                value = strip_parens(value)
                while isinstance(value, JsLogicalExpression) and value.operator == '||':
                    value = strip_parens(value.left)
                if self._base_is_the_global_object(value):
                    holders.add(binding)
                elif isinstance(value, JsIdentifier):
                    source = self.resolve(value)
                    if source is not None:
                        copies.setdefault(source, []).append(binding)
        pending = list(holders)
        while pending:
            for binding in copies.get(pending.pop(), ()):
                if binding not in holders:
                    holders.add(binding)
                    pending.append(binding)
        return frozenset(holders)

    def _ensure_implicit_global_from_alias_write(self, member: JsMemberExpression):
        """
        Give a global written through a member access on a global-object alias (`globalThis.g = ...`) an
        implicit-global binding when the name is otherwise undeclared, so the def-use pass resolves the
        reference to it. Only a write creates a global property, so a read establishes nothing; the write
        itself is recorded against the binding by `_build_def_use` like any other reference, so this
        establishes existence only.

        The binding minted here is one nothing reads a value out of: it carries no declaration, so
        both value queries decline for it, and all it does is give a reference somewhere to resolve
        to instead of standing free. A write through the `this` of a top level mints one where the
        file is a script; in a module of either kind that `this` is not the global object
        (`global_alias_member_name`).
        """
        if not is_member_write_target(member):
            return
        name = self.global_alias_member_name(member)
        if name is None:
            return
        self.root_scope.bindings.setdefault(
            name, Binding(name, BindingKind.IMPLICIT_GLOBAL, self.root_scope))

    def _global_alias_member_binding(self, member: JsMemberExpression) -> Binding | None:
        """
        The existing global binding a member access on a global-object alias references, or `None`.
        Unlike `_ensure_implicit_global_from_alias_write` this never creates a binding: a read of an
        otherwise-undeclared global has none to attribute and leaves the name free.

        Read through `may_name_a_global`, so a receiver a call may supply the global object for is
        recorded too. Nothing is created from that answer, so the widest it can be wrong is to keep
        a declaration a reader never reaches.
        """
        name = self.may_name_a_global(member)
        if name is None:
            return None
        return self.root_scope.bindings.get(name)

    def _record_global_alias_member_reference(self, member: JsMemberExpression):
        """
        Record a reference performed through a member access on a global-object alias (`globalThis.g`,
        `globalThis.g = ...`, `globalThis.g += 1`) against the global's binding, exactly as an ordinary
        identifier reference is recorded: `reference_role` decides whether the access reads, writes, or
        both. The binding must already exist — `_ensure_implicit_global_from_alias_write` established one
        for an alias write, while a read of an undeclared global stays free. The member node stands in
        for the referencing identifier the global has none of (see `Binding`). Without the read half a
        `globalThis.g` read would leave the binding looking unreferenced, so a remover could drop a live
        global whose only use is through the alias.
        """
        binding = self._global_alias_member_binding(member)
        if binding is None:
            return
        role = reference_role(member)
        if role is not Role.WRITE:
            binding.reads.append(member)
        if role is not Role.READ:
            binding.writes.append(member)
        binding.note_reference_from(self._node_scope.get(member))

    def _record_arguments_alias_references(self):
        """
        Record, against each parameter binding, the references made through an `arguments` object whose
        elements alias the parameters, exactly as a reference through a global-object alias is recorded
        by `_record_global_alias_member_reference`. The two are the same situation: a binding reached
        through an object rather than by its own name, which the identifier walk therefore does not see.
        Without this a body that only ever reads `arguments[0]` leaves its first parameter looking
        unreferenced, and a remover drops the write whose value that read answers with.

        `has_mapped_arguments` decides which functions have such an object at all, so a strict body, an
        arrow, and any list holding a default, a rest element or a destructuring pattern contribute
        nothing.

        Where the object is reached is `walk_receiver_scope`: an arrow reads the enclosing `arguments`
        and is descended, a nested function has its own and is not.

        An element access is attributed to the parameter it names. The read half of that access is a
        definite read of the parameter, and the write half never is: §10.2.11 maps an element onto a
        parameter only at a position the call supplied an argument for, so `arguments[0] = 9` writes
        the first parameter when the call passed one and creates an ordinary property when it passed
        none. Nothing in the text of the function says which, so the write is recorded as an
        `indefinite_writes` entry — a kill that names no value — and not as a definition a fold
        could answer with. A bare use of the object is asked what its governing construct can do with
        it: one that observes identity alone — a `typeof`, a truth test, a `for-in` head — is
        recorded as nothing, and one that reads every element and nothing else — a spread, a
        `for-of` head — as a read of each parameter. `_observes_identity_alone` and
        `_reads_every_element_alone` carry the argument for every admitted position, and an
        indefinite write recorded at one of them would refuse every fold in the function for a use
        that cannot write anything. Every use those two decline — the object handed to a call, the
        object bound to a second name — is recorded as a read of every parameter and an indefinite
        write of every one of them: reading is what makes a write to a parameter observable, which
        is the fact a remover needs, and the object may reach code that writes any element.
        `arguments[i] = v` for an `i` the model cannot read is recorded the same way, since it may
        write any single one and recording a definition of each would let a fold answer with a value
        only one of them can hold.

        The name is resolved rather than matched, because a body may bind `arguments` itself — as a
        parameter, a lexical declaration, a `var` given a value, or a catch parameter — and may also
        assign over the one it was given. In either case the name denotes something whose elements
        alias nothing, so attributing an access to a parameter would credit the parameter with a
        write the program never makes. Such a function is left alone entirely rather than up to the
        point of the rebinding, because which accesses run before it is a question about flow that a
        walk over the text does not answer. `_displaces_arguments` decides it.

        A function expression whose own name is `arguments` is not one of those: that name is bound
        in an environment the object's own shadows, so the body still reads the mapped object. The
        scope model records the two as one binding, which is why the binding's kind is admitted as
        well as `ARGUMENTS` here rather than only it.

        A name that resolves to nothing is still taken for the object where it stands: resolution
        answers `None` for a free name and across a `with`, neither of which is evidence that something
        else was bound.

        The function a CommonJS host wraps around the file has an `arguments` of its own too, whose
        elements alias the wrapper's parameters in their order
        (`refinery.lib.scripts.js.analysis.environment.COMMONJS_WRAPPER_PARAMETERS`), and its
        references are recorded the same way (`_record_wrapper_arguments`).
        """
        for fn in self.root.walk():
            if not isinstance(fn, (JsFunctionExpression, JsFunctionDeclaration)):
                continue
            if not has_mapped_arguments(fn, strict=False) or strict_mode_at(fn):
                continue
            own = self.lookup('arguments', self.function_scope(fn))
            if own is None or own.kind not in (BindingKind.ARGUMENTS, BindingKind.FUNC_NAME):
                continue
            if _displaces_arguments(own, fn):
                continue
            params = _last_positions([
                self.binding_of(param) if isinstance(param, JsIdentifier) else None
                for param in fn.params
            ])
            self._record_mapped_arguments(fn, own, params)
        self._record_wrapper_arguments()

    def _record_wrapper_arguments(self):
        """
        Record the references the `arguments` of the function a CommonJS host wraps around the file
        makes to that function's parameters, where the file may be CommonJS and its text leaves the
        top sloppy.
        """
        if not self.file_kind.may_be_commonjs or strict_mode_at(self.root):
            return
        own = self.root_scope.bindings.get('arguments')
        if own is None or not own.supplied_by_host:
            return
        if not references_own_arguments(self.root) or _displaces_arguments(own, self.root):
            return
        params = [self.root_scope.bindings.get(name) for name in COMMONJS_WRAPPER_PARAMETERS]
        self._record_mapped_arguments(self.root, own, params)

    def _record_mapped_arguments(self, fn: Node, own: Binding, params: list[Binding | None]):
        """
        Record, against each of *params* by its position, the references the `arguments` object
        *own* of *fn* makes to it (`_record_arguments_alias_references`).
        """
        for node in walk_receiver_scope(fn):
            if not isinstance(node, JsIdentifier) or node.name != 'arguments':
                continue
            if not self.is_reference(node):
                continue
            denotes = self.resolve(node)
            if denotes is not None and denotes is not own:
                continue
            access = _enclosing_member_access(node)
            if access is not None and denotes is not None:
                named = _aliased_parameter_positions(access, len(params))
                if named is not None:
                    role = reference_role(access)
                    for index in named:
                        self._record_alias_reference(params[index], access, role)
                    continue
            if access is None:
                governor = enclosing_operator(node)
                if _observes_identity_alone(governor, node):
                    continue
                if _reads_every_element_alone(governor, node):
                    for binding in params:
                        self._record_alias_reference(binding, node, Role.READ)
                    continue
            site: JsIdentifier | JsMemberExpression = node if access is None else access
            may_write = access is None or reference_role(access) is not Role.READ
            for binding in params:
                self._record_alias_reference(binding, node, Role.READ)
                if may_write:
                    self._record_alias_reference(binding, site, Role.WRITE)

    def _record_global_object_alias_references(self):
        """
        Record, against every binding a classic script's global object carries, the references a
        call may make through the object once it is handed one, but only for a hand-over the callee
        could read a property through. `a(globalThis, 'q')` and `a(this, 'q')` both give `a` an
        object whose properties are the script's top-level declarations, and a body that writes one
        of them writes the declaration — which no identifier in the text names, so the identifier
        walk sees nothing. A call that never reads a property of the object it is handed reaches no
        declaration through it, and admitting one there freezes every fold in the file for a
        reference the program never makes.

        `global_object_argument_is_observed` decides, per hand-over, whether the callee could read a
        property. An observed hand-over records the same way `_record_arguments_alias_references`
        does — a read of every binding, so a declaration reached only through the object is not
        removed, and an indefinite write of every one, so a fold does not carry a value across a
        write the callee made. Which properties the callee touches is not decided, and every binding
        is admitted, for the reason the argument list is admitted whole: a value only some of them
        can hold is not a definition of any of them. An unobserved hand-over records nothing, and
        the union is taken across hand-overs — a binding stays reachable if any one is observed.

        The gate runs in two phases because it reads `singular_value` to resolve a callee, and the
        record it is about to make is an indefinite write that would make that query decline. Every
        hand-over is judged first, against the model as it stands before this method writes anything,
        and only then are the observed ones recorded. `_build_def_use` finds the names holding the
        global object in the same order for the same reason — an answer read out of a walk still
        recording the writes it depends on would depend on how far the walk had got, not on what the
        program says.

        The object is recognized by `_holds_the_global_object`, so only the `this` a script's top
        level holds is one. A `this` inside a function is the receiver its call supplied, and
        admitting it costs every fold in a file that hands one to anything: a self-defending
        wrapper passes its own `this` to a call, and a run that took it for the global object
        leaves that sample twenty times its deobfuscated size. `may_be_global_object_base`
        admits every `this` for the opposite reason — there the wrong answer only keeps a
        declaration alive, and here it freezes the file.

        Only an argument is read. A `return` of the object hands it to a caller the text still
        shows, and taking that for an escape refuses `refinery.lib.scripts.js.deobfuscation
        .globalfinder` the very function whose removal makes the object nameable, leaving the two
        obfuscated samples that use a finder at their original size.
        """
        bindings = list(self.root_scope.bindings.values())
        observed: list[ReferenceNode] = []
        for node in self.root.walk():
            if not isinstance(node, (JsIdentifier, JsThisExpression)):
                continue
            if not _is_call_argument(node) or not self._holds_the_global_object(node):
                continue
            if self.global_object_argument_is_observed(node):
                observed.append(node)
        if observed:
            # A callee that could read a property of the object it was handed could write one under
            # a key no text spells, which is the opaque global write's own question, so an observed
            # hand-over makes that fact hold: `t(globalThis)` and `Reflect.set(globalThis, …)` are
            # otherwise invisible to every consumer of `has_opaque_global_write`. The walk runs
            # even where no binding exists to record against, because the fact is not a binding's.
            self._opaque_global_write = True
        for node in observed:
            for binding in bindings:
                binding.reachable_through_a_handed_object = True
                self._record_alias_reference(binding, node, Role.READWRITE)

    def global_object_argument_is_observed(self, node: Node) -> bool:
        """
        Whether the call *node* is handed to could read a property of the global object *node*
        stands for. An unobserved hand-over lets the globals the object carries stay foldable; an
        observed one, or one the model cannot resolve, is admitted whole the way it always has been.

        The callee is resolved to the function it runs: a function written in place, a name whose
        one value is a function, or a name whose one value is the zero-argument IIFE a self-defending
        wrapper's factory is, whose single returned function is the one that runs. A callee resolving
        to none of these is not read, so its object is observed. A function that reaches its own
        `arguments` is observed too, because an element of that object is the handed argument under
        another name, which the parameter walk does not follow.

        The argument is matched to the parameter it binds by position; a list with a rest, default,
        or destructuring element is not matched and its object is observed. An argument past the last
        parameter binds nothing the callee can name and is not observed. A parameter reflection can
        reach is observed. Otherwise `_parameter_is_observed` asks the body.
        """
        call = _enclosing_call(node)
        if call is None:
            return True
        function = self.target_function_of_call(call)
        if function is None:
            return True
        if references_own_arguments(function):
            return True
        mapping = self._argument_parameter_map(call, function)
        if mapping is None:
            return True
        parameter = next((b for b, argument in mapping.items() if argument is node), None)
        if parameter is None:
            return False
        if self.reflection_can_reach(parameter):
            return True
        return self._parameter_is_observed(function, parameter, mapping, {id(function)}, 0)

    def target_function_of_call(self, call: JsCallExpression | JsNewExpression) -> JsFunctionNode | None:
        """
        The function *call* runs, as far as it resolves without leaving the text: the callee written
        as a function, a name whose one value is a function, or a name whose one value is a
        zero-argument IIFE returning a single function — the shape the self-defending wrapper's
        factory takes. `None` when the callee resolves to none of these. A named callee is read
        through `singular_value`, the complete-singleton reading: a name that may hold another value
        — a parameter, or a declaration value later overwritten — resolves to `None`, and every
        consumer treats an unresolved callee as the observed hand-over.
        """
        callee = strip_parens(call.callee)
        if isinstance(callee, FUNCTION_NODES):
            return callee
        if not isinstance(callee, JsIdentifier):
            return None
        value = self.singular_value(self.resolve(callee))
        if value is None:
            return None
        value = strip_parens(value)
        if isinstance(value, FUNCTION_NODES):
            return value
        if isinstance(value, JsCallExpression) and not value.arguments:
            inner = strip_parens(value.callee)
            if isinstance(inner, FUNCTION_NODES):
                return _sole_returned_function(inner)
        return None

    def _argument_parameter_map(
        self,
        call: JsCallExpression | JsNewExpression,
        function: JsFunctionNode,
    ) -> dict[Binding | None, Node | None] | None:
        """
        A map from each parameter binding of *function* to the argument *call* supplies for it by
        position, or `None` when a parameter is not a plain name — a rest, default, or destructuring
        element the model cannot bind by position — or when an argument is a spread, whose element
        count is not known until it runs, so no argument after it aligns with a parameter by index. A
        parameter the call gives no argument for maps to `None`.
        """
        if any(not isinstance(parameter, JsIdentifier) for parameter in function.params):
            return None
        if any(isinstance(strip_parens(argument), JsSpreadElement) for argument in call.arguments):
            return None
        mapping: dict[Binding | None, Node | None] = {}
        for index, parameter in enumerate(function.params):
            if not isinstance(parameter, JsIdentifier):
                continue
            argument = strip_parens(call.arguments[index]) if index < len(call.arguments) else None
            mapping[self.binding_of(parameter)] = argument
        return mapping

    def _parameter_is_observed(
        self,
        function: JsFunctionNode,
        parameter: Binding | None,
        mapping: dict[Binding | None, Node | None],
        visiting: set[int],
        depth: int,
    ) -> bool:
        """
        Whether *function*'s body reads a property of the object bound to *parameter*. The parameter
        as the base of a member access reads one; the receiver an `apply`/`call` hands to a function
        that reads its own `this` does; a receiver handed to a `this`-free function does not, because
        that function never reads it. Every other use — returned, aliased, enumerated, handed on as
        a plain argument — is taken for an observation. The whole subtree is walked and each
        identifier resolved, so a use inside a nested closure that captures the parameter counts, and
        a shadowing binding of the same name does not.
        """
        if depth > _HANDED_OBJECT_OBSERVATION_DEPTH:
            return True
        for reference in function.walk():
            if not isinstance(reference, JsIdentifier) or not self.is_reference(reference):
                continue
            if self.resolve(reference) is not parameter:
                continue
            access = _enclosing_member_access(reference)
            if access is not None and strip_parens(access.object) is reference:
                return True
            if self._apply_receiver_is_safe(reference, mapping, visiting, depth) is not True:
                return True
        return False

    def _apply_receiver_is_safe(
        self,
        node: Node,
        mapping: dict[Binding | None, Node | None],
        visiting: set[int],
        depth: int,
    ) -> bool | None:
        """
        For a *node* that denotes the handed object: `True` when it is the `thisArg` of an
        `apply`/`call` that provably never reads the object; `False` when it is such a receiver but
        that is not proven; and `None` when *node* is not used as such a receiver at all, the case
        the caller reads as an observation. A named target is judged over every value it can hold
        during this call — `values_at_call`, so the argument mapped to a target parameter and every
        value assigned over it are weighed alike — and the receiver is safe only when that set is
        complete, non-empty, and every value never reads the receiver
        (`_apply_target_value_observes_the_receiver`). Every target — a name or a function written
        in place — presumes the intrinsic `apply` is what the dispatch finds, so any target is
        refused while a reflection surface stands, since reflected code can replace that intrinsic
        with a forwarder no matter which function the name holds — and this subsumes every way
        reflection could reach the target binding itself — and likewise while text can reach the
        prototype surface the dispatch walks (`_dispatch_surface_reachable`) or stores a property
        on the global object under a runtime key (`has_opaque_global_write`), which may replace
        the intrinsic the same way; a named target is
        further refused when the program installs properties through it
        (`_properties_installed_through`), which can shadow the intrinsic on the object alone.
        """
        parent = enclosing_operator(node)
        if not isinstance(parent, JsCallExpression):
            return None
        callee = strip_parens(parent.callee)
        if not isinstance(callee, JsMemberExpression):
            return None
        if not key_is(callee, _CALL_OR_APPLY, self.assumed_keys):
            return None
        if not parent.arguments or strip_parens(parent.arguments[0]) is not node:
            return None
        if not self.intrinsic_dispatch_trusted():
            return False
        target = strip_parens(callee.object)
        if isinstance(target, FUNCTION_NODES):
            return not self._function_observes_its_this(target, visiting, depth + 1)
        if not isinstance(target, JsIdentifier):
            return False
        binding = self.resolve(target)
        if binding is None:
            return False
        if self._properties_installed_through(binding):
            return False
        values, complete = self.values_at_call(binding, mapping)
        if not complete or not values:
            return False
        return not any(
            self._apply_target_value_observes_the_receiver(value, visiting, depth)
            for value in values
        )

    def _apply_target_value_observes_the_receiver(
        self,
        value: Node,
        visiting: set[int],
        depth: int,
    ) -> bool:
        """
        Whether *value*, dispatched as the target of an `apply`/`call`, may read the handed
        receiver. A function reads it exactly when it reads its own `this`
        (`_function_observes_its_this`). A `null` literal and an unshadowed `undefined` never do,
        because the apply then throws before the object is touched — and the obfuscator's
        self-defending wrapper writes `payload = null` after applying, so refusing them would
        refuse the wrapper's own fold. Every other value is taken for a reader.
        """
        value = strip_parens(value) or value
        if isinstance(value, FUNCTION_NODES):
            return self._function_observes_its_this(value, visiting, depth + 1)
        if isinstance(value, JsNullLiteral):
            return False
        if (
            isinstance(value, JsIdentifier)
            and value.name == 'undefined'
            and self.resolve(value) is None
        ):
            return False
        return True

    def _properties_installed_through(self, binding: Binding) -> bool:
        """
        Whether the text may install a property on the object *binding* holds, or on a prototype
        that object dispatches through — the two ways an own or inherited name can come to shadow
        an intrinsic a consumer trusts. Judged over every recorded read by what its position lets
        code do with the object, refusing wherever the answer would otherwise depend on code the
        model does not read. A member write anywhere on the access chain installs (`t.apply = f`,
        `t[k] = f`, and through the chain, `t.__proto__.apply = f`). A read whose chain passes a
        key that is not statically known, reaches the prototype surface, or names an accessor
        installer may perform or reveal an install (`_DISPLACING_CHAIN_KEYS`). Any escape hands
        the object to code that may install on it under another name — an alias, a call argument,
        which is how a `defineProperty` or an `Object.assign` receives its target, a return. A
        `VERDICT_READ` forwards nothing, so it is neither. A plain rebind of the name is a write
        of the binding rather than of the object, weighed by `values_at_call` and recorded as a
        write, so it never appears among the reads walked here. Which key an install stores is
        never asked: a computed write names no fixed key, and refusing every install keeps the
        answer independent of the folds that would reveal one.
        """
        for reference in binding.reads:
            role = container_reference_role(reference)
            if role is ContainerRole.MEMBER_WRITE:
                return True
            if role is ContainerRole.REBIND:
                continue
            if role is ContainerRole.ESCAPE:
                return True
            access = _enclosing_member_access(reference)
            while access is not None:
                if key_may_be(access, _DISPLACING_CHAIN_KEYS):
                    return True
                access = _enclosing_member_access(access)
        return False

    def intrinsic_dispatch_trusted(self) -> bool:
        """
        Whether a call of `call` or `apply` on a function finds the built-in: no reflection surface
        stands, through which code could replace it, nothing is stored on the global object under
        a runtime key (`has_opaque_global_write`), and no text reaches the prototype surface the
        dispatch walks (`_dispatch_surface_reachable`), `Function.prototype` among it. The one
        answer every reader that counts on such a call reaching the built-in asks.
        """
        return not (
            self.has_reflection_surface()
            or self.has_opaque_global_write()
            or self._dispatch_surface_reachable()
        )

    def _invocation_of(self, read: Node) -> ShownCall | None:
        """
        The call that invokes the value read at *read* (`_invocation`), where one that goes through
        `call` or `apply` finds the built-in (`intrinsic_dispatch_trusted`).
        """
        invocation = _invocation(read)
        if invocation is None:
            return None
        call, through_a_method = invocation
        if through_a_method and not self.intrinsic_dispatch_trusted():
            return None
        return call

    def _dispatch_surface_reachable(self) -> bool:
        """
        Whether text can obtain and then write through the prototype surface an `apply`/`call`
        dispatch walks. A write there may replace the intrinsic the dispatch is trusted to find, so
        while one is possible anywhere, no receiver hand-over is safe. A `getPrototypeOf`/
        `setPrototypeOf` member reaches the surface as a call and counts on sight, since an alias
        of it leaves no chain to climb. A `__proto__` or `constructor` key yields the surface as a
        value and counts exactly when what it yields is written through or flows onward
        (`_prototype_surface_escapes`): the obfuscator's own defense reads `.constructor(...)`
        merely to invoke it, and a gate refusing every such read would refuse the corpus it exists
        to fold. The `Function` intrinsic, read by its name, under a key naming it, or destructured,
        counts where it reaches its prototype (`_function_reaches_its_prototype`): the trusting
        model takes what it runs for inert and does not count its read as a reflection surface,
        but a write through its prototype is no code it runs. A key is read by
        `refinery.lib.scripts.js.keys.key_is`, which records one it cannot read.
        """
        if self._dispatch_surface_reached is None:
            self._dispatch_surface_reached = any(
                self._reaches_dispatch_surface(node) for node in self.root.walk()
            )
        return self._dispatch_surface_reached

    def _reaches_dispatch_surface(self, node: Node) -> bool:
        """
        Whether *node* lets text touch the prototype surface an `apply`/`call` dispatch walks, in
        the sense of `_dispatch_surface_reachable`.
        """
        if isinstance(node, JsMemberExpression):
            if _member_reaches_dispatch_surface(node, self.assumed_keys):
                return True
            return (
                not is_member_write_target(node)
                and _function_reaches_its_prototype(node)
                and key_is(node, _FUNCTION, self.assumed_keys)
            )
        if isinstance(node, JsIdentifier):
            return (
                node.name == 'Function'
                and self._reads_reflective_intrinsic(node)
                and _function_reaches_its_prototype(node)
            )
        if isinstance(node, (JsVariableDeclarator, JsAssignmentExpression)):
            return self._destructures_a_reflective_intrinsic(node)
        return False

    def _function_observes_its_this(
        self,
        function: JsFunctionNode,
        visiting: set[int],
        depth: int,
    ) -> bool:
        """
        Whether *function* reads the `this` its caller supplies. An arrow has none of its own and
        reads the enclosing one, so a receiver handed to it is never read; a regular function that
        names `this` anywhere in its own receiver scope, or runs a direct `eval` that could, reads
        it. The bound and the *visiting* set take a function that hands `this` on to itself, or a
        chain too deep to follow, for a reader.
        """
        if depth > _HANDED_OBJECT_OBSERVATION_DEPTH or id(function) in visiting:
            return True
        if isinstance(function, JsArrowFunctionExpression):
            return False
        if self._function_has_direct_eval(function):
            return True
        return any(isinstance(node, JsThisExpression) for node in walk_receiver_scope(function))

    def _record_alias_reference(
        self,
        binding: Binding | None,
        node: ReferenceNode,
        role: Role,
    ) -> None:
        """
        Record against a binding one reference made through an object that aliases it — a mapped
        `arguments` reaching a parameter, or the global object reaching a global — or through the
        copy Annex B makes of a block binding, which hands its value to a `var`. The read half is a
        definite read — the access observes whatever the binding holds — while the write half never
        is: what an object handed to a call writes through is decided by code the walk does not
        read, so it lands in `indefinite_writes` as a kill that names no value rather than in
        `writes` as a definition.
        """
        if binding is None:
            return
        if role is not Role.WRITE:
            binding.reads.append(node)
        if role is not Role.READ:
            binding.indefinite_writes.append(node)
        binding.note_reference_from(self._node_scope.get(node))

Methods

def scope_of(self, node)

The innermost scope that lexically contains node, or None if the node was not part of the script the model was built from.

Expand source code Browse git
def scope_of(self, node: Node) -> Scope | None:
    """
    The innermost scope that lexically contains *node*, or `None` if the node was not part of the
    script the model was built from.
    """
    return self._node_scope.get(node)
def knows(self, node)

Whether the model was built over node. The model holds every node it maps, so a node made after the build is never taken for one it mapped.

Expand source code Browse git
def knows(self, node: Node) -> bool:
    """
    Whether the model was built over *node*. The model holds every node it maps, so a node made
    after the build is never taken for one it mapped.
    """
    return node in self._node_scope
def function_scope(self, func)

The scope a function (or the script) introduces for its body: the script's root_scope, the body block's scope for a function node, the scope of its run for a field whose initializer runs at each construction, and None when func has none of these.

Expand source code Browse git
def function_scope(self, func: Node) -> Scope | None:
    """
    The scope a function (or the script) introduces for its body: the script's `root_scope`, the
    body block's scope for a function node, the scope of its run for a field whose initializer
    runs at each construction, and `None` when *func* has none of these.
    """
    if isinstance(func, JsScript):
        return self.root_scope
    body = getattr(func, 'body', None)
    if body is not None:
        return self.scope_of(body)
    for part in func.children():
        if evaluated_later(func, part):
            return self.scope_of(part)
    return None
def parameter_scope(self, func)

The scope holding func's parameters and the arguments object a call gives it, which is its body's scope but for a function whose parameter list holds an expression: that one binds them in a scope of its own standing between the body and what encloses the function.

A consumer reading a parameter binding out of a scope's own bindings asks for this one. function_scope answers the body's, which for such a function holds neither.

Expand source code Browse git
def parameter_scope(self, func: Node) -> Scope | None:
    """
    The scope holding *func*'s parameters and the `arguments` object a call gives it, which is
    its body's scope but for a function whose parameter list holds an expression: that one binds
    them in a scope of its own standing between the body and what encloses the function.

    A consumer reading a parameter binding out of a scope's own `bindings` asks for this one.
    `function_scope` answers the body's, which for such a function holds neither.
    """
    scope = self.function_scope(func)
    if scope is None:
        return None
    parent = scope.parent
    if parent is not None and parent.kind is ScopeKind.PARAMS and parent.node is scope.node:
        return parent
    return scope
def binding_of(self, decl_id)

The binding introduced by a binding-site identifier (a declarator id, parameter, function or class name, catch parameter, or import local), or None if the identifier is not a binding site. The name of a function declared in a block introduces the block's binding, which this answers; where Annex B copies it, the var the copy writes lists the name among its own declarations as well (AnnexBCopy).

Expand source code Browse git
def binding_of(self, decl_id: JsIdentifier) -> Binding | None:
    """
    The binding introduced by a binding-site identifier (a declarator id, parameter, function or
    class name, catch parameter, or import local), or `None` if the identifier is not a binding
    site. The name of a function declared in a block introduces the block's binding, which this
    answers; where Annex B copies it, the `var` the copy writes lists the name among its own
    declarations as well (`AnnexBCopy`).
    """
    return self._binding_of.get(decl_id)
def lookup(self, name, scope, *, cross_dynamic=False, exclude=())

Resolve name from scope outward through enclosing scopes, stopping at a dynamically-scoped region where the name could be injected at runtime. Returns None for a free name. With cross_dynamic, the walk does not stop at a dynamic boundary but continues outward to the binding the name would denote if the with object lacked the property — the lexical binding a dynamic scope could still reach at runtime — which is how a with-body reference is attributed to the binding it may touch. The default keeps the definite-resolution semantics every other caller relies on.

A binding in exclude is passed over as though its scope did not bind the name: the simulated post-rewrite resolution an atomic fold admits against, where one edit splices a body and deletes the bindings the splice consumes together. What the name denotes without them is what the tree after that edit reads.

Expand source code Browse git
def lookup(
    self,
    name: str,
    scope: Scope | None,
    *,
    cross_dynamic: bool = False,
    exclude: Collection[Binding] = (),
) -> Binding | None:
    """
    Resolve *name* from *scope* outward through enclosing scopes, stopping at a dynamically-scoped
    region where the name could be injected at runtime. Returns `None` for a free name. With
    *cross_dynamic*, the walk does not stop at a dynamic boundary but continues outward to the binding
    the name would denote if the `with` object lacked the property — the lexical binding a dynamic
    scope could still reach at runtime — which is how a `with`-body reference is attributed to the
    binding it may touch. The default keeps the definite-resolution semantics every other caller
    relies on.

    A binding in *exclude* is passed over as though its scope did not bind the name: the
    simulated post-rewrite resolution an atomic fold admits against, where one edit splices a
    body and deletes the bindings the splice consumes together. What the name denotes without
    them is what the tree after that edit reads.
    """
    while scope is not None:
        binding = scope.bindings.get(name)
        if binding is not None and binding not in exclude:
            return binding
        if scope.is_dynamic and not cross_dynamic:
            return None
        scope = scope.parent
    return None
def is_reference(self, node)

Whether node is a referencing occurrence of a name: it occupies a use position and is not a binding site, so it reads or writes an existing binding rather than declaring one or naming a property, key, label, or import/export specifier. The binding-aware companion to the syntactic is_use_position(); resolve resolves exactly the identifiers for which this holds.

Expand source code Browse git
def is_reference(self, node: JsIdentifier) -> bool:
    """
    Whether *node* is a referencing occurrence of a name: it occupies a use position and is not a
    binding site, so it reads or writes an existing binding rather than declaring one or naming a
    property, key, label, or import/export specifier. The binding-aware companion to the syntactic
    `is_use_position`; `resolve` resolves exactly the identifiers for which this holds.
    """
    return is_use_position(node) and node not in self._binding_of
def resolve(self, ref)

The binding a referencing identifier reads or writes, found by walking outward from its scope. Returns None when the name is free (an external global the program never assigns), when the identifier is not a reference (a property name, key, or label), or when resolution crosses a dynamically-scoped region where the name could be injected at runtime.

Expand source code Browse git
def resolve(self, ref: JsIdentifier) -> Binding | None:
    """
    The binding a referencing identifier reads or writes, found by walking outward from its scope.
    Returns `None` when the name is free (an external global the program never assigns), when the
    identifier is not a reference (a property name, key, or label), or when resolution crosses a
    dynamically-scoped region where the name could be injected at runtime.
    """
    if not self.is_reference(ref):
        return None
    return self.lookup(ref.name, self._node_scope.get(ref))
def references(self, binding, *, exclude=None)

Every reference (read or write) bound to binding, optionally omitting those that lie within the subtree of exclude. Each is a referencing identifier except where an object aliasing the binding stands in for one (see Binding).

Expand source code Browse git
def references(
    self, binding: Binding, *, exclude: Node | None = None,
) -> list[ReferenceNode]:
    """
    Every reference (read or write) bound to *binding*, optionally omitting those that lie within
    the subtree of *exclude*. Each is a referencing identifier except where an object aliasing the
    binding stands in for one (see `Binding`).
    """
    nodes = binding.reads + binding.writes
    if exclude is None:
        return nodes
    return [n for n in nodes if n is not exclude and not n.is_descendant_of(exclude)]
def dynamic_references(self, binding, *, exclude=None)

Every reference to binding that a dynamic scope resolves at runtime — an identifier inside a with body that could denote binding (it may instead denote a property of the with object, which is why the static references set omits it) — optionally omitting those within the subtree of exclude. Each is classified on demand by reference_role() or container_reference_role, the same oracles the definite references use, so a consumer applies one role logic to both; only the ordering and alias-following a resolved reference permits do not carry to an uncertain one.

Expand source code Browse git
def dynamic_references(
    self, binding: Binding, *, exclude: Node | None = None,
) -> list[JsIdentifier]:
    """
    Every reference to *binding* that a dynamic scope resolves at runtime — an identifier inside a
    `with` body that could denote *binding* (it may instead denote a property of the `with` object,
    which is why the static `references` set omits it) — optionally omitting those within the subtree
    of *exclude*. Each is classified on demand by `reference_role` or `container_reference_role`, the
    same oracles the definite references use, so a consumer applies one role logic to both; only the
    ordering and alias-following a resolved reference permits do not carry to an uncertain one.
    """
    nodes = binding.dynamic_refs
    if exclude is None:
        return list(nodes)
    return [n for n in nodes if n is not exclude and not n.is_descendant_of(exclude)]
def read_has_dynamic_effect(self, node)

Whether reading node as a value resolves through a dynamic scope — a bare identifier inside a with body — so that evaluating it is not a pure, droppable, or reorderable operand. Reading the bare name consults the with object first: a matching property fires the object's getter (or a proxy trap), an observable side effect; a missing one falls through to the lexical binding, or, failing that, throws a ReferenceError. Neither the getter nor the throw can be proved absent for an unknown object, so any reference that crosses a dynamic scope is effectful regardless of a lexical fallback. False for a statically resolved reference and any non-reference node.

Expand source code Browse git
def read_has_dynamic_effect(self, node: Node) -> bool:
    """
    Whether reading *node* as a value resolves through a dynamic scope — a bare identifier inside a
    `with` body — so that evaluating it is not a pure, droppable, or reorderable operand. Reading the
    bare name consults the `with` object first: a matching property fires the object's getter (or a
    proxy trap), an observable side effect; a missing one falls through to the lexical binding, or,
    failing that, throws a `ReferenceError`. Neither the getter nor the throw can be proved absent for
    an unknown object, so any reference that crosses a dynamic scope is effectful regardless of a
    lexical fallback. False for a statically resolved reference and any non-reference node.
    """
    if not isinstance(node, JsIdentifier) or not self.is_reference(node):
        return False
    return crosses_dynamic_scope(self._node_scope.get(node))
def read_may_throw(self, node)

Whether evaluating node as a read may throw a ReferenceError because the name it spells is not certain to denote a binding. The companion to read_has_dynamic_effect, which asks what else a read may do; this asks whether it may not happen at all. A caller that treats an unresolved read as free is asserting the host defines the name, which for a name the program neither declares nor assigns is an assertion about someone else's global object.

A name resolves for certain when a declaration binds it, or when the pinned host environment guarantees it on the global object (HostEnvironment.provides). The default universal environment provides exactly GUARANTEED_GLOBALS, the existence allowlist the language mandates everywhere, so globalThis resolves while the other GLOBAL_OBJECT_ALIASES spellings (window, self, top, frames, global) are a host assumption rather than a language one, and are not admitted: no host defines all of them, so a bare window throws under Node exactly as a bare global throws in a browser. An analyst who knows the host pins it with the djs unit's -e switch, and the names that host guarantees become certain here, recovering the reading the sound default refuses — except a host-conditional global the program deletes off a same-realm alias, which _scan_deleted_host_globals withholds program-wide so the bare read keeps its throw. Unpinned, the read is answered may-throw so no pass drops the ReferenceError the absent host raises, and _base_is_safe agrees, refusing to clear a property access on such an alias. Everything else may not be there:

  • a free name, which reaches the host and may simply not exist
  • a name whose only binding is an IMPLICIT_GLOBAL, which the assignment that creates it brings into existence, so a read that runs first — or whose creating assignment sits in a function nobody calls — throws exactly as a free name does
  • a name resolved through a with body whose object may not carry it and which has no lexical binding to fall through to, which the cross_dynamic lookup is what distinguishes
  • a binding one way the file may run has and another does not (Binding.may_be_absent)

A reference that is written and not read answers False, as do the two operator positions tolerates_unresolvable names. The write case is a scope boundary, not a claim that writing is safe: sloppy code assigning to a name nothing binds creates a property of the global object, while strict code throws the same ReferenceError a read does, which is a separate defect with its own pin (test_unfixed_defects.A_STRICT_REGION_ASSIGNING_TO_NO_BINDING).

Expand source code Browse git
def read_may_throw(self, node: JsIdentifier) -> bool:
    """
    Whether evaluating *node* as a read may throw a `ReferenceError` because the name it spells
    is not certain to denote a binding. The companion to `read_has_dynamic_effect`, which asks
    what else a read may do; this asks whether it may not happen at all. A caller that treats an
    unresolved read as free is asserting the host defines the name, which for a name the program
    neither declares nor assigns is an assertion about someone else's global object.

    A name resolves for certain when a declaration binds it, or when the pinned host environment
    guarantees it on the global object (`HostEnvironment.provides`). The default `universal`
    environment provides exactly `GUARANTEED_GLOBALS`, the existence allowlist the language
    mandates everywhere, so `globalThis` resolves while the other `GLOBAL_OBJECT_ALIASES`
    spellings (`window`, `self`, `top`, `frames`, `global`) are a *host* assumption rather than
    a language one, and are not admitted: no host defines all of them, so a bare `window` throws
    under Node exactly as a bare `global` throws in a browser. An analyst who knows the host
    pins it with the `djs` unit's `-e` switch, and the names that host guarantees become certain
    here, recovering the reading the sound default refuses — except a host-conditional global
    the program `delete`s off a same-realm alias, which `_scan_deleted_host_globals` withholds
    program-wide so the bare read keeps its throw. Unpinned, the read is answered may-throw so
    no pass drops the `ReferenceError` the absent host raises, and `_base_is_safe` agrees,
    refusing to clear a property access on such an alias. Everything else may not be there:

    - a free name, which reaches the host and may simply not exist
    - a name whose only binding is an `IMPLICIT_GLOBAL`, which the assignment that creates it
      brings into existence, so a read that runs first — or whose creating assignment sits in a
      function nobody calls — throws exactly as a free name does
    - a name resolved through a `with` body whose object may not carry it and which has no
      lexical binding to fall through to, which the `cross_dynamic` lookup is what distinguishes
    - a binding one way the file may run has and another does not (`Binding.may_be_absent`)

    A reference that is written and not read answers `False`, as do the two operator positions
    `tolerates_unresolvable` names. The write case is a scope boundary, not a claim that
    writing is safe: sloppy code assigning to a name nothing binds creates a property of the
    global object, while strict code throws the same `ReferenceError` a read does, which is a
    separate defect with its own pin
    (`test_unfixed_defects.A_STRICT_REGION_ASSIGNING_TO_NO_BINDING`).
    """
    if not self.is_reference(node) or reference_role(node) is Role.WRITE:
        return False
    if self._certainly_resolves(node.name):
        return False
    if tolerates_unresolvable(node):
        return False
    scope = self._node_scope.get(node)
    binding = self.lookup(node.name, scope, cross_dynamic=True)
    return (
        binding is None
        or binding.kind is BindingKind.IMPLICIT_GLOBAL
        or binding.may_be_absent
    )
def lexical_binding_read(self, node)

The let/const/class binding node reads, or None when node is not a read of one. A read of such a binding resolves for certain, yet may still raise a ReferenceError when it runs before the declaration that ends the binding's temporal dead zone; a caller that needs the binding — to defer it to a call site (EffectSummary.dead_zone_reads) — takes it from here rather than resolving a second time. Whether the read is in fact in the dead zone is an ordering question this layer does not answer — a caller's establishment proof (ModelCache.read_established) decides it against the dominance model.

Expand source code Browse git
def lexical_binding_read(self, node: JsIdentifier) -> Binding | None:
    """
    The `let`/`const`/`class` binding *node* reads, or `None` when *node* is not a read of
    one. A read of such a binding resolves for certain, yet may still raise a
    `ReferenceError` when it runs before the declaration that ends the binding's temporal
    dead zone; a caller that needs the binding — to defer it to a call site
    (`EffectSummary.dead_zone_reads`) — takes it from here rather than resolving a second
    time. Whether the read is in fact in the dead zone is an ordering question this layer
    does not answer — a caller's establishment proof (`ModelCache.read_established`) decides
    it against the dominance model.
    """
    if not self.is_reference(node) or reference_role(node) is Role.WRITE:
        return None
    binding = self.resolve(node)
    return binding if binding is not None and binding.is_lexical else None
def reads_lexical_binding(self, node)

Whether node is a read of a let, const, or class binding. The companion flag to read_may_throw, which instead flags a name that may denote no binding at all; together they are the complete set of reads a discarding context must not drop without a proof the read is past its establishing point. The binding itself, when a caller needs it, comes from lexical_binding_read.

Expand source code Browse git
def reads_lexical_binding(self, node: JsIdentifier) -> bool:
    """
    Whether *node* is a read of a `let`, `const`, or `class` binding. The companion flag to
    `read_may_throw`, which instead flags a name that may denote no binding at all; together
    they are the complete set of reads a discarding context must not drop without a proof the
    read is past its establishing point. The binding itself, when a caller needs it, comes from
    `lexical_binding_read`.
    """
    return self.lexical_binding_read(node) is not None
def read_may_raise_reference_error(self, node)

Whether evaluating node as a read may raise a ReferenceError: it may denote no binding at all (read_may_throw) or it reads a let/const/class binding that may still be in its temporal dead zone (reads_lexical_binding). The one flag a discarding context tests before dropping a read; a context holding an ordering proof clears the dead-zone case through it (EffectModel.read_throws()), a context holding none fails closed and keeps the read (EffectModel._read_effectful_or_throwing).

Expand source code Browse git
def read_may_raise_reference_error(self, node: JsIdentifier) -> bool:
    """
    Whether evaluating *node* as a read may raise a `ReferenceError`: it may denote no binding at
    all (`read_may_throw`) or it reads a `let`/`const`/`class` binding that may still be in its
    temporal dead zone (`reads_lexical_binding`). The one flag a discarding context tests before
    dropping a read; a context holding an ordering proof clears the dead-zone case through it
    (`EffectModel.read_throws`), a context holding none fails closed and keeps the read
    (`EffectModel._read_effectful_or_throwing`).
    """
    return self.read_may_throw(node) or self.reads_lexical_binding(node)
def statement_cannot_throw(self, statement, store_cannot_throw=None)

Whether evaluating statement throws on no run at all, so that a run leaving it for a handler never leaves it half done. Only a declaration of plain names, a function declaration and an expression statement qualify, the first and the last when evaluation_cannot_throw holds for everything they evaluate. Binding a destructuring pattern can throw after its initializer ran, and a var declared inside a with body stores through the object, whose setter may throw, so a declaration with either does not qualify. Reaching a function declaration does nothing, since its function is in place before the statements of its scope run, except where Annex B copies it out of its block: that copy stores the function into the name of the enclosing scope, which is ruled as every other store into a declared name is. A declaration standing as the target of a for-in or for-of head is asked as the store of the value the head hands it on every iteration. store_cannot_throw is handed on to evaluation_cannot_throw.

Expand source code Browse git
def statement_cannot_throw(
    self,
    statement: Node | None,
    store_cannot_throw: Callable[[JsAssignmentExpression], bool] | None = None,
) -> bool:
    """
    Whether evaluating *statement* throws on no run at all, so that a run leaving it for a
    handler never leaves it half done. Only a declaration of plain names, a function declaration
    and an expression statement qualify, the first and the last when `evaluation_cannot_throw`
    holds for everything they evaluate. Binding a destructuring pattern can throw after its
    initializer ran, and a `var` declared inside a `with` body stores through the object, whose
    setter may throw, so a declaration with either does not qualify. Reaching a function
    declaration does nothing, since its function is in place before the statements of its scope
    run, except where Annex B copies it out of its block: that copy stores the function into the
    name of the enclosing scope, which is ruled as every other store into a declared name is. A
    declaration standing as the target of a `for-in` or `for-of` head is asked as the store of
    the value the head hands it on every iteration. *store_cannot_throw* is handed on to
    `evaluation_cannot_throw`.
    """
    if isinstance(statement, JsVariableDeclarator):
        return self._declarator_cannot_throw(statement, store_cannot_throw)
    if isinstance(statement, JsVariableDeclaration):
        return all(
            isinstance(declarator, JsVariableDeclarator)
            and self._declarator_cannot_throw(declarator, store_cannot_throw)
            for declarator in statement.declarations
        )
    if isinstance(statement, JsFunctionDeclaration):
        binding = None if statement.id is None else self.binding_of(statement.id)
        if binding is None:
            return False
        copy = _copy_made_by(binding, statement)
        if copy is None:
            return True
        return self._stores_into_without_throwing(copy.var, statement)
    if isinstance(statement, JsExpressionStatement):
        return self.evaluation_cannot_throw(statement.expression, store_cannot_throw)
    return False
def evaluation_cannot_throw(self, node, store_cannot_throw=None)

Whether evaluating the expression node throws on no run at all. Deliberately minimal: a primitive literal, a function value, a read that neither read_may_raise_reference_error nor read_has_dynamic_effect flags, and a plain = store of such a value to a name, joined by parentheses and the comma operator.

A store to a name cannot throw when the name resolves, through no with body, to a var, a function, a parameter or a catch binding, unless it is a strict store to a script-level var or function. Such a binding is a property of the global object, whose attributes are the host's: the language makes undefined, NaN and Infinity read-only, and a browser gives window, document, top, navigator and many more a getter without a setter, which a strict store refuses and a sloppy one ignores. A let may still be in its temporal dead zone, a const refuses every store, and a function expression's own name and a name nothing declares refuse a strict one. store_cannot_throw vouches for any other store its caller holds a proof for.

Expand source code Browse git
def evaluation_cannot_throw(
    self,
    node: Node | None,
    store_cannot_throw: Callable[[JsAssignmentExpression], bool] | None = None,
) -> bool:
    """
    Whether evaluating the expression *node* throws on no run at all. Deliberately minimal: a
    primitive literal, a function value, a read that neither `read_may_raise_reference_error`
    nor `read_has_dynamic_effect` flags, and a plain `=` store of such a value to a name, joined
    by parentheses and the comma operator.

    A store to a name cannot throw when the name resolves, through no `with` body, to a `var`,
    a function, a parameter or a catch binding, unless it is a strict store to a script-level
    `var` or function. Such a binding is a property of the global object, whose attributes are
    the host's: the language makes `undefined`, `NaN` and `Infinity` read-only, and a browser
    gives `window`, `document`, `top`, `navigator` and many more a getter without a setter,
    which a strict store refuses and a sloppy one ignores. A `let` may still be in its temporal
    dead zone, a `const` refuses every store, and a function expression's own name and a name
    nothing declares refuse a strict one. *store_cannot_throw* vouches for any other store its
    caller holds a proof for.
    """
    if node is None:
        return False
    if isinstance(node, (
        JsStringLiteral,
        JsNumericLiteral,
        JsBooleanLiteral,
        JsNullLiteral,
    )):
        return True
    if isinstance(node, FUNCTION_NODES):
        return True
    if isinstance(node, JsParenthesizedExpression):
        return self.evaluation_cannot_throw(node.expression, store_cannot_throw)
    if isinstance(node, JsSequenceExpression):
        return all(
            self.evaluation_cannot_throw(part, store_cannot_throw) for part in node.expressions
        )
    if isinstance(node, JsIdentifier):
        return (
            self.is_reference(node)
            and not self.read_has_dynamic_effect(node)
            and not self.read_may_raise_reference_error(node)
        )
    if isinstance(node, JsAssignmentExpression):
        if node.operator != '=':
            return False
        target = strip_parens(node.left)
        if not isinstance(target, JsIdentifier):
            return False
        binding = self.resolve(target)
        vouched = binding is not None and self._stores_into_without_throwing(binding, node)
        if not vouched and (store_cannot_throw is None or not store_cannot_throw(node)):
            return False
        return self.evaluation_cannot_throw(node.right, store_cannot_throw)
    return False
def function_names(self, function)

The names function is called and observed through, or None for a function no name holds: an IIFE, a callback, a function stored through anything but a name, which a consumer reads as running where it stands or wherever what holds it hands it on. The own name is the declared name of a function declaration, the one declarator a function or arrow expression initializes, or the name a lone assignment installs it in (f = function(){}), so that a function held in a hoisted var assigned once is ordered by its calls rather than by its creation. A function declared in a block has the var Annex B copies it into as a second name wherever something observes that copy (AnnexBCopy.observed).

A caller confirms that each name is singly declared, binding_pinned_to function, and free of dynamic references before trusting its reads to enumerate every invocation.

Expand source code Browse git
def function_names(self, function: Node) -> FunctionNames | None:
    """
    The names *function* is called and observed through, or `None` for a function no name
    holds: an IIFE, a callback, a function stored through anything but a name, which a consumer
    reads as running where it stands or wherever what holds it hands it on. The own name is the
    declared name of a function declaration, the one declarator a function or arrow expression
    initializes, or the name a lone assignment installs it in (`f = function(){}`), so that a
    function held in a hoisted `var` assigned once is ordered by its calls rather than by its
    creation. A function declared in a block has the `var` Annex B copies it into as a second
    name wherever something observes that copy (`AnnexBCopy.observed`).

    A caller confirms that each name is singly declared, `binding_pinned_to` *function*, and
    free of dynamic references before trusting its reads to enumerate every invocation.
    """
    own = self._invocation_binding(function)
    if own is None:
        return None
    copy = None
    if isinstance(function, JsFunctionDeclaration):
        copy = _copy_made_by(own, function)
    if copy is not None and not copy.observed:
        copy = None
    return FunctionNames(own, copy)
def name_holding(self, read)

The one name the value read at read is stored into, when read is the whole value a declarator or an assignment whose own value is discarded stores into a name. An assignment whose value is used hands it on as well, so its target is not the one name holding the value: f(x = ref) and var y = (x = ref).

Expand source code Browse git
def name_holding(self, read: Node) -> Binding | None:
    """
    The one name the value read at *read* is stored into, when *read* is the whole value a
    declarator or an assignment whose own value is discarded stores into a name. An assignment
    whose value is used hands it on as well, so its target is not the one name holding the
    value: `f(x = ref)` and `var y = (x = ref)`.
    """
    store = enclosing_operator(read)
    if isinstance(store, JsVariableDeclarator):
        if strip_parens(store.init) is not read or not isinstance(store.id, JsIdentifier):
            return None
        return self.binding_of(store.id)
    if not isinstance(store, JsAssignmentExpression):
        return None
    if store.operator != '=' or strip_parens(store.right) is not read:
        return None
    if not value_is_discarded(store):
        return None
    target = strip_parens(store.left)
    if not isinstance(target, JsIdentifier):
        return None
    return self.resolve(target)
def binding_pinned_to(self, binding, function)

Whether binding holds function as its one assigned value, so every read of it outside the value's temporal dead zone denotes function and its reads enumerate function's invocations. True when the binding's only write is the assignment that establishes function — a bare name = function(){} records that target as its sole write — and false once any other write could give the name a different value. A named function declaration or a declarator initializer installs the value with no recorded write, so any write at all is a reassignment that unpins it. The single-declaration and dynamic-reference checks a caller also needs are left to the caller; this answers only the reassignment question — the whole of it, so a write that leaves no writes entry because nothing says what it stored (has_indefinite_write) unpins the name as much as one that does. A copy Annex B makes into the name is a write too, and it unpins the name unless it stores the function its declaration declares (copy_stores_the_function).

Expand source code Browse git
def binding_pinned_to(self, binding: Binding, function: Node) -> bool:
    """
    Whether *binding* holds *function* as its one assigned value, so every read of it outside the
    value's temporal dead zone denotes *function* and its reads enumerate *function*'s invocations.
    True when the binding's only write is the assignment that establishes *function* — a bare
    `name = function(){}` records that target as its sole write — and false once any other write could
    give the name a different value. A named function declaration or a declarator initializer installs
    the value with no recorded write, so any write at all is a reassignment that unpins it. The
    single-declaration and dynamic-reference checks a caller also needs are left to the caller; this
    answers only the reassignment question — the whole of it, so a write that leaves no `writes`
    entry because nothing says what it stored (`has_indefinite_write`) unpins the name as much
    as one that does. A copy Annex B makes into the name is a write too, and it unpins the name
    unless it stores the function its declaration declares (`copy_stores_the_function`).
    """
    parent = function.parent
    establishing = None
    if (
        isinstance(parent, JsAssignmentExpression)
        and parent.operator == '='
        and parent.right is function
    ):
        establishing = strip_parens(parent.left)
    if binding.has_indefinite_write:
        return False
    if not all(self.copy_stores_the_function(copy) for copy in binding.copies_received):
        return False
    return all(write is establishing for write in binding.writes)
def copy_stores_the_function(self, copy)

Whether copy stores the function its declaration declares, and nothing else can come of the function through the block binding: the block binding has that one declaration, no write of any kind and nothing that could rebind it at run time, so it holds the function wherever the copy runs; and every other read of it calls or constructs it, so the one function object the two names share is not handed on, stored or given properties through the name the var cannot see.

Expand source code Browse git
def copy_stores_the_function(self, copy: AnnexBCopy) -> bool:
    """
    Whether *copy* stores the function its declaration declares, and nothing else can come of
    the function through the block binding: the block binding has that one declaration, no write
    of any kind and nothing that could rebind it at run time, so it holds the function wherever
    the copy runs; and every other read of it calls or constructs it, so the one function object
    the two names share is not handed on, stored or given properties through the name the `var`
    cannot see.
    """
    block = copy.block
    if (
        len(block.declarations) != 1
        or block.writes
        or block.has_indefinite_write
        or block.dynamic_refs
        or self.binding_maybe_reassigned_dynamically(block)
    ):
        return False
    return all(
        isinstance(read, JsFunctionDeclaration) or is_constructed_or_invoked(read)
        for read in block.reads
    )
def binding_values(self, binding, *, ignore_dynamic_rebinds=False)

Every value expression the text stores under binding through a channel that spells its stored value, in no promised order, and whether that list is complete — whether no other channel can give the name a value. The readable channels are a declarator's initializer, the function or class of a declaration, and the right side of a plain = written through the referencing identifier. Every other way a value can arrive makes the answer incomplete without contributing a value: every parameter is incomplete, because the call site is a value channel this model cannot see, and so are a catch or import binding and a function expression's own name; a compound assignment, an update, a for-in/for-of head, and a destructuring target store a value they do not spell; a write recorded with no value and a dynamic rebinding (binding_maybe_reassigned_dynamically) say a value arrived without saying which. A write through a member access on a global-object alias is left unread and breaks completeness too: which member accesses are such writes depends on the names holding the global object, and those are found from these values (_bindings_holding_the_global_object), so a value read out of such a write would feed the very set that decides whether it is one. A binding with no declaration — an implicit global, and the binding _ensure_implicit_global_from_alias_write mints — contributes nothing and is never complete, even where the file assigns it by name. With ignore_dynamic_rebinds the dynamic-rebind conjunct is left out of the completeness verdict: the values answer what the text spells, and whether a rebind crosses a given read is the caller's ordering question over binding_dynamic_rebind_sites.

The values hold wherever the name is not in their temporal dead zone; a bare declarator contributes no value even though the name reads undefined there, and a consumer that needs a value established before a use orders it separately (binding_establishment_sites). A recognizer whose safe direction is admitting decides on ANY value and ignores completeness; a consumer whose rewrite needs the binding to hold nothing else requires completeness first, and must also require a value, since a complete empty list answers every universal question vacuously. refinery.lib.scripts.js.analysis.intrinsics._binding_value_roots is the may-side sibling that over-approximates where this list refuses, and Binding.store_sites the one that lists where a value is stored rather than which.

Expand source code Browse git
def binding_values(
    self, binding: Binding | None, *, ignore_dynamic_rebinds: bool = False,
) -> tuple[list[Node], bool]:
    """
    Every value expression the text stores under *binding* through a channel that spells its stored
    value, in no promised order, and whether that list is complete — whether no other channel can
    give the name a value.
    The readable channels are a declarator's initializer, the function or class of a declaration,
    and the right side of a plain `=` written through the referencing identifier. Every other way a
    value can arrive makes the answer incomplete without contributing a value: **every** parameter
    is incomplete, because the call site is a value channel this model cannot see, and so are a
    catch or import binding and a function expression's own name; a compound assignment, an update,
    a `for-in`/`for-of` head, and a destructuring target store a value they do not spell; a write
    recorded with no value and a dynamic rebinding (`binding_maybe_reassigned_dynamically`) say a
    value arrived without saying which. A write through a member access on a global-object alias
    is left unread and breaks completeness too: which member accesses are such writes depends on
    the names holding the global object, and those are found from these values
    (`_bindings_holding_the_global_object`), so a value read out of such a write would feed the
    very set that decides whether it is one. A binding with no declaration — an implicit global,
    and the binding `_ensure_implicit_global_from_alias_write` mints — contributes nothing and
    is never complete, even where the file assigns it by name. With *ignore_dynamic_rebinds* the
    dynamic-rebind conjunct is left out of the completeness verdict: the values answer what the
    text spells, and whether a rebind crosses a given read is the caller's ordering question
    over `binding_dynamic_rebind_sites`.

    The values hold wherever the name is not in their temporal dead zone; a bare declarator
    contributes no value even though the name reads `undefined` there, and a consumer that needs a
    value established before a use orders it separately (`binding_establishment_sites`). A
    recognizer whose safe direction is admitting decides on ANY value and ignores completeness; a
    consumer whose rewrite needs the binding to hold nothing else requires completeness first, and
    must also require a value, since a complete empty list answers every universal question
    vacuously. `refinery.lib.scripts.js.analysis.intrinsics._binding_value_roots` is the
    may-side sibling that over-approximates where this list refuses, and `Binding.store_sites`
    the one that lists where a value is stored rather than which.
    """
    channels, complete = self._binding_value_channels(
        binding, ignore_dynamic_rebinds=ignore_dynamic_rebinds)
    return [value for _, value in channels], complete
def values_at_call(self, binding, arguments)

binding_values read at one call site: arguments maps each parameter binding of the called function to the argument that call supplies for it (_argument_parameter_map), and for a binding it covers, the mapped argument is the entry channel binding_values cannot see — so the answer can be complete where the plain query never is. A parameter the call supplies no argument for maps to None and stays incomplete, unless the parameter list gives it a default; the default is then one of its values, as it is beside an argument that may be undefined. Every other rule is binding_values' own: a write the text spells no value for, and any dynamic rebinding — a direct eval in the function, and a write through its own arguments object — still poison the answer. For a binding arguments does not cover, the answer is exactly binding_values.

Expand source code Browse git
def values_at_call(
    self,
    binding: Binding | None,
    arguments: dict[Binding | None, Node | None],
) -> tuple[list[Node], bool]:
    """
    `binding_values` read at one call site: *arguments* maps each parameter binding of the
    called function to the argument that call supplies for it (`_argument_parameter_map`), and
    for a binding it covers, the mapped argument is the entry channel `binding_values` cannot
    see — so the answer can be complete where the plain query never is. A parameter the
    call supplies no argument for maps to `None` and stays incomplete, unless the parameter list
    gives it a default; the default is then one of its values, as it is beside an argument that
    may be `undefined`. Every other rule is
    `binding_values`' own: a write the text spells no value for, and any dynamic rebinding —
    a direct `eval` in the function, and a write through its own `arguments` object — still
    poison the answer. For a binding *arguments* does not cover, the answer is exactly
    `binding_values`.
    """
    channels, complete = self._binding_value_channels(binding, arguments)
    return [value for _, value in channels], complete
def values_through_calls(self, binding)

binding_values, with a parameter read through the calls of its function the text shows: its values are its own writes, its default, and the argument each such call passes at its position (values_at_call). A call shown is one _calls_shown lists: an invocation of a name holding the function, its own name included, of a table entry it may select, or of the function where it is written; for a class constructor, a new of its class the same way, and a super call in the constructor of a class extending it. A value one of these calls passes is listed whatever other calls exist and whatever the other parameters are; only a spread at or before the position, or arguments the call does not write out, hide which argument the call passes there.

The list is complete only when no call can come another way (_calls_shown) and every shown call passes an argument at the position, or the parameter has a default. Every consumer reads a parameter through here, so none can forget a way a call comes. Memoized per binding.

Expand source code Browse git
def values_through_calls(self, binding: Binding | None) -> tuple[list[Node], bool]:
    """
    `binding_values`, with a parameter read through the calls of its function the text shows:
    its values are its own writes, its default, and the argument each such call passes at its
    position (`values_at_call`). A call shown is one `_calls_shown` lists: an invocation of a
    name holding the function, its own name included, of a table entry it may select, or of
    the function where it is written; for a class constructor, a `new` of its class the same
    way, and a `super` call in the constructor of a class extending it. A value one of these
    calls passes is listed whatever other calls exist and whatever the other parameters are;
    only a spread at or before the position, or arguments the call does not write out, hide
    which argument the call passes there.

    The list is complete only when no call can come another way (`_calls_shown`) and every
    shown call passes an argument at the position, or the parameter has a default. Every
    consumer reads a parameter through here, so none can forget a way a call comes. Memoized
    per binding.
    """
    if binding is None or not binding.declarations:
        return self.binding_values(binding)
    declared = _declared_parameter(binding.declarations[0])
    if declared is None:
        return self.binding_values(binding)
    cached = self._values_through_calls.get(binding)
    if cached is not None:
        return cached
    function, position, default = declared
    values, _ = self.binding_values(binding)
    listed = {id(value) for value in values}

    def list_value(value: Node) -> None:
        if id(value) not in listed:
            listed.add(id(value))
            values.append(value)

    calls, complete = self._calls_shown(function)
    for call in calls:
        argument, hidden = _passed_at(call, position)
        if hidden:
            complete = False
            continue
        received, received_all = self.values_at_call(binding, {binding: argument})
        complete = complete and received_all
        for value in received:
            list_value(value)
    if default is not None:
        list_value(default)
    cached = self._values_through_calls[binding] = values, complete
    return cached
def arguments_through_calls(self, function, position)

What an element of the arguments of function holds: the arguments the calls of the function the text shows (values_through_calls) pass at position, or at any position when it is None. The list is complete only when no call can come another way (_calls_shown) and every shown call passes an argument there that no spread hides.

Expand source code Browse git
def arguments_through_calls(self, function: JsFunctionNode, position: int | None) -> tuple[list[Node], bool]:
    """
    What an element of the `arguments` of *function* holds: the arguments the calls of the
    function the text shows (`values_through_calls`) pass at *position*, or at any position
    when it is `None`. The list is complete only when no call can come another way
    (`_calls_shown`) and every shown call passes an argument there that no spread hides.
    """
    calls, complete = self._calls_shown(function)
    passed: list[Node] = []
    for call in calls:
        if position is None:
            if call.arguments is None:
                complete = False
                continue
            for argument in call.arguments:
                argument = strip_parens(argument)
                if isinstance(argument, JsSpreadElement):
                    complete = False
                elif argument is not None:
                    passed.append(argument)
            continue
        argument, _ = _passed_at(call, position)
        if argument is None:
            complete = False
        else:
            passed.append(argument)
    return passed, complete
def parameter_never_passed(self, binding, *, own_keys=False)

Whether the function of the parameter binding is called, and no call can pass an argument to the parameter, which then holds undefined or its default whenever the function starts: it is a name of a parameter list, plain or with a default, every way to its function is a call the text shows (_calls_shown, with own_keys), there is one at least, and none of those passes an argument at its position or hides which argument it passes there.

Expand source code Browse git
def parameter_never_passed(self, binding: Binding, *, own_keys: bool = False) -> bool:
    """
    Whether the function of the parameter *binding* is called, and no call can pass an argument
    to the parameter, which then holds `undefined` or its default whenever the function starts:
    it is a name of a parameter list, plain or with a default, every way to its function is a
    call the text shows (`_calls_shown`, with *own_keys*), there is one at least, and none of
    those passes an argument at its position or hides which argument it passes there.
    """
    if len(binding.declarations) != 1:
        return False
    declared = _declared_parameter(binding.declarations[0])
    if declared is None:
        return False
    function, position, _ = declared
    calls, complete = self._calls_shown(function, own_keys=own_keys)
    if not complete or not calls:
        return False
    for call in calls:
        argument, hidden = _passed_at(call, position)
        if argument is not None or hidden:
            return False
    return True
def shown_calls(self, function)

Every call and construction that runs function, which are the expressions its return value becomes (_calls_shown), or None where one may come another way.

Expand source code Browse git
def shown_calls(
    self,
    function: JsFunctionNode,
) -> list[JsCallExpression | JsNewExpression] | None:
    """
    Every call and construction that runs *function*, which are the expressions its return
    value becomes (`_calls_shown`), or `None` where one may come another way.
    """
    calls, complete = self._calls_shown(function)
    return [call.node for call in calls] if complete else None
def reached_only_by_shown_calls(self, function, *, own_keys=False)

Whether every way to function is a call the text shows or a test of its names (SemanticModel._calls_shown, with own_keys), and no class extends it, so that nothing reads a property of it: the static code of a class extending it reads its properties through super.

Expand source code Browse git
def reached_only_by_shown_calls(self, function: JsFunctionNode, *, own_keys: bool = False) -> bool:
    """
    Whether every way to *function* is a call the text shows or a test of its names
    (`SemanticModel._calls_shown`, with *own_keys*), and no class extends it, so that nothing
    reads a property of it: the static code of a class extending it reads its properties
    through `super`.
    """
    if not self._calls_shown(function, own_keys=own_keys)[1]:
        return False
    return not any(
        _extended_by(read) is not None
        for _, reads in self._shown_names(function)
        for read in reads
    )
def binding_value_sites(self, binding)

Every node whose execution stores into binding (Binding.store_sites), or None where a value may arrive at no such node: the call writes the name before any statement runs (Binding.written_at_entry, a parameter, a catch or import binding, a function expression's own name), some access writes it without saying where (Binding.has_indefinite_write, a dynamic rebinding), the host supplies it, or a function it declares is in place before any statement of its scope runs. Ordering the sites against a point is the caller's, since that needs the dominance model this layer must not depend on.

Expand source code Browse git
def binding_value_sites(self, binding: Binding | None) -> list[ReferenceNode] | None:
    """
    Every node whose execution stores into *binding* (`Binding.store_sites`), or `None` where a
    value may arrive at no such node: the call writes the name before any statement runs
    (`Binding.written_at_entry`, a parameter, a catch or import binding, a function expression's
    own name), some access writes it without saying where (`Binding.has_indefinite_write`, a
    dynamic rebinding), the host supplies it, or a function it declares is in place before any
    statement of its scope runs. Ordering the sites against a point is the caller's, since that
    needs the dominance model this layer must not depend on.
    """
    if (
        binding is None
        or not binding.declarations
        or binding.written_at_entry
        or binding.has_indefinite_write
        or binding.supplied_by_host
        or self.binding_maybe_reassigned_dynamically(binding)
        or any(
            _where_it_stores(binding, declaration) is _DeclarationStore.ON_ENTRY
            for declaration in binding.declarations
        )
    ):
        return None
    return binding.store_sites
def singular_value(self, binding)

The single value node a binding provably holds: the sole entry of a complete binding_values answer. None when the binding is absent, stores more than one value, or has any channel the text does not spell — a name whose declaration carries a value and is then assigned holds two values across its life and is refused, as is every parameter. The value is what the name denotes wherever it is not in the value's temporal dead zone; a consumer that also needs the value established before a use orders it separately, since a bare-assignment binding reads undefined before its write. EffectModel.function_of() is the function-typed specialization of this query, and it is the value-resolution the bare-assignment recognition sites route through instead of re-deriving binding shapes.

Expand source code Browse git
def singular_value(self, binding: Binding | None) -> Node | None:
    """
    The single value node a *binding* provably holds: the sole entry of a complete
    `binding_values` answer. `None` when the binding is absent, stores more than one value, or has
    any channel the text does not spell — a name whose declaration carries a value and is then
    assigned holds two values across its life and is refused, as is every parameter. The value is
    what the name denotes wherever it is not in the value's temporal dead zone; a consumer that also
    needs the value established before a use orders it separately, since a bare-assignment binding
    reads `undefined` before its write. `EffectModel.function_of` is the function-typed specialization
    of this query, and it is the value-resolution the bare-assignment recognition sites route through
    instead of re-deriving binding shapes.
    """
    channels, complete = self._binding_value_channels(binding)
    if not complete or len(channels) != 1:
        return None
    return channels[0][1]
def binding_establishment_sites(self, binding, *, ignore_dynamic_rebinds=False)

The nodes that must all have executed before binding's singular_value is installed, for a consumer that gates a use on execution order. An empty list when the value is hoisted into place before any statement runs — a function declaration — so no ordering is required; the declaration when the value is a function a copy Annex B makes stores there (AnnexBCopy); the declarator when the value is a var/let/const initializer, which is absent until that declarator runs; the class declaration when the value is a class, which is in its temporal dead zone until it runs; the recorded write when a lone assignment installs it (f = function(){}, the form namespace flattening leaves). None when the binding holds no single such value, so its presence cannot be ordered and the caller declines — decided by the same complete-singleton binding_values answer singular_value requires, so the two queries can never disagree about which bindings have an orderable value: one returns the value and the other the node that establishes it. With ignore_dynamic_rebinds that answer is read on the view binding_values documents, the one a positioned consumer orders rebind hazards against itself. Ordering the returned nodes against the use is the caller's job, since that needs the dominance model this layer must not depend on.

Expand source code Browse git
def binding_establishment_sites(
    self, binding: Binding | None, *, ignore_dynamic_rebinds: bool = False,
) -> list[Node] | None:
    """
    The nodes that must all have executed before *binding*'s `singular_value` is installed, for a
    consumer that gates a use on execution order. An empty list when the value is hoisted into
    place before any statement runs — a function declaration — so no ordering is required; the
    declaration when the value is a function a copy Annex B makes stores there (`AnnexBCopy`);
    the declarator when the value is a `var`/`let`/`const` initializer, which is absent until that
    declarator runs; the class declaration when the value is a class, which is in its temporal
    dead zone until it runs; the recorded write when a lone assignment installs it
    (`f = function(){}`, the form namespace flattening leaves). `None` when the binding holds no
    single such value, so its presence cannot be ordered and the caller declines — decided by the
    same complete-singleton `binding_values` answer `singular_value` requires, so the two queries
    can never disagree about which bindings have an orderable value: one returns the value and
    the other the node that establishes it. With *ignore_dynamic_rebinds* that answer is read on
    the view `binding_values` documents, the one a positioned consumer orders rebind hazards
    against itself. Ordering the returned nodes against the use is the caller's job, since that
    needs the dominance model this layer must not depend on.
    """
    if binding is None:
        return None
    channels, complete = self._binding_value_channels(
        binding, ignore_dynamic_rebinds=ignore_dynamic_rebinds)
    if not complete or len(channels) != 1:
        return None
    site, _ = channels[0]
    if isinstance(site, JsFunctionDeclaration):
        return [site] if _copy_received_by(binding, site) is not None else []
    return [site]
def is_shadowed(self, name, at, outer)

Whether name, referenced at at, resolves to a binding declared strictly inside outer rather than in outer itself or an enclosing scope. This replaces the various hand-rolled shadowing checks: a name shadowed below outer does not refer to outer's binding.

Expand source code Browse git
def is_shadowed(self, name: str, at: Node, outer: Scope) -> bool:
    """
    Whether *name*, referenced at *at*, resolves to a binding declared strictly inside *outer*
    rather than in *outer* itself or an enclosing scope. This replaces the various hand-rolled
    shadowing checks: a name shadowed below *outer* does not refer to *outer*'s binding.
    """
    binding = None
    if isinstance(at, JsIdentifier) and at.name == name:
        binding = self._copy_written_at(at)
    if binding is None:
        binding = self.lookup(name, self._node_scope.get(at))
    if binding is None:
        return False
    return outer.contains(binding.scope, strict=True)
def may_run_as_strict(self, node)

Whether the code at node may run as strict code: the file may be an ES module, which is strict code throughout whatever its text says, or the text makes it strict.

Expand source code Browse git
def may_run_as_strict(self, node: Node) -> bool:
    """
    Whether the code at *node* may run as strict code: the file may be an ES module, which is
    strict code throughout whatever its text says, or the text makes it strict.
    """
    return self.file_kind.may_be_an_es_module or strict_mode_at(node)
def annex_b_copy_observed(self, declaration)

Whether anything may observe the copy Annex B makes of the function declaration, declared in a block, or the var that copy declares: the var is read, written, named in a with body, exported, carried by an object handed to a call, or reachable by reflection. A write observes the var because without it the same assignment lands on whatever the name means outside the function, or creates a global. A declaration this model was not built over may be observed.

Expand source code Browse git
def annex_b_copy_observed(self, declaration: JsFunctionDeclaration) -> bool:
    """
    Whether anything may observe the copy Annex B makes of the function *declaration*, declared
    in a block, or the `var` that copy declares: the `var` is read, written, named in a `with`
    body, exported, carried by an object handed to a call, or reachable by reflection. A write
    observes the `var` because without it the same assignment lands on whatever the name means
    outside the function, or creates a global. A declaration this model was not built over may
    be observed.
    """
    if declaration.id is None:
        return False
    block = self.binding_of(declaration.id)
    if block is None:
        return True
    return any(
        copy.observed
        or copy.var.writes
        or copy.var.has_indefinite_write
        or self.reflection_can_reach(copy.var)
        for copy in block.copies_made
        if copy.declaration is declaration
    )
def declaration_may_land(self, names, scope, *, exclude=())

Whether declarations of names may be introduced directly in scope: none of them captures a use already meaningful there (would_capture), and none lands on a binding the host supplies (Binding.supplied_by_host), which no identifier of the text spells for that walk to find. At the top of a CommonJS file a let of a parameter's name makes Node refuse the file as CommonJS, and a var of it starts out holding what the host passed rather than undefined.

None lands at the top of a script under a name a host reaches (host_reaches_global) either: there a var is the property of the global object the host calls, and a let shadows it, where the declaration it came from bound a name of its own body. A Function body declaring var onload and spliced at the top would install a load handler the window then calls once more.

Expand source code Browse git
def declaration_may_land(
    self,
    names: set[str],
    scope: Scope,
    *,
    exclude: Collection[Binding] = (),
) -> bool:
    """
    Whether declarations of *names* may be introduced directly in *scope*: none of them captures
    a use already meaningful there (`would_capture`), and none lands on a binding the host
    supplies (`Binding.supplied_by_host`), which no identifier of the text spells for that walk
    to find. At the top of a CommonJS file a `let` of a parameter's name makes Node refuse the
    file as CommonJS, and a `var` of it starts out holding what the host passed rather than
    `undefined`.

    None lands at the top of a script under a name a host reaches (`host_reaches_global`)
    either: there a `var` is the property of the global object the host calls, and a `let`
    shadows it, where the declaration it came from bound a name of its own body. A `Function`
    body declaring `var onload` and spliced at the top would install a load handler the window
    then calls once more.
    """
    if (
        scope is self.root_scope
        and not self.file_kind.scopes_declarations_to_the_file
        and any(self.host_reaches_global(name) for name in names)
    ):
        return False
    for name in names:
        binding = scope.bindings.get(name)
        if binding is not None and binding.supplied_by_host:
            return False
    return not self.would_capture(names, scope, exclude=exclude)
def would_capture(self, names, scope, *, exclude=())

Whether introducing a binding for any of names directly in scope would capture an identifier already meaningful there. Every use-position occurrence of one of names within scope, including in a nested function that would close over the new binding, must already resolve to a binding strictly nested below scope (see is_shadowed); otherwise that occurrence — free, inherited from an enclosing scope, or bound in scope itself — would be rebound by the introduced declaration.

A use resolving to a binding in exclude is skipped: the edit the caller asks this for deletes that binding with the same splice that introduces the declaration, so the use is carried off rather than captured. The natural binding is what decides membership — a resolution walked past an excluded binding answers where the name would land after the edit, which is the question the callers below _admit_reflected_body ask of their free names, not the one this capture rule asks of a use the tree still holds.

Expand source code Browse git
def would_capture(
    self, names: set[str], scope: Scope, *, exclude: Collection[Binding] = (),
) -> bool:
    """
    Whether introducing a binding for any of *names* directly in *scope* would capture an
    identifier already meaningful there. Every use-position occurrence of one of *names* within
    *scope*, including in a nested function that would close over the new binding, must already
    resolve to a binding strictly nested below *scope* (see `is_shadowed`); otherwise that
    occurrence — free, inherited from an enclosing scope, or bound in *scope* itself — would be
    rebound by the introduced declaration.

    A use resolving to a binding in *exclude* is skipped: the edit the caller asks this for
    deletes that binding with the same splice that introduces the declaration, so the use is
    carried off rather than captured. The natural binding is what decides membership — a
    resolution walked past an excluded binding answers where the name would land *after* the
    edit, which is the question the callers below `_admit_reflected_body` ask of their free
    names, not the one this capture rule asks of a use the tree still holds.
    """
    return next(self._capturing_uses(names, scope, exclude), None) is not None
def captured_names(self, names, scope, *, exclude=())

Those of names for which would_capture holds, read in one walk of scope.

Expand source code Browse git
def captured_names(
    self, names: set[str], scope: Scope, *, exclude: Collection[Binding] = (),
) -> set[str]:
    """
    Those of *names* for which `would_capture` holds, read in one walk of *scope*.
    """
    return {node.name for node in self._capturing_uses(names, scope, exclude)}
def has_reflection_surface(self)

Whether the program still contains a construct through which code could reference a global by name at runtime: a value-read of the eval or Function intrinsic in any form — a direct or indirect call, an alias (var e = eval), a comma sequence ((0, eval)), or a member access (window.eval, g['Function']) — a string-valued timer, a dynamic property read on the global object (window[expr]), a with statement, or a span of source this model never read, which may spell a name nothing here records. Computed conservatively (over-reporting is safe): while any such surface remains, a dead global must not be removed, because reflective code may read it. A computed global write names no global (has_opaque_global_write owns that question) and is not counted here.

Expand source code Browse git
def has_reflection_surface(self) -> bool:
    """
    Whether the program still contains a construct through which code could reference a global
    by name at runtime: a value-read of the `eval` or `Function` intrinsic in any form — a
    direct or indirect call, an alias (`var e = eval`), a comma sequence (`(0, eval)`), or a
    member access (`window.eval`, `g['Function']`) — a string-valued timer, a dynamic property
    read on the global object (`window[expr]`), a `with` statement, or a span of source this
    model never read, which may spell a name nothing here records. Computed conservatively
    (over-reporting is safe): while any such surface remains, a dead global must not be removed,
    because reflective code may read it. A computed global *write* names no global
    (`has_opaque_global_write` owns that question) and is not counted here.
    """
    self._ensure_reflection_detected()
    assert self._reflection_surface is not None
    return self._reflection_surface
def reached_without_its_name(self, function)

Whether function may be called, constructed or handed out where no reference of a name holding it stands: the program may read the caller of a function (spells_caller), which is whichever function called the one it is read on; the body may reach the function through its own arguments (arguments_name_their_function); or the body reads new.target (references_new_target), which is the function itself wherever new runs it. The one answer every reader asking how a function may be invoked or obtained shares, memoized per function.

Expand source code Browse git
def reached_without_its_name(self, function: Node) -> bool:
    """
    Whether *function* may be called, constructed or handed out where no reference of a name
    holding it stands: the program may read the `caller` of a function (`spells_caller`), which
    is whichever function called the one it is read on; the body may reach the function through
    its own `arguments` (`arguments_name_their_function`); or the body reads `new.target`
    (`references_new_target`), which is the function itself wherever `new` runs it. The one
    answer every reader asking how a function may be invoked or obtained shares, memoized per
    function.
    """
    cached = self._reached_unnamed.get(function)
    if cached is None:
        cached = self._reached_unnamed[function] = (
            self.spells_caller()
            or arguments_name_their_function(function)
            or references_new_target(function)
        )
    return cached
def spells_caller(self)

Whether the program may read the caller of a function: it spells the name as a property it reads or destructures, in a string, or as a name that no binding of the program holds, which a with body may resolve to a property. In sloppy mode g.caller is whichever function called g, so such a program can reach every function that calls another one without naming it. A variable named caller and a key of that name in an object literal or a class read no such property. Memoized.

Expand source code Browse git
def spells_caller(self) -> bool:
    """
    Whether the program may read the `caller` of a function: it spells the name as a property
    it reads or destructures, in a string, or as a name that no binding of the program holds,
    which a `with` body may resolve to a property. In sloppy mode `g.caller` is whichever
    function called `g`, so such a program can reach every function that calls another one
    without naming it. A variable named `caller` and a key of that name in an object literal or
    a class read no such property. Memoized.
    """
    if self._spells_caller is None:
        self._spells_caller = any(self._may_read_caller_at(node) for node in self.root.walk())
    return self._spells_caller
def host_reaches_global(self, name)

Whether a host reaches the property name of the global object from outside the file: the host environment calls the event handler stored there (HostEnvironment.calls_handler()), or the analyst declared the name one it reaches (host_entrypoint).

Expand source code Browse git
def host_reaches_global(self, name: str) -> bool:
    """
    Whether a host reaches the property *name* of the global object from outside the file: the
    host *environment* calls the event handler stored there
    (`refinery.lib.scripts.js.analysis.environment.HostEnvironment.calls_handler`), or the
    analyst declared the name one it reaches (*host_entrypoint*).
    """
    if self.environment.calls_handler(name):
        return True
    return self._host_entrypoint is not None and self._host_entrypoint(name)
def named_by_the_host(self, binding)

Whether a host may reach binding by name: it reaches the property of the global object under that name (host_reaches_global), and the binding is that property (reaches_global_object), itself or through a var Annex B copies it into. The name alone does not decide it: a nested binding, a let, or anything a module scopes to the file never becomes a property of the global object. A declared object is reached as much as a declared function, and the host may call a method installed on it whenever the script hands it control.

Expand source code Browse git
def named_by_the_host(self, binding: Binding) -> bool:
    """
    Whether a host may reach *binding* by name: it reaches the property of the global object
    under that name (`host_reaches_global`), and the binding is that property
    (`reaches_global_object`), itself or through a `var` Annex B copies it into. The name alone
    does not decide it: a nested binding, a `let`, or anything a module scopes to the file never
    becomes a property of the global object. A declared object is reached as much as a declared
    function, and the host may call a method installed on it whenever the script hands it
    control.
    """
    if not self.host_reaches_global(binding.name):
        return False
    if any(self.reaches_global_object(copy.var) for copy in binding.copies_made):
        return True
    return self.reaches_global_object(binding)
def reached_from_outside(self, binding)

Whether code outside the file may reach binding by name at some point through the global object: a host names it (named_by_the_host), or the file is a library, holding nothing but function declarations at its top (declares_only_functions), and binding names one of them as a property of the global object, which code that loads the library calls once the file has run. An importer reaching an export is not among them: that is Binding.exported, which a caller asks beside this. A pass that could delete such a declaration, fold a read of one into its value, or relocate one out of the global scope asks here before doing so, so that the answer is one predicate's rather than each pass's own.

Expand source code Browse git
def reached_from_outside(self, binding: Binding) -> bool:
    """
    Whether code outside the file may reach *binding* by name at some point through the global
    object: a host names it (`named_by_the_host`), or the file is a library, holding nothing but
    function declarations at its top (`declares_only_functions`), and *binding* names one of
    them as a property of the global object, which code that loads the library calls once the
    file has run. An importer reaching an export is not among them: that is
    `Binding.exported`, which a caller asks beside this. A pass that could delete such a
    declaration, fold a read of one into its value, or relocate one out of the global scope
    asks here before doing so, so that the answer is one predicate's rather than each pass's
    own.
    """
    if self.named_by_the_host(binding):
        return True
    return (
        self.declares_only_functions()
        and self.reaches_global_object(binding)
        and any(
            isinstance(function := declaration.parent, JsFunctionDeclaration)
            and function.parent is self.root
            for declaration in binding.declarations
        )
    )
def reached_without_a_read(self, binding)

Whether code may reach binding where no read of it in the file stands: an importer reads it as an export, the call writes it on entry (Binding.written_at_entry), a body the file does not read is handed the object carrying it (Binding.reachable_through_a_handed_object), reflection reaches it (reflection_can_reach), or code outside the file reaches it by name (reached_from_outside). A reader asking whether the reads of a name are every way to what it holds asks here.

Expand source code Browse git
def reached_without_a_read(self, binding: Binding) -> bool:
    """
    Whether code may reach *binding* where no read of it in the file stands: an importer reads
    it as an export, the call writes it on entry (`Binding.written_at_entry`), a body the file
    does not read is handed the object carrying it
    (`Binding.reachable_through_a_handed_object`), reflection reaches it
    (`reflection_can_reach`), or code outside the file reaches it by name
    (`reached_from_outside`). A reader asking whether the reads of a name are every way to what
    it holds asks here.
    """
    return (
        binding.exported
        or binding.written_at_entry
        or binding.reachable_through_a_handed_object
        or self.reflection_can_reach(binding)
        or self.reached_from_outside(binding)
    )
def importer_may_run_first(self, binding)

Whether binding is exported from a module an importer can run in before its body has finished (module_may_be_reentered), so the importer can read it at a point the file's text does not order.

Expand source code Browse git
def importer_may_run_first(self, binding: Binding) -> bool:
    """
    Whether *binding* is exported from a module an importer can run in before its body has
    finished (`module_may_be_reentered`), so the importer can read it at a point the file's
    text does not order.
    """
    return binding.exported and self.module_may_be_reentered()
def callable_from_outside(self, function)

Whether code outside the file can call function while the file's own text still runs, at a point that text does not order: a host calls a function it names (named_by_the_host) whenever the script hands it control, and an importer in an import cycle calls an export, one exported by name or an anonymous export default function, before the module's body has finished (importer_may_run_first). The functions of a library (reached_from_outside) are not among them, and neither are the exports an importer outside every cycle reaches: code that loads the file calls those once it has run.

Expand source code Browse git
def callable_from_outside(self, function: Node) -> bool:
    """
    Whether code outside the file can call *function* while the file's own text still runs, at
    a point that text does not order: a host calls a function it names (`named_by_the_host`)
    whenever the script hands it control, and an importer in an import cycle calls an export,
    one exported by name or an anonymous `export default function`, before the module's body
    has finished (`importer_may_run_first`). The functions of a library
    (`reached_from_outside`) are not among them, and neither are the exports an importer
    outside every cycle reaches: code that loads the file calls those once it has run.
    """
    names = self.function_names(function)
    for binding in () if names is None else names.bindings:
        if self.importer_may_run_first(binding) or self.named_by_the_host(binding):
            return True
    return (
        isinstance(function.parent, JsExportDefaultDeclaration)
        and self.module_may_be_reentered()
    )
def declares_only_functions(self)

Whether the top level of the file holds function declarations and nothing else. Memoized.

Expand source code Browse git
def declares_only_functions(self) -> bool:
    """
    Whether the top level of the file holds function declarations and nothing else. Memoized.
    """
    if self._declares_only_functions is None:
        body = self.root.body
        self._declares_only_functions = bool(body) and all(
            isinstance(statement, JsFunctionDeclaration) for statement in body
        )
    return self._declares_only_functions
def constructions_may_be_hidden(self)

Whether a function the program constructs may be constructed with arguments no call the text shows passes it: a constructor may escape (constructors_may_escape), or the program constructs one without naming it (_constructs_unseen), through a constructor property or through new.target. Memoized.

Expand source code Browse git
def constructions_may_be_hidden(self) -> bool:
    """
    Whether a function the program constructs may be constructed with arguments no call the
    text shows passes it: a constructor may escape (`constructors_may_escape`), or the program
    constructs one without naming it (`_constructs_unseen`), through a `constructor` property or
    through `new.target`. Memoized.
    """
    if self._constructions_hidden is None:
        self._constructions_hidden = self.constructors_may_escape() or any(
            _constructs_unseen(node, self.assumed_keys) for node in self.root.walk()
        )
    return self._constructions_hidden
def constructors_may_escape(self)

Whether a constructor may run on a receiver or with arguments of the program's choice: the program spells "constructor" in a string or in a concatenation of constant strings, reads a constructor property other than to construct with it, compare it, read its name or test it, or destructures one. An instance reaches the function that built it through that property, and so does its prototype. Memoized.

Expand source code Browse git
def constructors_may_escape(self) -> bool:
    """
    Whether a constructor may run on a receiver or with arguments of the program's choice: the
    program spells `"constructor"` in a string or in a concatenation of constant strings, reads
    a `constructor` property other than to construct with it, compare it, read its name or test
    it, or destructures one. An instance reaches the function that built it through that
    property, and so does its prototype. Memoized.
    """
    if self._constructors_escape is None:
        concatenations: list[JsBinaryExpression] = []
        escapes = False
        for node in self.root.walk():
            if isinstance(node, JsBinaryExpression):
                if node.operator == '+':
                    concatenations.append(node)
            elif _lets_a_constructor_escape(node):
                escapes = True
                break
        self._constructors_escape = escapes or concatenation_spells(concatenations, 'constructor')
    return self._constructors_escape
def strings_still_unseen(self)

The names of STRINGS_READ_ANYWHERE whose readers answer no for this program, so that a string spelling one anywhere would change an answer: one spelling constructor lets a constructor escape (constructors_may_escape), and one spelling caller may read the function that called another (spells_caller).

Expand source code Browse git
def strings_still_unseen(self) -> frozenset[str]:
    """
    The names of `refinery.lib.scripts.js.keys.STRINGS_READ_ANYWHERE` whose readers answer no
    for this program, so that a string spelling one anywhere would change an answer: one
    spelling `constructor` lets a constructor escape (`constructors_may_escape`), and one
    spelling `caller` may read the function that called another (`spells_caller`).
    """
    unseen: set[str] = set()
    if not self.constructors_may_escape():
        unseen.add('constructor')
    if not self.spells_caller():
        unseen.add('caller')
    return frozenset(unseen)
def has_opaque_global_write(self)

Whether the program stores a property on the global object under a key only the runtime resolves (window[expr] = x), or installs one there under such a key (opaque_global_write), so an intrinsic or a script-scope name may hold something else than what the text spells once the program runs. The read-naming question has_reflection_surface answers is unaffected by such a write — storing a property names nothing and runs nothing — but the replacement questions are not: a written key may be Math, String, or the name a top-level var carries, so a consumer that trusts an intrinsic by name, or that a script-scope binding keeps its spelled value, refuses while this holds.

Detection is model-aware, distinct from the spelling-level exemption _is_reflective_member grants the same sites: a base may be the global object here (may_be_the_global_object), so a local holding the object (var g = globalThis; g[k] = 1<code>) and the receiver a sloppy call supplies a write through (</code>this[k] = 1) are both counted, not only its spelled names — the alias would otherwise store a global under a key the spelling never saw, and the receiver is the one spelling a callee can choose freely. Every store form counts (is_member_write_target: plain and compound assignment, update, delete, for-in/for-of heads, destructuring patterns), so the fact stands on its own wherever a consumer consults it. Computed lazily and memoized, but never while _build_def_use is still recording references: which member accesses reach the global object depends on the names holding it, which that step finds from the writes it records, so an answer taken inside it would depend on how far it had got; asked there, the conservative True is answered instead of a partial fact.

Expand source code Browse git
def has_opaque_global_write(self) -> bool:
    """
    Whether the program stores a property on the global object under a key only the runtime
    resolves (`window[expr] = x`), or installs one there under such a key
    (`opaque_global_write`), so an intrinsic or a script-scope name may hold something else
    than what the text spells once the program runs. The read-naming question
    `has_reflection_surface` answers is unaffected by such a write — storing a property names
    nothing and runs nothing — but the *replacement* questions are not: a written key may be
    `Math`, `String`, or the name a top-level `var` carries, so a consumer that trusts an
    intrinsic by name, or that a script-scope binding keeps its spelled value, refuses while
    this holds.

    Detection is model-aware, distinct from the spelling-level exemption
    `_is_reflective_member` grants the same sites: a base may be the global object here
    (`may_be_the_global_object`), so a local holding the object (`var g = globalThis;
    g[k] = 1`) and the receiver a sloppy call supplies a write through (`this[k] = 1`) are
    both counted, not only its spelled names — the alias would otherwise store a global under
    a key the spelling never saw, and the receiver is the one spelling a callee can choose
    freely. Every store form counts
    (`is_member_write_target`: plain and compound assignment, update, `delete`, `for-in`/`for-of`
    heads, destructuring patterns), so the fact stands on its own wherever a consumer consults it.
    Computed lazily and memoized,
    but never while `_build_def_use` is still recording references: which member accesses reach
    the global object depends on the names holding it, which that step finds from the writes it
    records, so an answer taken inside it would depend on how far it had got; asked there, the
    conservative `True` is answered instead of a partial fact.
    """
    if self._recording_def_use:
        return True
    if not self._opaque_global_write_sites_known:
        self._compute_opaque_global_write_sites()
    return self._opaque_global_write is True
def opaque_global_write_sites(self)

The member expressions storing a property on the global object under a key only the runtime resolves — the located form of the fact has_opaque_global_write reports, for a consumer that orders the fact's consequences rather than refusing on it. None when the fact holds without a site to order: an observed hand-over of the object to a callee that may write it, or an answer taken while the reference-recording walk is still running, where the boolean answers True for the same reason. A consumer that turns sites into kills treats None as volatility it cannot locate.

Expand source code Browse git
def opaque_global_write_sites(self) -> list[JsMemberExpression] | None:
    """
    The member expressions storing a property on the global object under a key only the runtime
    resolves — the located form of the fact `has_opaque_global_write` reports, for a consumer
    that orders the fact's consequences rather than refusing on it. `None` when the fact holds
    without a site to order: an observed hand-over of the object to a callee that may write it,
    or an answer taken while the reference-recording walk is still running, where the boolean
    answers `True` for the same reason. A consumer that turns sites into kills treats `None` as
    volatility it cannot locate.
    """
    if self._recording_def_use:
        return None
    if not self._opaque_global_write_sites_known:
        self._compute_opaque_global_write_sites()
    sites = self._opaque_global_write_sites
    assert sites is not None
    if sites or self._opaque_global_write is not True:
        return sites
    return None
def opaque_global_write_replacement_sites(self, binding)

The opaque global writes that could replace the value binding holds, or None when that question cannot be answered in sites. Only a script-scope name is replaceable — it is a property of the global object under the script execution model the write stores to — so a binding in any other scope answers None: its reflection hazards, if any, are not this write's. A script-scope name under a reflection surface answers None too, since any surface could write the name from anywhere, and so does the fact when it holds without a site (opaque_global_write_sites). None therefore means the binding is reflection-reachable for reasons no located site spells, and a consumer that turned sites into kills keeps the value volatile instead.

Expand source code Browse git
def opaque_global_write_replacement_sites(self, binding: Binding) -> list[JsMemberExpression] | None:
    """
    The opaque global writes that could replace the value *binding* holds, or `None` when that
    question cannot be answered in sites. Only a script-scope name is replaceable — it is a
    property of the global object under the script execution model the write stores to — so a
    binding in any other scope answers `None`: its reflection hazards, if any, are not this
    write's. A script-scope name under a reflection surface answers `None` too, since any surface
    could write the name from anywhere, and so does the fact when it holds without a site
    (`opaque_global_write_sites`). `None` therefore means the binding is reflection-reachable for
    reasons no located site spells, and a consumer that turned sites into kills keeps the value
    volatile instead.
    """
    owner = binding.scope.var_scope
    if owner is None or self._named_by_global_code(binding):
        if self.has_reflection_surface():
            return None
        return self.opaque_global_write_sites()
    return None
def reflection_can_reach(self, binding)

Whether a runtime name lookup could read or write binding without a reference this model records. Derived over the precise dynamic-scope facts. A global is reachable through any reflective surface — eval, Function, a string timer, dynamic global access, with — all of which run in the global scope, so it defers to the whole-program has_reflection_surface, and by an opaque global write rebinding its name (has_opaque_global_write), which no reference records either. A function-local is reachable only from within its own function and only by name: a with body that names it (a dynamic_references entry), a direct eval in the function (local_reachable_by_direct_eval), or a span of the function this model never read (unread_source_can_reach), which may spell the name where nothing records that it does. A with that never names it cannot reach it, and reflective code in the global scope cannot name a local — so the local answer is exact, while the global one stays conservative (any surface).

Where the file is a module of either kind a declaration of the top-level scope is scoped to the file: global-scope code cannot name it, so it is reached the way a function-local is, from the file's own text only (reflection_can_reach_from_its_scope). An implicit global stays a global.

Expand source code Browse git
def reflection_can_reach(self, binding: Binding) -> bool:
    """
    Whether a runtime name lookup could read or write *binding* without a reference this model
    records. Derived over the precise dynamic-scope facts. A global is reachable through any
    reflective surface — `eval`, `Function`, a string timer, dynamic global access, `with` — all
    of which run in the global scope, so it defers to the whole-program
    `has_reflection_surface`, and by an opaque global write rebinding its name
    (`has_opaque_global_write`), which no reference records either. A function-local is
    reachable only from within its own function and only by name: a `with` body that names
    it (a `dynamic_references` entry), a direct `eval` in the function
    (`local_reachable_by_direct_eval`), or a span of the function this model never read
    (`unread_source_can_reach`), which may spell the name where nothing records that it does.
    A `with` that never names it cannot reach it, and reflective code in the global scope
    cannot name a local — so the local answer is exact, while the global one stays
    conservative (any surface).

    Where the file is a module of either kind a declaration of the top-level scope is scoped to
    the file: global-scope code cannot name it, so it is reached the way a function-local is,
    from the file's own text only (`reflection_can_reach_from_its_scope`). An implicit global
    stays a global.
    """
    if self._named_by_global_code(binding):
        return self.has_reflection_surface() or self.has_opaque_global_write()
    return self.reflection_can_reach_from_its_scope(binding)
def reflection_can_reach_from_its_scope(self, binding)

Whether code standing in the scope of binding that this model cannot read could name it: a with body that names it, a direct eval in its function, or a span of that function this model never read. The scope of an implicit global is the global scope, where every reflective surface runs. Code in the global scope outside the file is not asked about, which is the whole question for a binding scoped to a function or to a module and one half of it for a declaration of a script's top scope (reflection_can_reach).

Expand source code Browse git
def reflection_can_reach_from_its_scope(self, binding: Binding) -> bool:
    """
    Whether code standing in the scope of *binding* that this model cannot read could name it: a
    `with` body that names it, a direct `eval` in its function, or a span of that function this
    model never read. The scope of an implicit global is the global scope, where every
    reflective surface runs. Code in the global scope outside the file is not asked about, which
    is the whole question for a binding scoped to a function or to a module and one half of it
    for a declaration of a script's top scope (`reflection_can_reach`).
    """
    owner = binding.scope.var_scope
    if owner is None or binding.kind is BindingKind.IMPLICIT_GLOBAL:
        return self.has_reflection_surface() or self.has_opaque_global_write()
    return (
        bool(binding.dynamic_refs)
        or self._function_has_direct_eval(owner.node)
        or bool(self._unread_source_sites(owner.node))
    )
def reachable_by_opaque_reflection(self, binding)

Whether an opaque reflective surface — a value-read of eval or Function, a string timer, a dynamic access on the global object, or a span of source this model never read — could name binding at runtime with no reference this model records. Unlike reflection_can_reach, a with body is not counted: a with that names the binding is attributed precisely as a dynamic_references entry, so a caller that already consults dynamic_refs needs only the opaque surfaces here, the ones that leave no attributable reference. A global is reachable through any such surface, all of which run in the global scope; a function-local only through a direct eval or an unread span in its own function, since a surface running in the global scope cannot name a local. The boolean companion of reflection_surface_sites — true exactly when that site list is non-empty.

Expand source code Browse git
def reachable_by_opaque_reflection(self, binding: Binding) -> bool:
    """
    Whether an opaque reflective surface — a value-read of `eval` or `Function`, a string timer,
    a dynamic access on the global object, or a span of source this model never read — could
    name *binding* at runtime with no reference this model records. Unlike
    `reflection_can_reach`, a `with` body is not counted: a `with` that names the binding is
    attributed precisely as a `dynamic_references` entry, so a caller that already consults
    `dynamic_refs` needs only the opaque surfaces here, the ones that leave no attributable
    reference. A global is reachable through any such surface, all of which run in the global
    scope; a function-local only through a direct `eval` or an unread span in its own function,
    since a surface running in the global scope cannot name a local. The boolean companion of
    `reflection_surface_sites` — true exactly when that site list is non-empty.
    """
    return bool(self.reflection_surface_sites(binding))
def reflection_surface_sites(self, binding)

The AST nodes of the opaque reflective surfaces that could name binding at runtime with no reference this model records — the points no reflected invocation of it can precede. A caller ranks a definition against these to prove it runs before every such invocation, the site-level companion of reachable_by_opaque_reflection. For a global (script-scope) binding they are the whole-program opaque surfaces (opaque_reflection_sites), each running in the global scope and able to name any global; for a function-local, the direct eval sites in its owning function (_direct_eval_sites) and the spans of that function this model never read (_unread_source_sites), the only opaque surfaces that stand in the local's own scope and can name it. Empty exactly when the binding is not opaque-reflection reachable. A with surface is not included — a with that names the binding is attributed as a dynamic_references entry a caller consults separately.

Expand source code Browse git
def reflection_surface_sites(self, binding: Binding) -> list[Node]:
    """
    The AST nodes of the opaque reflective surfaces that could name *binding* at runtime with no
    reference this model records — the points no reflected invocation of it can precede. A
    caller ranks a definition against these to prove it runs before every such invocation, the
    site-level companion of `reachable_by_opaque_reflection`. For a global (script-scope)
    binding they are the whole-program opaque surfaces (`opaque_reflection_sites`), each
    running in the global scope and able to name any global; for a function-local, the direct
    `eval` sites in its owning function (`_direct_eval_sites`) and the spans of that function
    this model never read (`_unread_source_sites`), the only opaque surfaces that stand in the
    local's own scope and can name it. Empty exactly when the binding is not opaque-reflection
    reachable. A `with` surface is not included — a `with` that names the binding is attributed
    as a `dynamic_references` entry a caller consults separately.
    """
    owner = binding.scope.var_scope
    if owner is None or self._named_by_global_code(binding):
        return self.opaque_reflection_sites()
    return self._direct_eval_sites(owner.node) + self._unread_source_sites(owner.node)
def local_reachable_by_direct_eval(self, binding)

Whether a direct eval positioned to name binding could read or write it with no reference this model records. True only for a function-local whose owning function — or a closure nested inside it, which inherits its scope — contains a direct eval, the one reflective surface that runs in the caller's own scope and can therefore name a local. False for a global: an opaque global-scope surface can name any global, but that is what the whole-program reflection_can_reach answers, and freezing every global on it is an over-approximation the caller must choose to accept, not a fact this query asserts. The with surface is not counted — a with body's accesses are attributed precisely as dynamic_references, so only the opaque eval case needs this per-function answer. A binding of the top scope that may be absent is a local of the CommonJS wrapper where it exists, and counts as one (_named_by_code_of_its_function).

Expand source code Browse git
def local_reachable_by_direct_eval(self, binding: Binding) -> bool:
    """
    Whether a direct `eval` positioned to name *binding* could read or write it with no reference this
    model records. True only for a function-local whose owning function — or a closure nested inside
    it, which inherits its scope — contains a direct `eval`, the one reflective surface that runs in
    the caller's own scope and can therefore name a local. False for a global: an opaque global-scope
    surface can name any global, but that is what the whole-program `reflection_can_reach` answers, and
    freezing every global on it is an over-approximation the caller must choose to accept, not a fact
    this query asserts. The `with` surface is not counted — a `with` body's accesses are attributed
    precisely as `dynamic_references`, so only the opaque `eval` case needs this per-function answer.
    A binding of the top scope that may be absent is a local of the CommonJS wrapper where it
    exists, and counts as one (`_named_by_code_of_its_function`).
    """
    owner = binding.scope.var_scope
    if owner is None or not self._named_by_code_of_its_function(binding):
        return False
    return self._function_has_direct_eval(owner.node)
def unread_source_can_reach(self, binding)

Whether a span of source this model never read stands where it could name binding. Such a span is text the file holds at a definite position, and nothing says what it references, so everything in scope where it stands may be read or written by it with no reference this model records. A binding of the script is within reach of every span in the file; a function-local only of one inside its own function, since no span outside it can name a local.

This is not folded into the eval answers, even though both surfaces are opaque, because the two are known to different degrees. Whether an eval anywhere in a file rebinds a given global is a question about text no one has, and freezing every global on it is the over-approximation local_reachable_by_direct_eval documents as refused; an unread span is the file's own text, standing in one place, and refusing to count it is what drops the write that text spells.

Expand source code Browse git
def unread_source_can_reach(self, binding: Binding) -> bool:
    """
    Whether a span of source this model never read stands where it could name *binding*. Such a
    span is text the file holds at a definite position, and nothing says what it references, so
    everything in scope where it stands may be read or written by it with no reference this
    model records. A binding of the script is within reach of every span in the file; a
    function-local only of one inside its own function, since no span outside it can name a
    local.

    This is not folded into the `eval` answers, even though both surfaces are opaque, because
    the two are known to different degrees. Whether an `eval` anywhere in a file rebinds a given
    global is a question about text no one has, and freezing every global on it is the
    over-approximation `local_reachable_by_direct_eval` documents as refused; an unread span is
    the file's own text, standing in one place, and refusing to count it is what drops the write
    that text spells.
    """
    owner = binding.scope.var_scope
    if owner is None or self._named_by_global_code(binding):
        return bool(self._unread_source_sites(self.root))
    return bool(self._unread_source_sites(owner.node))
def free_name_reachable_by_direct_eval(self, node)

Whether a direct eval could have installed a binding that a free name at node reads instead of the global one. resolve answering None means this model saw no declaration of the name, which is not the same as there being none: eval('var undefined = 4') declares one that no reference here records, and a read of that name afterwards is the binding, not the global.

Only var and function declarations escape an eval — a let inside one lives in a scope discarded with the call — so a binding it installs lands in the var scope the call itself stands in, and is visible at node exactly when that var scope contains node's scope. This is the mirror of local_reachable_by_direct_eval, which asks whether an eval can name a binding that already exists and therefore counts one nested below the binding's owner; a nested eval declares into its own function and so is not counted here.

An eval whose own argument contains node is excluded, and that exclusion is about order rather than scope: the arguments of a call are evaluated before the call runs, so the code the eval is about to execute cannot have declared anything the argument reads. Without it, eval(atob('...')) — the shape most of this tool's corpus is written in — would refuse to read atob on the strength of the very eval it is decoding the body of.

Expand source code Browse git
def free_name_reachable_by_direct_eval(self, node: Node) -> bool:
    """
    Whether a direct `eval` could have installed a binding that a free name at *node* reads instead
    of the global one. `resolve` answering `None` means this model saw no declaration of the name,
    which is not the same as there being none: `eval('var undefined = 4')` declares one that no
    reference here records, and a read of that name afterwards is the binding, not the global.

    Only `var` and function declarations escape an `eval` — a `let` inside one lives in a scope
    discarded with the call — so a binding it installs lands in the var scope the call itself stands
    in, and is visible at *node* exactly when that var scope contains *node*'s scope. This is the
    mirror of `local_reachable_by_direct_eval`, which asks whether an `eval` can name a binding that
    already exists and therefore counts one nested *below* the binding's owner; a nested `eval`
    declares into its own function and so is not counted here.

    An `eval` whose own argument contains *node* is excluded, and that exclusion is about order
    rather than scope: the arguments of a call are evaluated before the call runs, so the code the
    `eval` is about to execute cannot have declared anything the argument reads. Without it,
    `eval(atob('...'))` — the shape most of this tool's corpus is written in — would refuse to read
    `atob` on the strength of the very `eval` it is decoding the body of.
    """
    scope = self.scope_of(node)
    if scope is None:
        return True
    return any(
        owner is None or owner.contains(scope)
        for owner in self._var_scopes_of_evals_before(node)
    )
def direct_eval_declares_below(self, node, outer)

Whether a direct eval could have installed a var that a name written at node finds before its lookup reaches the scope outer: one whose var scope contains the scope of node and lies strictly inside outer. The bounded form of free_name_reachable_by_direct_eval, for a caller about to declare the name in outer.

Expand source code Browse git
def direct_eval_declares_below(self, node: Node, outer: Scope) -> bool:
    """
    Whether a direct `eval` could have installed a `var` that a name written at *node* finds
    before its lookup reaches the scope *outer*: one whose var scope contains the scope of
    *node* and lies strictly inside *outer*. The bounded form of
    `free_name_reachable_by_direct_eval`, for a caller about to declare the name in *outer*.
    """
    scope = self.scope_of(node)
    if scope is None:
        return True
    return any(
        owner is None or owner.contains(scope) and outer.contains(owner, strict=True)
        for owner in self._var_scopes_of_evals_before(node)
    )
def bare_name_reaches_the_host(self, name, at)

Whether a bare name read at at denotes what the host supplies under that name: nothing binds it where at stands, not even an assignment the model records as an IMPLICIT_GLOBAL, no with body between at and the global scope may supply it from its object, and no direct eval may have declared it (free_name_reachable_by_direct_eval). at need not be an identifier, so a pass can ask before it writes the name there, as one that rewrites a this to globalThis must.

Expand source code Browse git
def bare_name_reaches_the_host(self, name: str, at: Node) -> bool:
    """
    Whether a bare *name* read at *at* denotes what the host supplies under that name: nothing
    binds it where *at* stands, not even an assignment the model records as an
    `IMPLICIT_GLOBAL`, no `with` body between *at* and the global scope may supply it from its
    object, and no direct `eval` may have declared it (`free_name_reachable_by_direct_eval`).
    *at* need not be an identifier, so a pass can ask before it writes the name there, as one
    that rewrites a `this` to `globalThis` must.
    """
    scope = self.scope_of(at)
    return (
        scope is not None
        and self.lookup(name, scope) is None
        and not crosses_dynamic_scope(scope)
        and not self.free_name_reachable_by_direct_eval(at)
    )
def binding_dynamic_rebind_sites(self, binding)

The AST nodes at which a dynamic scope could rebind binding, or None for the one such rebind that holds no node to order — the write a call makes on entry (written_at_entry), which the text does not spell. The located form of binding_maybe_reassigned_dynamically, which is re-derived from this answer, so the two can never disagree about which bindings are volatile: a consumer that gets a list holds a hazard per node, and one that gets None holds the nodeless kill. Each leg of the boolean contributes its nodes — a write through an object that aliases the binding (indefinite_writes), a with-body reference whose role is not a plain read, a direct eval in the owning function, and a span of source the model never read — with the two scope lines the boolean draws drawn identically: the eval leg is a function-local's only (a global is not frozen on a global-scope surface, and a binding of the top scope that may be absent is a local of the CommonJS wrapper where it exists), and the unread-source leg reaches a global from anywhere in the file but a local only from its own function. An empty list is a binding no dynamic scope can rebind. Memoized per binding once the model is built: while it is, the references made through objects aliasing a binding are still being recorded, and an answer taken then would depend on how far that had got.

Expand source code Browse git
def binding_dynamic_rebind_sites(self, binding: Binding) -> list[Node] | None:
    """
    The AST nodes at which a dynamic scope could rebind *binding*, or `None` for the one such
    rebind that holds no node to order — the write a call makes on entry (`written_at_entry`),
    which the text does not spell. The located form of
    `binding_maybe_reassigned_dynamically`, which is re-derived from this answer, so the two
    can never disagree about which bindings are volatile: a consumer that gets a list holds a
    hazard per node, and one that gets `None` holds the nodeless kill. Each leg of the boolean
    contributes its nodes — a write through an object that aliases the binding
    (`indefinite_writes`), a `with`-body reference whose role is not a plain read, a direct
    `eval` in the owning function, and a span of source the model never read — with the two
    scope lines the boolean draws drawn identically: the eval leg is a function-local's only
    (a global is not frozen on a global-scope surface, and a binding of the top scope that may
    be absent is a local of the CommonJS wrapper where it exists), and the unread-source leg
    reaches a global from anywhere in the file but a local only from its own function. An
    empty list is a binding no dynamic scope can rebind. Memoized per binding once the model is
    built: while it is, the references made through objects aliasing a binding are still being
    recorded, and an answer taken then would depend on how far that had got.
    """
    if binding in self._dynamic_rebind_sites:
        return self._dynamic_rebind_sites[binding]
    sites = self._dynamic_rebind_sites_of(binding)
    if self._built:
        self._dynamic_rebind_sites[binding] = sites
    return sites
def binding_maybe_reassigned_dynamically(self, binding)

Whether a dynamic scope could rebind binding — give the name a new value through a surface the static writes set does not record. A with body that names it as an assignment target may rebind it (the target may instead be a property of the with object, but may equally be this binding, so it is treated as a possible rebind), a direct eval in its owning function can rebind it opaquely, and so can a span of source this model never read (unread_source_can_reach), whose text may spell an assignment to the name. A member write or method call through the name does not rebind it — the name keeps its value — so only a dynamic reference whose role is not a plain read counts. A write through an object that aliases the binding — indefinite_writes — is counted here too: it replaces the value under the name while leaving no entry that says with what. A consumer that judges a binding's value stable from writes alone must also consult this, since none of these reassignments leaves a writes entry; a script-scope binding reassigned only through an opaque eval stays the documented residual, as local_reachable_by_direct_eval reports it false there. The boolean form of binding_dynamic_rebind_sites: true exactly when that answer is None or holds a node.

Expand source code Browse git
def binding_maybe_reassigned_dynamically(self, binding: Binding) -> bool:
    """
    Whether a dynamic scope could rebind *binding* — give the name a new value through a surface
    the static `writes` set does not record. A `with` body that names it as an assignment target
    may rebind it (the target may instead be a property of the `with` object, but may equally be
    this binding, so it is treated as a possible rebind), a direct `eval` in its owning function
    can rebind it opaquely, and so can a span of source this model never read
    (`unread_source_can_reach`), whose text may spell an assignment to the name. A member write
    or method call through the name does not rebind it — the name keeps its value — so only a
    dynamic reference whose role is not a plain read counts. A write through an object that
    aliases the binding — `indefinite_writes` — is counted here too: it replaces the value under
    the name while leaving no entry that says with what. A consumer that judges a binding's
    value stable from `writes` alone must also consult this, since none of these reassignments
    leaves a `writes` entry; a script-scope binding reassigned only through an opaque `eval`
    stays the documented residual, as `local_reachable_by_direct_eval` reports it false there.
    The boolean form of `binding_dynamic_rebind_sites`: true exactly when that answer is
    `None` or holds a node.
    """
    sites = self.binding_dynamic_rebind_sites(binding)
    return sites is None or bool(sites)
def binding_reflection_kill_sites(self, binding)

The AST nodes at which a reflective surface could change the value binding holds — the located kills a flow query orders a read against — or None when the question has no located answer.

A script-scope binding is replaceable only by an opaque global write, and the existing opaque_global_write_replacement_sites answer is the whole of it: None under any whole-program reflective surface, which could write the name from anywhere, and the member sites otherwise. A function-local can change value only through a surface standing in its own scope: a direct eval in its owning function or a span of that function this model never read (reflection_surface_sites), or a reference a with body resolves at runtime (dynamic_references). Every with-governed reference counts, read or not — reading the bare name consults the with object first, and a getter there runs code that can rebind the local — so only a located answer keeps that hazard orderable rather than refusing on it. Each site is a node a consumer that has ordered a definition against a use can also order the kill against; None is the volatility it cannot.

Expand source code Browse git
def binding_reflection_kill_sites(self, binding: Binding) -> list[Node] | None:
    """
    The AST nodes at which a reflective surface could change the value *binding* holds — the
    located kills a flow query orders a read against — or `None` when the question has no
    located answer.

    A script-scope binding is replaceable only by an opaque global write, and the existing
    `opaque_global_write_replacement_sites` answer is the whole of it: `None` under any
    whole-program reflective surface, which could write the name from anywhere, and the member
    sites otherwise. A function-local can change value only through a surface standing in its
    own scope: a direct `eval` in its owning function or a span of that function this model
    never read (`reflection_surface_sites`), or a reference a `with` body resolves at runtime
    (`dynamic_references`). Every `with`-governed reference counts, read or not — reading the
    bare name consults the `with` object first, and a getter there runs code that can rebind
    the local — so only a located answer keeps that hazard orderable rather than refusing on
    it. Each site is a node a consumer that has ordered a definition against a use can also
    order the kill against; `None` is the volatility it cannot.
    """
    owner = binding.scope.var_scope
    if owner is None or self._named_by_global_code(binding):
        return self.opaque_global_write_replacement_sites(binding)
    return self.reflection_surface_sites(binding) + self.dynamic_references(binding)
def holds_what_the_host_passed(self, binding)

Whether binding is supplied by the host and still holds what the host passed wherever it is read: nothing in the file declares or writes it, and no direct eval or span of source this model never read stands where it could. Where the ES-module reading leaves the name to a global, no reflective surface may reach it either.

Expand source code Browse git
def holds_what_the_host_passed(self, binding: Binding) -> bool:
    """
    Whether *binding* is supplied by the host and still holds what the host passed wherever it
    is read: nothing in the file declares or writes it, and no direct `eval` or span of source
    this model never read stands where it could. Where the ES-module reading leaves the name to
    a global, no reflective surface may reach it either.
    """
    return (
        binding.supplied_by_host
        and not binding.declarations
        and not binding.writes
        and self.binding_dynamic_rebind_sites(binding) == []
        and not (binding.may_be_absent and self.reflection_can_reach(binding))
    )
def module_exports_unread(self)

Whether nothing reads what the file stores in module.exports. Outside the file, only code that loads it reads its exports, and the run assumes none does unless told otherwise (exports_read_outside). Node's module registry is part of loading: a file reaching itself through require.cache, require.main, process.mainModule or a require or import() of its own path reads its exports as a loader does. Inside the file, the object the store replaces is read through the module the host passed, which is therefore named only as the target of a store module.exports = v, as the operand of typeof, or compared with null, and through the arguments of the function the host wraps around the file, which the file does not name, nor reaches through a function's caller. Memoized.

Expand source code Browse git
def module_exports_unread(self) -> bool:
    """
    Whether nothing reads what the file stores in `module.exports`. Outside the file, only code
    that loads it reads its exports, and the run assumes none does unless told otherwise
    (`exports_read_outside`). Node's module registry is part of loading: a file reaching itself
    through `require.cache`, `require.main`, `process.mainModule` or a `require` or `import()`
    of its own path reads its exports as a loader does. Inside the file, the object the store
    replaces is read through the `module` the host passed, which is therefore named only as the
    target of a store `module.exports = v`, as the operand of `typeof`, or compared with `null`,
    and through the `arguments` of the function the host wraps around the file, which the file
    does not name, nor reaches through a function's `caller`. Memoized.
    """
    if self._module_exports_unread is None:
        self._module_exports_unread = self._compute_module_exports_unread()
    return self._module_exports_unread
def supplied_from_outside(self, binding)

Whether a name resolving to binding holds something the file does not supply: it resolves to no binding, so to a global the host may define, or to one the host passed and the file never replaces (holds_what_the_host_passed). A callee read through such a name runs nothing the file declares.

Expand source code Browse git
def supplied_from_outside(self, binding: Binding | None) -> bool:
    """
    Whether a name resolving to *binding* holds something the file does not supply: it resolves
    to no binding, so to a global the host may define, or to one the host passed and the file
    never replaces (`holds_what_the_host_passed`). A callee read through such a name runs
    nothing the file declares.
    """
    return binding is None or self.holds_what_the_host_passed(binding)
def binding_never_reassigned(self, binding)

Whether binding holds one value for its whole lifetime: it is never written after its declaration, statically (writes) or through a dynamic scope (binding_maybe_reassigned_dynamically). This is the value-stability contract a caller needs before treating the binding's initializer as its value everywhere — distinct from the orderability contract dynamic_refs expresses (whether every reference can be ranked), which a with-body read violates while a stable value does not. It does not itself require a single declaration; a caller that needs one checks declarations alongside.

Expand source code Browse git
def binding_never_reassigned(self, binding: Binding) -> bool:
    """
    Whether *binding* holds one value for its whole lifetime: it is never written after its
    declaration, statically (`writes`) or through a dynamic scope
    (`binding_maybe_reassigned_dynamically`). This is the value-stability contract a caller needs
    before treating the binding's initializer as its value everywhere — distinct from the
    orderability contract `dynamic_refs` expresses (whether every reference can be ranked), which a
    `with`-body read violates while a stable value does not. It does not itself require a single
    declaration; a caller that needs one checks `declarations` alongside.
    """
    return not binding.writes and not self.binding_maybe_reassigned_dynamically(binding)
def reaches_global_object(self, binding)

Whether binding is a property of the global object at runtime — the global a free name in global-scope reflected code (a Function body, an indirect eval, a string timer) resolves to. An implicit global always is. A top-level var/function declaration is where the file is a script; in a module of either kind it is scoped to the file and never reaches the global. A top-level let/const/class, or any binding nested below the script, is a distinct lexical binding that global-scope code cannot see.

Expand source code Browse git
def reaches_global_object(self, binding: Binding) -> bool:
    """
    Whether *binding* is a property of the global object at runtime — the global a free name in
    global-scope reflected code (a `Function` body, an indirect `eval`, a string timer) resolves to.
    An implicit global always is. A top-level `var`/function declaration is where the file is a
    script; in a module of either kind it is scoped to the file and never reaches the global. A
    top-level `let`/`const`/`class`, or any binding nested below the script, is a distinct
    lexical binding that global-scope code cannot see.
    """
    if binding.kind is BindingKind.IMPLICIT_GLOBAL:
        return True
    if self.file_kind.scopes_declarations_to_the_file:
        return False
    return (
        binding.scope is self.root_scope
        and binding.is_hoisted
    )
def module_may_be_reentered(self)

Whether code of another module can run after this module is linked and before its body has finished (_lets_another_module_run). A module in an import cycle with this one can then call an exported function before the declarations after its definition have run. A module that imports nothing is in no cycle and runs its whole body before any importer does, so every importer calls into it after its last statement.

Expand source code Browse git
def module_may_be_reentered(self) -> bool:
    """
    Whether code of another module can run after this module is linked and before its body has
    finished (`_lets_another_module_run`). A module in an import cycle with this one can then
    call an exported function before the declarations after its definition have run. A module
    that imports nothing is in no cycle and runs its whole body before any importer does, so
    every importer calls into it after its last statement.
    """
    return self._module_may_be_reentered
def code_load_sites(self)

The calls by which the program has Node's require load code its text does not hold (_loads_code_from_outside): a file, a package, or a built-in module that runs code. What such code does is unknown; what a loaded module commonly does is set up globals, such as a window for code written for a page or an AMD define. A question that orders a point against code the text does not hold takes these as sites (TamperingModel, and the typeof of a global name through TamperingModel.code_load_may_precede()). They are no reflection surface: that would withdraw every trust the whole program is asked for from a file that holds one in a function it never calls. Memoized.

Expand source code Browse git
def code_load_sites(self) -> list[Node]:
    """
    The calls by which the program has Node's `require` load code its text does not hold
    (`_loads_code_from_outside`): a file, a package, or a built-in module that runs code. What
    such code does is unknown; what a loaded module commonly does is set up globals, such as a
    `window` for code written for a page or an AMD `define`. A question that orders a point
    against code the text does not hold takes these as sites
    (`refinery.lib.scripts.js.analysis.tampering.TamperingModel`, and the `typeof` of a global
    name through
    `refinery.lib.scripts.js.analysis.tampering.TamperingModel.code_load_may_precede`). They
    are no reflection surface: that would withdraw every trust the whole program is asked for
    from a file that holds one in a function it never calls. Memoized.
    """
    if self._code_load_sites is None:
        self._code_load_sites = [
            site for node in self.root.walk()
            if isinstance(node, (JsIdentifier, JsMemberExpression))
            and (site := self._loads_code_from_outside(node)) is not None
        ]
    return self._code_load_sites
def imports_code(self)

Whether the file has the host load code its text does not hold before its body runs: an import or an export … from declaration (_lets_another_module_run) naming a module that is not a built-in one running no code (_specifier_loads_code). The host links and evaluates every such module before the first statement of the file, wherever the declaration stands, so no point of the file is ordered before the load.

Expand source code Browse git
def imports_code(self) -> bool:
    """
    Whether the file has the host load code its text does not hold before its body runs: an
    `import` or an `export … from` declaration (`_lets_another_module_run`) naming a module
    that is not a built-in one running no code (`_specifier_loads_code`). The host links and
    evaluates every such module before the first statement of the file, wherever the
    declaration stands, so no point of the file is ordered before the load.
    """
    for statement in self.root.body:
        if not isinstance(statement, (JsImportDeclaration, JsExportAllDeclaration, JsExportNamedDeclaration)):
            continue
        if not _lets_another_module_run(statement):
            continue
        source = statement.source
        if self._specifier_loads_code(None if source is None else source.value):
            return True
    return False
def opaque_reflection_sites(self)

The AST nodes of the whole-program opaque reflective surfaces — a value-read of the eval/Function intrinsic, a reflective global-object member, a string-valued timer, an import(), or a span of source this model never read. A with statement is not opaque (its body's accesses are attributed as dynamic references) and is excluded. Computed once and memoized; empty exactly when the program has no opaque surface, which _has_opaque_reflection_surface reports as its non-emptiness.

Expand source code Browse git
def opaque_reflection_sites(self) -> list[Node]:
    """
    The AST nodes of the whole-program opaque reflective surfaces — a value-read of the
    `eval`/`Function` intrinsic, a reflective global-object member, a string-valued timer, an
    `import()`, or a span of source this model never read. A `with` statement is not opaque (its
    body's accesses are attributed as dynamic references) and is excluded. Computed once and
    memoized; empty exactly when the program has no opaque surface, which
    `_has_opaque_reflection_surface` reports as its non-emptiness.
    """
    self._ensure_reflection_detected()
    assert self._opaque_surface_sites is not None
    return self._opaque_surface_sites
def global_alias_member_name(self, member)

The name of the global that a member access on a global-object alias references (globalThis.g, window['g'] → g), or None when member is not such an access. The alias must be an unshadowed GLOBAL_OBJECT_ALIASES identifier (a local window names an ordinary object, not the global) with a statically known property name, and the access must not cross a dynamic scope, where the alias could be rebound or the target could be a with-object property — in either case the model cannot claim the reference denotes a global.

A this where the top level holds one denotes the global object only where the file is a script (file_kind): in an ES module it denotes nothing, and in a CommonJS file that file's exports.

Expand source code Browse git
def global_alias_member_name(self, member: JsMemberExpression) -> str | None:
    """
    The name of the global that a member access on a global-object alias references
    (`globalThis.g`, `window['g']` → `g`), or `None` when *member* is not such an access. The alias
    must be an unshadowed `GLOBAL_OBJECT_ALIASES` identifier (a local `window` names an ordinary
    object, not the global) with a statically known property name, and the access must not cross a
    dynamic scope, where the alias could be rebound or the target could be a `with`-object property —
    in either case the model cannot claim the reference denotes a global.

    A `this` where the top level holds one denotes the global object only where the file is a
    script (`file_kind`): in an ES module it denotes nothing, and in a CommonJS file that file's
    exports.
    """
    if (
        self.file_kind.scopes_declarations_to_the_file
        and isinstance(strip_parens(member.object), JsThisExpression)
    ):
        return None
    return self._global_member_name(member, self._base_is_the_global_object)
def may_name_a_global(self, member)

The name of the global that a member access may reference once the program runs, read through may_be_global_object_base rather than through the spelling alone, or None.

The reading half of global_alias_member_name, and separate from it because the two answers are spent on opposite things. This one is recorded as a reference, where admitting an access whose receiver turns out to be another object keeps a declaration nothing reaches. That one drives a rewrite, where the same admission renames a method's own property to a global: refinery.lib.scripts.js.deobfuscation.reflection resolves a member callee through it, and a this.eval(…) answered as the global eval rewrites a call to an ordinary method.

No binding is minted from this answer. _ensure_implicit_global_from_alias_write keeps the spelling question, because a minted global is a name every intrinsic-trust and reflection reader then sees, and one minted from a receiver that was some other object withdraws trust the file never gave up.

Expand source code Browse git
def may_name_a_global(self, member: JsMemberExpression) -> str | None:
    """
    The name of the global that a member access *may* reference once the program runs, read
    through `may_be_global_object_base` rather than through the spelling alone, or `None`.

    The reading half of `global_alias_member_name`, and separate from it because the two answers
    are spent on opposite things. This one is recorded as a reference, where admitting an access
    whose receiver turns out to be another object keeps a declaration nothing reaches. That one
    drives a rewrite, where the same admission renames a method's own property to a global:
    `refinery.lib.scripts.js.deobfuscation.reflection` resolves a member callee through it, and
    a `this.eval(...)` answered as the global `eval` rewrites a call to an ordinary method.

    No binding is minted from this answer. `_ensure_implicit_global_from_alias_write` keeps the
    spelling question, because a minted global is a name every intrinsic-trust and reflection
    reader then sees, and one minted from a receiver that was some other object withdraws trust
    the file never gave up.
    """
    return self._global_member_name(member, self._base_may_be_the_global_object)
def may_be_the_global_object(self, node)

Whether node may be the global object once the program runs, asked of the node alone: spelled as one and not bound to something else, a receiver any call may supply (this), or a name any value of which the file gives the object. The node-level form of the base question may_name_a_global asks of a member access, so a consumer deciding whether a write, an install, or a hand-over reached the global object shares this one reading rather than each spelling a narrower one — the miss of one narrower spelling is how a written global went unrecorded while the fold kept trusting it.

Expand source code Browse git
def may_be_the_global_object(self, node: Node | None) -> bool:
    """
    Whether *node* may be the global object once the program runs, asked of the node alone:
    spelled as one and not bound to something else, a receiver any call may supply (`this`), or
    a name any value of which the file gives the object. The node-level form of the base
    question `may_name_a_global` asks of a member access, so a consumer deciding whether a
    write, an install, or a hand-over reached the global object shares this one reading rather
    than each spelling a narrower one — the miss of one narrower spelling is how a written
    global went unrecorded while the fold kept trusting it.
    """
    return self._base_may_be_the_global_object(node)
def names_the_global_object(self, node)

Whether node is a name the file gives the global object, so a property read on it may be a read of a global: one of the names _bindings_holding_the_global_object finds. ANY value of binding_values being the object is enough, and completeness is not asked: the callers record a reference, where one admission too many keeps a declaration and one refusal too many deletes one, so admitting is this answer's safe direction — a name that held the object on one branch of its life records the reads made through it even where another branch gave it something else. Parentheses around the name are looked through, as they are for a name spelled as the object (is_global_object_base).

A name the file never declares answers nothing, since binding_values declines for a binding with no declaration: an implicit global the file assigns by name, and the binding _ensure_implicit_global_from_alias_write mints for a name the file installs on the global object, whose value only a member access gives.

The values hold wherever the name is not in their temporal dead zone, and nothing here orders an establishing definition before the read. A caller driving a rewrite has to — and needs the complete-singleton reading singular_value gives, not this one.

Expand source code Browse git
def names_the_global_object(self, node: Node | None) -> bool:
    """
    Whether *node* is a name the file gives the global object, so a property read on it may be a
    read of a global: one of the names `_bindings_holding_the_global_object` finds. ANY value of
    `binding_values` being the object is enough, and completeness is not asked: the callers
    record a reference, where one admission too many keeps a declaration and one refusal too
    many deletes one, so admitting is this answer's safe direction — a name that held the object
    on one branch of its life records the reads made through it even where another branch gave
    it something else. Parentheses around the name are looked through, as they are for a name
    spelled as the object (`is_global_object_base`).

    A name the file never declares answers nothing, since `binding_values` declines for a
    binding with no declaration: an implicit global the file assigns by name, and the binding
    `_ensure_implicit_global_from_alias_write` mints for a name the file installs on the global
    object, whose value only a member access gives.

    The values hold wherever the name is not in their temporal dead zone, and nothing here orders
    an establishing definition before the read. A caller driving a rewrite has to — and needs the
    complete-singleton reading `singular_value` gives, not this one.
    """
    if not self._global_object_holders:
        return False
    node = strip_parens(node)
    return isinstance(node, JsIdentifier) and self.resolve(node) in self._global_object_holders
def global_object_argument_is_observed(self, node)

Whether the call node is handed to could read a property of the global object node stands for. An unobserved hand-over lets the globals the object carries stay foldable; an observed one, or one the model cannot resolve, is admitted whole the way it always has been.

The callee is resolved to the function it runs: a function written in place, a name whose one value is a function, or a name whose one value is the zero-argument IIFE a self-defending wrapper's factory is, whose single returned function is the one that runs. A callee resolving to none of these is not read, so its object is observed. A function that reaches its own arguments is observed too, because an element of that object is the handed argument under another name, which the parameter walk does not follow.

The argument is matched to the parameter it binds by position; a list with a rest, default, or destructuring element is not matched and its object is observed. An argument past the last parameter binds nothing the callee can name and is not observed. A parameter reflection can reach is observed. Otherwise _parameter_is_observed asks the body.

Expand source code Browse git
def global_object_argument_is_observed(self, node: Node) -> bool:
    """
    Whether the call *node* is handed to could read a property of the global object *node*
    stands for. An unobserved hand-over lets the globals the object carries stay foldable; an
    observed one, or one the model cannot resolve, is admitted whole the way it always has been.

    The callee is resolved to the function it runs: a function written in place, a name whose
    one value is a function, or a name whose one value is the zero-argument IIFE a self-defending
    wrapper's factory is, whose single returned function is the one that runs. A callee resolving
    to none of these is not read, so its object is observed. A function that reaches its own
    `arguments` is observed too, because an element of that object is the handed argument under
    another name, which the parameter walk does not follow.

    The argument is matched to the parameter it binds by position; a list with a rest, default,
    or destructuring element is not matched and its object is observed. An argument past the last
    parameter binds nothing the callee can name and is not observed. A parameter reflection can
    reach is observed. Otherwise `_parameter_is_observed` asks the body.
    """
    call = _enclosing_call(node)
    if call is None:
        return True
    function = self.target_function_of_call(call)
    if function is None:
        return True
    if references_own_arguments(function):
        return True
    mapping = self._argument_parameter_map(call, function)
    if mapping is None:
        return True
    parameter = next((b for b, argument in mapping.items() if argument is node), None)
    if parameter is None:
        return False
    if self.reflection_can_reach(parameter):
        return True
    return self._parameter_is_observed(function, parameter, mapping, {id(function)}, 0)
def target_function_of_call(self, call)

The function call runs, as far as it resolves without leaving the text: the callee written as a function, a name whose one value is a function, or a name whose one value is a zero-argument IIFE returning a single function — the shape the self-defending wrapper's factory takes. None when the callee resolves to none of these. A named callee is read through singular_value, the complete-singleton reading: a name that may hold another value — a parameter, or a declaration value later overwritten — resolves to None, and every consumer treats an unresolved callee as the observed hand-over.

Expand source code Browse git
def target_function_of_call(self, call: JsCallExpression | JsNewExpression) -> JsFunctionNode | None:
    """
    The function *call* runs, as far as it resolves without leaving the text: the callee written
    as a function, a name whose one value is a function, or a name whose one value is a
    zero-argument IIFE returning a single function — the shape the self-defending wrapper's
    factory takes. `None` when the callee resolves to none of these. A named callee is read
    through `singular_value`, the complete-singleton reading: a name that may hold another value
    — a parameter, or a declaration value later overwritten — resolves to `None`, and every
    consumer treats an unresolved callee as the observed hand-over.
    """
    callee = strip_parens(call.callee)
    if isinstance(callee, FUNCTION_NODES):
        return callee
    if not isinstance(callee, JsIdentifier):
        return None
    value = self.singular_value(self.resolve(callee))
    if value is None:
        return None
    value = strip_parens(value)
    if isinstance(value, FUNCTION_NODES):
        return value
    if isinstance(value, JsCallExpression) and not value.arguments:
        inner = strip_parens(value.callee)
        if isinstance(inner, FUNCTION_NODES):
            return _sole_returned_function(inner)
    return None
def intrinsic_dispatch_trusted(self)

Whether a call of call or apply on a function finds the built-in: no reflection surface stands, through which code could replace it, nothing is stored on the global object under a runtime key (has_opaque_global_write), and no text reaches the prototype surface the dispatch walks (_dispatch_surface_reachable), Function.prototype among it. The one answer every reader that counts on such a call reaching the built-in asks.

Expand source code Browse git
def intrinsic_dispatch_trusted(self) -> bool:
    """
    Whether a call of `call` or `apply` on a function finds the built-in: no reflection surface
    stands, through which code could replace it, nothing is stored on the global object under
    a runtime key (`has_opaque_global_write`), and no text reaches the prototype surface the
    dispatch walks (`_dispatch_surface_reachable`), `Function.prototype` among it. The one
    answer every reader that counts on such a call reaching the built-in asks.
    """
    return not (
        self.has_reflection_surface()
        or self.has_opaque_global_write()
        or self._dispatch_surface_reachable()
    )