Module 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 every transform in a run, rebuilding the models
only after that script's tree changes — whether a transform announces the change through
Transformer.changed or an in-pass mutation advances the script's
tree_version() counter — instead of each transform rebuilding from scratch on
every pass. The version tracking, invalidation, and transformer-reuse mechanism live in
ModelCacheBase; this module only declares the JavaScript model
slots and their build_* wiring.
Expand source code Browse git
"""
A per-run cache of the JavaScript analysis models. The deobfuscation pipeline builds one cache over
the script being transformed and shares it across every transform in a run, rebuilding the models
only after that script's tree changes — whether a transform announces the change through
`refinery.lib.scripts.Transformer.changed` or an in-pass mutation advances the script's
`refinery.lib.scripts.tree_version` counter — instead of each transform rebuilding from scratch on
every pass. The version tracking, invalidation, and transformer-reuse mechanism live in
`refinery.lib.scripts.modelcache.ModelCacheBase`; this module only declares the JavaScript model
slots and their `build_*` wiring.
"""
from __future__ import annotations
from contextlib import contextmanager
from functools import partial
from typing import Generator
from refinery.lib.scripts import Node, Transformer
from refinery.lib.scripts.analysis.cycles import CycleModel
from refinery.lib.scripts.js.analysis.assignment import (
DefiniteAssignmentModel,
build_definite_assignment,
)
from refinery.lib.scripts.js.analysis.cfg import ControlFlowModel, build_control_flow_model
from refinery.lib.scripts.js.analysis.conversions import ConversionSettlement
from refinery.lib.scripts.js.analysis.dominance import DominanceModel, build_dominance
from refinery.lib.scripts.js.analysis.effects import AnchoredTrust, EffectModel, build_effects
from refinery.lib.scripts.js.analysis.environment import FileKind
from refinery.lib.scripts.js.analysis.intrinsics import (
IntrinsicWrites,
UnfollowedWriteRefusals,
WriteSpeculation,
build_intrinsic_writes,
)
from refinery.lib.scripts.js.analysis.liveness import LivenessModel, build_liveness
from refinery.lib.scripts.js.analysis.model import SemanticModel, build_semantic_model
from refinery.lib.scripts.js.analysis.reaching import ReachingModel, build_reaching
from refinery.lib.scripts.js.analysis.tampering import TamperingModel, build_tampering
from refinery.lib.scripts.js.model import JsCallExpression, JsIdentifier, JsNewExpression, JsScript
from refinery.lib.scripts.js.options import (
eval_is_trusted,
exports_are_read_outside,
file_kind,
host_environment,
is_host_entrypoint,
)
from refinery.lib.scripts.modelcache import ModelCacheBase
class ModelCache(ModelCacheBase):
"""
Lazily builds and memoizes the `refinery.lib.scripts.js.analysis.model.SemanticModel`, the
`refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites` scan the effect and dominance
models both read, the `refinery.lib.scripts.js.analysis.effects.EffectModel`, the
`refinery.lib.scripts.js.analysis.cfg.ControlFlowModel` shared by the
`refinery.lib.scripts.js.analysis.liveness.LivenessModel` and
`refinery.lib.scripts.js.analysis.dominance.DominanceModel`, the
`refinery.lib.scripts.js.analysis.reaching.ReachingModel` layered on them, and the
`refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel`, for one root script.
What the file is (`refinery.lib.scripts.js.options.file_kind`) is decided once, on the tree the
cache is created over, and every semantic model it builds holds that answer: an edit that
removes the declaration which made a file an ES module does not make it something else.
The memoized models are dropped whenever this root's AST-mutation counter advances past the
value they were built at, so a transform that reads the cache after an earlier mutation in the
same pass — even one not yet announced through `refinery.lib.scripts.Transformer.changed` —
observes models consistent with the current tree. The derived models are always built on the
current semantic model, so dropping them together keeps them consistent.
A transform may hold the models for the length of one pass through `pinned`, which suppresses both
drops until the pass ends. The obligation is the one
`refinery.lib.scripts.modelcache.ModelCacheBase.pinned` states: the transform's own rewrites must
never make the models it reads more permissive. A rewrite that only restricts what the models would
answer makes the held answer the stricter one, so the pass declines where it could have proceeded;
one that could reveal a fact the held models predate would act on the stale, more permissive answer
and must not read them pinned. The `--no-pin` differential (`test/conftest.py`) is the mechanical
check: the whole suite must pass identically with every pin neutralized.
The cache hands the record of the run it serves, *settlement*, to every effect model it builds
(`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`). Only
`refinery.lib.scripts.js.deobfuscation.deobfuscate` creates one; a cache built anywhere else
holds none, and its effect models then refuse what they would only answer on a tree a run has
settled. It hands the run's record of folds refused because the scan does not follow every
write, *refusals*, to them the same way
(`refinery.lib.scripts.js.analysis.intrinsics.UnfollowedWriteRefusals`). A run that assumes
that every write the scan does not follow writes nothing runs inside `speculating`, which hands
every build of the scan to the record of that assumption.
"""
_SLOTS = (
'_model',
'_intrinsic_writes',
'_control_flow',
'_cycles',
'_effects',
'_liveness',
'_dominance',
'_reaching',
'_assignment',
'_tampering',
)
# The slots whose build walks the live tree — the guard refuses any of these built late over a
# moved tree: `model`/`control_flow` from `root`, `intrinsic_writes`/`effects`/`assignment`
# which re-walk `model.root` at build, and `liveness` which walks each graph's element subtrees
# at build.
# `dominance` and `reaching` build purely from held base models — the former is built by
# warming `effects` all the same, as the ordering base its summary computation reads.
# Query-time tree reads — `tampering`'s site enumeration, `reaching`'s call walk — no warming
# can force; a pass reading them across its edits owns that.
_ROOT_SLOTS = (
'_model',
'_intrinsic_writes',
'_control_flow',
'_effects',
'_assignment',
'_liveness',
)
# The tree readers every pinned pass reads, warmed at entry. `liveness` is a tree reader only
# `unused` reads, which names it to `warm`, so it stays in the guard set above yet out of the
# warm build — warming it would solve liveness in pinned blocks that never ask.
_WARM_SLOTS = (
'_model',
'_intrinsic_writes',
'_control_flow',
'_effects',
'_assignment',
)
root: JsScript
file_kind: FileKind
_model: SemanticModel | None
_intrinsic_writes: IntrinsicWrites | None
_control_flow: ControlFlowModel | None
_cycles: CycleModel | None
_effects: EffectModel | None
_liveness: LivenessModel | None
_dominance: DominanceModel | None
_reaching: ReachingModel | None
_assignment: DefiniteAssignmentModel | None
_tampering: TamperingModel | None
def __init__(
self,
root: Node,
options: object | None = None,
settlement: ConversionSettlement | None = None,
refusals: UnfollowedWriteRefusals | None = None,
):
self.settlement = settlement
self.refusals = refusals
self._speculation: WriteSpeculation | None = None
super().__init__(root, options)
self.file_kind = file_kind(self.options, self.root)
@contextmanager
def speculating(self) -> Generator[WriteSpeculation, None, None]:
"""
Serve the block's models under a new
`refinery.lib.scripts.js.analysis.intrinsics.WriteSpeculation`, which every build of the
write scan in the block passes through. While it assumes, every build of the semantic model
also builds the scan, so a tree is checked before a pass acts on what the model says of it,
and so is the tree the block leaves. The models held before the block were built without the
speculation and are dropped on entry; on exit, those built while it assumed are dropped as
well, since they read a scan presented as following writes it does not follow.
"""
speculation = self._speculation = WriteSpeculation()
self.invalidate()
try:
yield speculation
self._held_intrinsic_writes()
finally:
self._speculation = None
if not speculation.discharged:
self.invalidate()
@property
def model(self) -> SemanticModel:
model = self._semantic_model()
speculation = self._speculation
if speculation is not None and speculation.assuming:
self._held_intrinsic_writes()
return model
def _semantic_model(self) -> SemanticModel:
return self._lazy('_model', lambda: build_semantic_model(
self.root,
host_environment(self.options),
eval_is_trusted(self.options),
self.file_kind,
exports_are_read_outside(self.options),
partial(is_host_entrypoint, self.options),
))
@property
def intrinsic_writes(self) -> IntrinsicWrites:
return self._held_intrinsic_writes()
def _held_intrinsic_writes(self) -> IntrinsicWrites:
return self._lazy('_intrinsic_writes', self._observed_intrinsic_writes)
def _observed_intrinsic_writes(self) -> IntrinsicWrites:
writes = build_intrinsic_writes(self._semantic_model())
speculation = self._speculation
return writes if speculation is None else speculation.observe(writes)
@property
def effects(self) -> EffectModel:
return self._lazy('_effects', lambda: build_effects(
self.model,
self.dominance,
self.intrinsic_writes,
self.settlement,
self.refusals,
))
@property
def control_flow(self) -> ControlFlowModel:
return self._lazy('_control_flow', lambda: build_control_flow_model(self.root))
@property
def cycles(self) -> CycleModel:
"""
The `refinery.lib.scripts.analysis.cycles.CycleModel` over the graphs of `control_flow`:
which points of the script control can reach more than once.
"""
return self._lazy('_cycles', lambda: CycleModel(self.control_flow))
@property
def liveness(self) -> LivenessModel:
return self._lazy('_liveness', lambda: build_liveness(self.model, self.control_flow))
@property
def dominance(self) -> DominanceModel:
"""
The `refinery.lib.scripts.js.analysis.dominance.DominanceModel` for this root.
"""
return self._lazy('_dominance', lambda: build_dominance(
self.model,
self.control_flow,
intrinsic_writes=self.intrinsic_writes,
))
@property
def reaching(self) -> ReachingModel:
return self._lazy('_reaching', lambda: build_reaching(self.dominance, self.effects))
@property
def assignment(self) -> DefiniteAssignmentModel:
"""
The `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel` for this root.
It is the one establishment answer for implicit-global reads: every consumer in a run reads
this slot, so no tree is ever judged under two establishment answers at once.
"""
return self._lazy('_assignment', lambda: build_definite_assignment(
self.model,
self.control_flow,
))
@property
def tampering(self) -> TamperingModel:
"""
The `refinery.lib.scripts.js.analysis.tampering.TamperingModel` for this root — the model
every anchored trust question in a run shares, so no call is ever judged under two
tampering verdicts at once.
"""
return self._lazy('_tampering', lambda: build_tampering(
self.model,
self.effects,
self.dominance,
self.cycles,
))
def builtins_intact_at(self, anchor: Node) -> bool:
"""
Whether every built-in is still what the language says at the moment *anchor* is evaluated:
no program-wide refusal, every tampering site guaranteed to run after the anchor, and the
anchor executing at most once — the conjunction
`refinery.lib.scripts.js.analysis.tampering.TamperingModel.builtins_intact_at` states in
full. The one composition point every anchored consumer shares; a consumer that does not
ask keeps the program-wide refusal the no-anchor arms answer.
"""
return self.tampering.builtins_intact_at(anchor)
def trust_at(self, anchor: Node) -> AnchoredTrust:
"""
The trust questions of a fold at *anchor*:
`refinery.lib.scripts.js.analysis.tampering.TamperingModel.trust_at` on the run's tampering
model, the one place the oracle and the per-name arms are composed.
"""
return self.tampering.trust_at(anchor)
def call_established(self, call: JsCallExpression | JsNewExpression) -> bool:
"""
Whether *call* may be cleared by the purity oracle at all: its callee is a trusted
intrinsic, or a local function whose definition reaches the call, so a call textually before
a not-yet-established function keeps its runtime throw. The callee's summary may also defer
outer `let`/`const`/`class` bindings its body reads (`EffectSummary.dead_zone_reads`); the
call keeps its throw unless each such binding's declaration is guaranteed to have run first,
judged by the same dominance model. This is the one composition of the effect and dominance
models every consumer shares, so no pass can pair a purity verdict with a weaker
establishment reading than another.
"""
return self.effects.call_clearable(
call,
lambda name: self.dominance.binding_established_before(name, call),
lambda binding: self.dominance.past_dead_zone(binding, call),
)
def read_established(self, node: JsIdentifier) -> bool:
"""
Whether reading *node* cannot be the evaluation that raises a `ReferenceError`: the one
composition of the establishment and dominance models every discarding context shares, so no
pass drops a read another would keep. True when a creating write has certainly completed
(`refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel.read_established`, the
implicit-global case), or when *node* reads a `let`/`const`/`class` binding whose
declaration is guaranteed to have run first, ending its temporal dead zone
(`refinery.lib.scripts.js.analysis.dominance.DominanceModel.runs_before` over the binding's
declarations). A lexical read the ordering cannot vouch for — one that may run in the dead
zone, or one whose binding carries no declaration site to order against — is not
established, so the sweep keeps the store and the throw with it. This generalizes the
write-only proof the consumers threaded before: every context that dropped a read the moment
a creating write reached it now keeps a lexical read the same context would move out of its
dead zone.
"""
binding = self.model.resolve(node)
if self.assignment.definitely_assigned_at(binding, node):
return True
if binding is None or not binding.is_lexical:
return False
return self.dominance.past_dead_zone(binding, node)
def model_cache(transformer: Transformer, root: JsScript) -> ModelCache:
"""
The pipeline's shared `ModelCache` for *root* when one is attached to *transformer* and built
over that same root, otherwise a fresh cache stashed back onto *transformer* for reuse within
its single-pass lifetime. See `refinery.lib.scripts.modelcache.ModelCacheBase.for_transformer`.
"""
return ModelCache.for_transformer(transformer, root)
Functions
def model_cache(transformer, root)-
The pipeline's shared
ModelCachefor root when one is attached to transformer and built over that same root, otherwise a fresh cache stashed back onto transformer for reuse within its single-pass lifetime. SeeModelCacheBase.for_transformer().Expand source code Browse git
def model_cache(transformer: Transformer, root: JsScript) -> ModelCache: """ The pipeline's shared `ModelCache` for *root* when one is attached to *transformer* and built over that same root, otherwise a fresh cache stashed back onto *transformer* for reuse within its single-pass lifetime. See `refinery.lib.scripts.modelcache.ModelCacheBase.for_transformer`. """ return ModelCache.for_transformer(transformer, root)
Classes
class ModelCache (root, options=None, settlement=None, refusals=None)-
Lazily builds and memoizes the
SemanticModel, theIntrinsicWritesscan the effect and dominance models both read, theEffectModel, theControlFlowModelshared by theLivenessModelandDominanceModel, theReachingModellayered on them, and theDefiniteAssignmentModel, for one root script. What the file is (file_kind()) is decided once, on the tree the cache is created over, and every semantic model it builds holds that answer: an edit that removes the declaration which made a file an ES module does not make it something else. The memoized models are dropped whenever this root's AST-mutation counter advances past the value they were built at, so a transform that reads the cache after an earlier mutation in the same pass — even one not yet announced throughTransformer.changed— observes models consistent with the current tree. The derived models are always built on the current semantic model, so dropping them together keeps them consistent.A transform may hold the models for the length of one pass through
pinned, which suppresses both drops until the pass ends. The obligation is the oneModelCacheBase.pinned()states: the transform's own rewrites must never make the models it reads more permissive. A rewrite that only restricts what the models would answer makes the held answer the stricter one, so the pass declines where it could have proceeded; one that could reveal a fact the held models predate would act on the stale, more permissive answer and must not read them pinned. The--no-pindifferential (test/conftest.py) is the mechanical check: the whole suite must pass identically with every pin neutralized.The cache hands the record of the run it serves, settlement, to every effect model it builds (
ConversionSettlement). Onlydeobfuscate()creates one; a cache built anywhere else holds none, and its effect models then refuse what they would only answer on a tree a run has settled. It hands the run's record of folds refused because the scan does not follow every write, refusals, to them the same way (UnfollowedWriteRefusals). A run that assumes that every write the scan does not follow writes nothing runs insidespeculating, which hands every build of the scan to the record of that assumption.Expand source code Browse git
class ModelCache(ModelCacheBase): """ Lazily builds and memoizes the `refinery.lib.scripts.js.analysis.model.SemanticModel`, the `refinery.lib.scripts.js.analysis.intrinsics.IntrinsicWrites` scan the effect and dominance models both read, the `refinery.lib.scripts.js.analysis.effects.EffectModel`, the `refinery.lib.scripts.js.analysis.cfg.ControlFlowModel` shared by the `refinery.lib.scripts.js.analysis.liveness.LivenessModel` and `refinery.lib.scripts.js.analysis.dominance.DominanceModel`, the `refinery.lib.scripts.js.analysis.reaching.ReachingModel` layered on them, and the `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel`, for one root script. What the file is (`refinery.lib.scripts.js.options.file_kind`) is decided once, on the tree the cache is created over, and every semantic model it builds holds that answer: an edit that removes the declaration which made a file an ES module does not make it something else. The memoized models are dropped whenever this root's AST-mutation counter advances past the value they were built at, so a transform that reads the cache after an earlier mutation in the same pass — even one not yet announced through `refinery.lib.scripts.Transformer.changed` — observes models consistent with the current tree. The derived models are always built on the current semantic model, so dropping them together keeps them consistent. A transform may hold the models for the length of one pass through `pinned`, which suppresses both drops until the pass ends. The obligation is the one `refinery.lib.scripts.modelcache.ModelCacheBase.pinned` states: the transform's own rewrites must never make the models it reads more permissive. A rewrite that only restricts what the models would answer makes the held answer the stricter one, so the pass declines where it could have proceeded; one that could reveal a fact the held models predate would act on the stale, more permissive answer and must not read them pinned. The `--no-pin` differential (`test/conftest.py`) is the mechanical check: the whole suite must pass identically with every pin neutralized. The cache hands the record of the run it serves, *settlement*, to every effect model it builds (`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`). Only `refinery.lib.scripts.js.deobfuscation.deobfuscate` creates one; a cache built anywhere else holds none, and its effect models then refuse what they would only answer on a tree a run has settled. It hands the run's record of folds refused because the scan does not follow every write, *refusals*, to them the same way (`refinery.lib.scripts.js.analysis.intrinsics.UnfollowedWriteRefusals`). A run that assumes that every write the scan does not follow writes nothing runs inside `speculating`, which hands every build of the scan to the record of that assumption. """ _SLOTS = ( '_model', '_intrinsic_writes', '_control_flow', '_cycles', '_effects', '_liveness', '_dominance', '_reaching', '_assignment', '_tampering', ) # The slots whose build walks the live tree — the guard refuses any of these built late over a # moved tree: `model`/`control_flow` from `root`, `intrinsic_writes`/`effects`/`assignment` # which re-walk `model.root` at build, and `liveness` which walks each graph's element subtrees # at build. # `dominance` and `reaching` build purely from held base models — the former is built by # warming `effects` all the same, as the ordering base its summary computation reads. # Query-time tree reads — `tampering`'s site enumeration, `reaching`'s call walk — no warming # can force; a pass reading them across its edits owns that. _ROOT_SLOTS = ( '_model', '_intrinsic_writes', '_control_flow', '_effects', '_assignment', '_liveness', ) # The tree readers every pinned pass reads, warmed at entry. `liveness` is a tree reader only # `unused` reads, which names it to `warm`, so it stays in the guard set above yet out of the # warm build — warming it would solve liveness in pinned blocks that never ask. _WARM_SLOTS = ( '_model', '_intrinsic_writes', '_control_flow', '_effects', '_assignment', ) root: JsScript file_kind: FileKind _model: SemanticModel | None _intrinsic_writes: IntrinsicWrites | None _control_flow: ControlFlowModel | None _cycles: CycleModel | None _effects: EffectModel | None _liveness: LivenessModel | None _dominance: DominanceModel | None _reaching: ReachingModel | None _assignment: DefiniteAssignmentModel | None _tampering: TamperingModel | None def __init__( self, root: Node, options: object | None = None, settlement: ConversionSettlement | None = None, refusals: UnfollowedWriteRefusals | None = None, ): self.settlement = settlement self.refusals = refusals self._speculation: WriteSpeculation | None = None super().__init__(root, options) self.file_kind = file_kind(self.options, self.root) @contextmanager def speculating(self) -> Generator[WriteSpeculation, None, None]: """ Serve the block's models under a new `refinery.lib.scripts.js.analysis.intrinsics.WriteSpeculation`, which every build of the write scan in the block passes through. While it assumes, every build of the semantic model also builds the scan, so a tree is checked before a pass acts on what the model says of it, and so is the tree the block leaves. The models held before the block were built without the speculation and are dropped on entry; on exit, those built while it assumed are dropped as well, since they read a scan presented as following writes it does not follow. """ speculation = self._speculation = WriteSpeculation() self.invalidate() try: yield speculation self._held_intrinsic_writes() finally: self._speculation = None if not speculation.discharged: self.invalidate() @property def model(self) -> SemanticModel: model = self._semantic_model() speculation = self._speculation if speculation is not None and speculation.assuming: self._held_intrinsic_writes() return model def _semantic_model(self) -> SemanticModel: return self._lazy('_model', lambda: build_semantic_model( self.root, host_environment(self.options), eval_is_trusted(self.options), self.file_kind, exports_are_read_outside(self.options), partial(is_host_entrypoint, self.options), )) @property def intrinsic_writes(self) -> IntrinsicWrites: return self._held_intrinsic_writes() def _held_intrinsic_writes(self) -> IntrinsicWrites: return self._lazy('_intrinsic_writes', self._observed_intrinsic_writes) def _observed_intrinsic_writes(self) -> IntrinsicWrites: writes = build_intrinsic_writes(self._semantic_model()) speculation = self._speculation return writes if speculation is None else speculation.observe(writes) @property def effects(self) -> EffectModel: return self._lazy('_effects', lambda: build_effects( self.model, self.dominance, self.intrinsic_writes, self.settlement, self.refusals, )) @property def control_flow(self) -> ControlFlowModel: return self._lazy('_control_flow', lambda: build_control_flow_model(self.root)) @property def cycles(self) -> CycleModel: """ The `refinery.lib.scripts.analysis.cycles.CycleModel` over the graphs of `control_flow`: which points of the script control can reach more than once. """ return self._lazy('_cycles', lambda: CycleModel(self.control_flow)) @property def liveness(self) -> LivenessModel: return self._lazy('_liveness', lambda: build_liveness(self.model, self.control_flow)) @property def dominance(self) -> DominanceModel: """ The `refinery.lib.scripts.js.analysis.dominance.DominanceModel` for this root. """ return self._lazy('_dominance', lambda: build_dominance( self.model, self.control_flow, intrinsic_writes=self.intrinsic_writes, )) @property def reaching(self) -> ReachingModel: return self._lazy('_reaching', lambda: build_reaching(self.dominance, self.effects)) @property def assignment(self) -> DefiniteAssignmentModel: """ The `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel` for this root. It is the one establishment answer for implicit-global reads: every consumer in a run reads this slot, so no tree is ever judged under two establishment answers at once. """ return self._lazy('_assignment', lambda: build_definite_assignment( self.model, self.control_flow, )) @property def tampering(self) -> TamperingModel: """ The `refinery.lib.scripts.js.analysis.tampering.TamperingModel` for this root — the model every anchored trust question in a run shares, so no call is ever judged under two tampering verdicts at once. """ return self._lazy('_tampering', lambda: build_tampering( self.model, self.effects, self.dominance, self.cycles, )) def builtins_intact_at(self, anchor: Node) -> bool: """ Whether every built-in is still what the language says at the moment *anchor* is evaluated: no program-wide refusal, every tampering site guaranteed to run after the anchor, and the anchor executing at most once — the conjunction `refinery.lib.scripts.js.analysis.tampering.TamperingModel.builtins_intact_at` states in full. The one composition point every anchored consumer shares; a consumer that does not ask keeps the program-wide refusal the no-anchor arms answer. """ return self.tampering.builtins_intact_at(anchor) def trust_at(self, anchor: Node) -> AnchoredTrust: """ The trust questions of a fold at *anchor*: `refinery.lib.scripts.js.analysis.tampering.TamperingModel.trust_at` on the run's tampering model, the one place the oracle and the per-name arms are composed. """ return self.tampering.trust_at(anchor) def call_established(self, call: JsCallExpression | JsNewExpression) -> bool: """ Whether *call* may be cleared by the purity oracle at all: its callee is a trusted intrinsic, or a local function whose definition reaches the call, so a call textually before a not-yet-established function keeps its runtime throw. The callee's summary may also defer outer `let`/`const`/`class` bindings its body reads (`EffectSummary.dead_zone_reads`); the call keeps its throw unless each such binding's declaration is guaranteed to have run first, judged by the same dominance model. This is the one composition of the effect and dominance models every consumer shares, so no pass can pair a purity verdict with a weaker establishment reading than another. """ return self.effects.call_clearable( call, lambda name: self.dominance.binding_established_before(name, call), lambda binding: self.dominance.past_dead_zone(binding, call), ) def read_established(self, node: JsIdentifier) -> bool: """ Whether reading *node* cannot be the evaluation that raises a `ReferenceError`: the one composition of the establishment and dominance models every discarding context shares, so no pass drops a read another would keep. True when a creating write has certainly completed (`refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel.read_established`, the implicit-global case), or when *node* reads a `let`/`const`/`class` binding whose declaration is guaranteed to have run first, ending its temporal dead zone (`refinery.lib.scripts.js.analysis.dominance.DominanceModel.runs_before` over the binding's declarations). A lexical read the ordering cannot vouch for — one that may run in the dead zone, or one whose binding carries no declaration site to order against — is not established, so the sweep keeps the store and the throw with it. This generalizes the write-only proof the consumers threaded before: every context that dropped a read the moment a creating write reached it now keeps a lexical read the same context would move out of its dead zone. """ binding = self.model.resolve(node) if self.assignment.definitely_assigned_at(binding, node): return True if binding is None or not binding.is_lexical: return False return self.dominance.past_dead_zone(binding, node)Ancestors
Class variables
var file_kind-
The type of the None singleton.
Instance variables
var model-
Expand source code Browse git
@property def model(self) -> SemanticModel: model = self._semantic_model() speculation = self._speculation if speculation is not None and speculation.assuming: self._held_intrinsic_writes() return model var intrinsic_writes-
Expand source code Browse git
@property def intrinsic_writes(self) -> IntrinsicWrites: return self._held_intrinsic_writes() var effects-
Expand source code Browse git
@property def effects(self) -> EffectModel: return self._lazy('_effects', lambda: build_effects( self.model, self.dominance, self.intrinsic_writes, self.settlement, self.refusals, )) var control_flow-
Expand source code Browse git
@property def control_flow(self) -> ControlFlowModel: return self._lazy('_control_flow', lambda: build_control_flow_model(self.root)) var cycles-
The
CycleModelover the graphs ofcontrol_flow: which points of the script control can reach more than once.Expand source code Browse git
@property def cycles(self) -> CycleModel: """ The `refinery.lib.scripts.analysis.cycles.CycleModel` over the graphs of `control_flow`: which points of the script control can reach more than once. """ return self._lazy('_cycles', lambda: CycleModel(self.control_flow)) var liveness-
Expand source code Browse git
@property def liveness(self) -> LivenessModel: return self._lazy('_liveness', lambda: build_liveness(self.model, self.control_flow)) var dominance-
The
DominanceModelfor this root.Expand source code Browse git
@property def dominance(self) -> DominanceModel: """ The `refinery.lib.scripts.js.analysis.dominance.DominanceModel` for this root. """ return self._lazy('_dominance', lambda: build_dominance( self.model, self.control_flow, intrinsic_writes=self.intrinsic_writes, )) var reaching-
Expand source code Browse git
@property def reaching(self) -> ReachingModel: return self._lazy('_reaching', lambda: build_reaching(self.dominance, self.effects)) var assignment-
The
DefiniteAssignmentModelfor this root. It is the one establishment answer for implicit-global reads: every consumer in a run reads this slot, so no tree is ever judged under two establishment answers at once.Expand source code Browse git
@property def assignment(self) -> DefiniteAssignmentModel: """ The `refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel` for this root. It is the one establishment answer for implicit-global reads: every consumer in a run reads this slot, so no tree is ever judged under two establishment answers at once. """ return self._lazy('_assignment', lambda: build_definite_assignment( self.model, self.control_flow, )) var tampering-
The
TamperingModelfor this root — the model every anchored trust question in a run shares, so no call is ever judged under two tampering verdicts at once.Expand source code Browse git
@property def tampering(self) -> TamperingModel: """ The `refinery.lib.scripts.js.analysis.tampering.TamperingModel` for this root — the model every anchored trust question in a run shares, so no call is ever judged under two tampering verdicts at once. """ return self._lazy('_tampering', lambda: build_tampering( self.model, self.effects, self.dominance, self.cycles, ))
Methods
def speculating(self)-
Serve the block's models under a new
WriteSpeculation, which every build of the write scan in the block passes through. While it assumes, every build of the semantic model also builds the scan, so a tree is checked before a pass acts on what the model says of it, and so is the tree the block leaves. The models held before the block were built without the speculation and are dropped on entry; on exit, those built while it assumed are dropped as well, since they read a scan presented as following writes it does not follow.Expand source code Browse git
@contextmanager def speculating(self) -> Generator[WriteSpeculation, None, None]: """ Serve the block's models under a new `refinery.lib.scripts.js.analysis.intrinsics.WriteSpeculation`, which every build of the write scan in the block passes through. While it assumes, every build of the semantic model also builds the scan, so a tree is checked before a pass acts on what the model says of it, and so is the tree the block leaves. The models held before the block were built without the speculation and are dropped on entry; on exit, those built while it assumed are dropped as well, since they read a scan presented as following writes it does not follow. """ speculation = self._speculation = WriteSpeculation() self.invalidate() try: yield speculation self._held_intrinsic_writes() finally: self._speculation = None if not speculation.discharged: self.invalidate() def builtins_intact_at(self, anchor)-
Whether every built-in is still what the language says at the moment anchor is evaluated: no program-wide refusal, every tampering site guaranteed to run after the anchor, and the anchor executing at most once — the conjunction
TamperingModel.builtins_intact_at()states in full. The one composition point every anchored consumer shares; a consumer that does not ask keeps the program-wide refusal the no-anchor arms answer.Expand source code Browse git
def builtins_intact_at(self, anchor: Node) -> bool: """ Whether every built-in is still what the language says at the moment *anchor* is evaluated: no program-wide refusal, every tampering site guaranteed to run after the anchor, and the anchor executing at most once — the conjunction `refinery.lib.scripts.js.analysis.tampering.TamperingModel.builtins_intact_at` states in full. The one composition point every anchored consumer shares; a consumer that does not ask keeps the program-wide refusal the no-anchor arms answer. """ return self.tampering.builtins_intact_at(anchor) def trust_at(self, anchor)-
The trust questions of a fold at anchor:
TamperingModel.trust_at()on the run's tampering model, the one place the oracle and the per-name arms are composed.Expand source code Browse git
def trust_at(self, anchor: Node) -> AnchoredTrust: """ The trust questions of a fold at *anchor*: `refinery.lib.scripts.js.analysis.tampering.TamperingModel.trust_at` on the run's tampering model, the one place the oracle and the per-name arms are composed. """ return self.tampering.trust_at(anchor) def call_established(self, call)-
Whether call may be cleared by the purity oracle at all: its callee is a trusted intrinsic, or a local function whose definition reaches the call, so a call textually before a not-yet-established function keeps its runtime throw. The callee's summary may also defer outer
let/const/classbindings its body reads (EffectSummary.dead_zone_reads); the call keeps its throw unless each such binding's declaration is guaranteed to have run first, judged by the same dominance model. This is the one composition of the effect and dominance models every consumer shares, so no pass can pair a purity verdict with a weaker establishment reading than another.Expand source code Browse git
def call_established(self, call: JsCallExpression | JsNewExpression) -> bool: """ Whether *call* may be cleared by the purity oracle at all: its callee is a trusted intrinsic, or a local function whose definition reaches the call, so a call textually before a not-yet-established function keeps its runtime throw. The callee's summary may also defer outer `let`/`const`/`class` bindings its body reads (`EffectSummary.dead_zone_reads`); the call keeps its throw unless each such binding's declaration is guaranteed to have run first, judged by the same dominance model. This is the one composition of the effect and dominance models every consumer shares, so no pass can pair a purity verdict with a weaker establishment reading than another. """ return self.effects.call_clearable( call, lambda name: self.dominance.binding_established_before(name, call), lambda binding: self.dominance.past_dead_zone(binding, call), ) def read_established(self, node)-
Whether reading node cannot be the evaluation that raises a
ReferenceError: the one composition of the establishment and dominance models every discarding context shares, so no pass drops a read another would keep. True when a creating write has certainly completed (DefiniteAssignmentModel.read_established(), the implicit-global case), or when node reads alet/const/classbinding whose declaration is guaranteed to have run first, ending its temporal dead zone (DominanceModel.runs_before()over the binding's declarations). A lexical read the ordering cannot vouch for — one that may run in the dead zone, or one whose binding carries no declaration site to order against — is not established, so the sweep keeps the store and the throw with it. This generalizes the write-only proof the consumers threaded before: every context that dropped a read the moment a creating write reached it now keeps a lexical read the same context would move out of its dead zone.Expand source code Browse git
def read_established(self, node: JsIdentifier) -> bool: """ Whether reading *node* cannot be the evaluation that raises a `ReferenceError`: the one composition of the establishment and dominance models every discarding context shares, so no pass drops a read another would keep. True when a creating write has certainly completed (`refinery.lib.scripts.js.analysis.assignment.DefiniteAssignmentModel.read_established`, the implicit-global case), or when *node* reads a `let`/`const`/`class` binding whose declaration is guaranteed to have run first, ending its temporal dead zone (`refinery.lib.scripts.js.analysis.dominance.DominanceModel.runs_before` over the binding's declarations). A lexical read the ordering cannot vouch for — one that may run in the dead zone, or one whose binding carries no declaration site to order against — is not established, so the sweep keeps the store and the throw with it. This generalizes the write-only proof the consumers threaded before: every context that dropped a read the moment a creating write reached it now keeps a lexical read the same context would move out of its dead zone. """ binding = self.model.resolve(node) if self.assignment.definitely_assigned_at(binding, node): return True if binding is None or not binding.is_lexical: return False return self.dominance.past_dead_zone(binding, node)
Inherited members