Module 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 locals to the top of the file, and moving them
back lets every later pass treat them as the locals they are.
A var behaves as a local of a function when every run of the function reads only what that run
stored: the function writes it before any read
(LivenessModel.is_dead_on_entry()), so no value carried
over from load or from an earlier run is read, and it is dead wherever another run of the function
may write it while this one is still under way. Another run starts or resumes only at a point where
the body hands control to other code: a call that may run the function again, and an await, a
yield, or a point leaving the body of a for await loop, which waits for the iterator to close,
at which the body waits while other code runs. These are the points at which code other than the
body's own statements may change the binding
(ReachingModel.foreign_kills()), and the var must not
be live at any of them (LivenessModel.live_around()). A
function a run of which may start where no call of its name shows, through the global object, its
own arguments, the caller of a function it calls, new.target, a step of a generator object,
or code outside the file, has no such list of points
(EffectModel.function_escapes()), and its var stays. In
var x; function f(n) { x = n; if (n) f(n - 1); console.log(x); }
the call f(n - 1) writes x before the outer run reads it, and f(2) prints 0 three times;
with x moved into f, it prints 0, 1 and 2.
Expand source code Browse git
"""
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 locals to the top of the file, and moving them
back lets every later pass treat them as the locals they are.
A `var` behaves as a local of a function when every run of the function reads only what that run
stored: the function writes it before any read
(`refinery.lib.scripts.js.analysis.liveness.LivenessModel.is_dead_on_entry`), so no value carried
over from load or from an earlier run is read, and it is dead wherever another run of the function
may write it while this one is still under way. Another run starts or resumes only at a point where
the body hands control to other code: a call that may run the function again, and an `await`, a
`yield`, or a point leaving the body of a `for await` loop, which waits for the iterator to close,
at which the body waits while other code runs. These are the points at which code other than the
body's own statements may change the binding
(`refinery.lib.scripts.js.analysis.reaching.ReachingModel.foreign_kills`), and the `var` must not
be live at any of them (`refinery.lib.scripts.js.analysis.liveness.LivenessModel.live_around`). A
function a run of which may start where no call of its name shows, through the global object, its
own `arguments`, the `caller` of a function it calls, `new.target`, a step of a generator object,
or code outside the file, has no such list of points
(`refinery.lib.scripts.js.analysis.effects.EffectModel.function_escapes`), and its `var` stays. In
var x; function f(n) { x = n; if (n) f(n - 1); console.log(x); }
the call `f(n - 1)` writes `x` before the outer run reads it, and `f(2)` prints `0` three times;
with `x` moved into `f`, it prints `0`, `1` and `2`.
"""
from __future__ import annotations
from refinery.lib.scripts import Node
from refinery.lib.scripts.js.analysis.liveness import LivenessModel
from refinery.lib.scripts.js.analysis.model import Binding
from refinery.lib.scripts.js.analysis.reaching import ReachingModel
from refinery.lib.scripts.js.model import JsIdentifier
def localization_target(
liveness: LivenessModel,
reaching: ReachingModel,
binding: Binding,
) -> Node | None:
"""
The function into which *binding*, a script-scope `var`, can be soundly relocated, or `None`. A
binding qualifies when one function's body holds every read of it and every store into it
(`refinery.lib.scripts.js.analysis.liveness.LivenessModel.pseudo_local_owner`), every run of
that function reads only what the run stored (see the module documentation), every reference
spells its name rather than reaching it through an object — a member of a name that may hold
the global object, `globalThis.x` as much as `G.x` after `var G = this`, would no longer find it
once it leaves the global object — it is not exported (an importer reads its value where the
module leaves it, which a body local cannot answer), the program keeps no reflection surface
that could read it by name, and the program stores no property on the global object under a
runtime key (`refinery.lib.scripts.js.analysis.model.SemanticModel.has_opaque_global_write`):
such a write may target the binding's own name, which a relocation would no longer receive.
"""
model = liveness.model
if model.has_reflection_surface() or model.has_opaque_global_write():
return None
if binding.exported:
return None
if not all(isinstance(ref, JsIdentifier) for ref in (*binding.reads, *binding.writes)):
return None
function = liveness.pseudo_local_owner(binding)
if function is None:
return None
if not liveness.is_dead_on_entry(binding, function):
return None
if not _dead_where_another_run_may_write(liveness, reaching, binding, function):
return None
return function
def localizable_bindings(
liveness: LivenessModel,
reaching: ReachingModel,
) -> list[tuple[Binding, Node]]:
"""
Every script-scope `var` binding that can be relocated into a function, each paired with that
function, in the order the script declares them.
"""
result: list[tuple[Binding, Node]] = []
for binding in liveness.model.root_scope.bindings.values():
function = localization_target(liveness, reaching, binding)
if function is not None:
result.append((binding, function))
return result
def _dead_where_another_run_may_write(
liveness: LivenessModel,
reaching: ReachingModel,
binding: Binding,
function: Node,
) -> bool:
"""
Whether *binding* is live at no point of *function*'s body at which code other than the body's
own statements may write it. The graph does not order the parts of one node, so a binding read
or live anywhere in the node of such a point counts as live there.
"""
sites = reaching.foreign_kills(binding, function)
if sites is None:
return False
for site in sites:
live = liveness.live_around(site)
if live is None or binding in live:
return False
return True
Functions
def localization_target(liveness, reaching, binding)-
The function into which binding, a script-scope
var, can be soundly relocated, orNone. A binding qualifies when one function's body holds every read of it and every store into it (LivenessModel.pseudo_local_owner()), every run of that function reads only what the run stored (see the module documentation), every reference spells its name rather than reaching it through an object — a member of a name that may hold the global object,globalThis.xas much asG.xaftervar G = this, would no longer find it once it leaves the global object — it is not exported (an importer reads its value where the module leaves it, which a body local cannot answer), the program keeps no reflection surface that could read it by name, and the program stores no property on the global object under a runtime key (SemanticModel.has_opaque_global_write()): such a write may target the binding's own name, which a relocation would no longer receive.Expand source code Browse git
def localization_target( liveness: LivenessModel, reaching: ReachingModel, binding: Binding, ) -> Node | None: """ The function into which *binding*, a script-scope `var`, can be soundly relocated, or `None`. A binding qualifies when one function's body holds every read of it and every store into it (`refinery.lib.scripts.js.analysis.liveness.LivenessModel.pseudo_local_owner`), every run of that function reads only what the run stored (see the module documentation), every reference spells its name rather than reaching it through an object — a member of a name that may hold the global object, `globalThis.x` as much as `G.x` after `var G = this`, would no longer find it once it leaves the global object — it is not exported (an importer reads its value where the module leaves it, which a body local cannot answer), the program keeps no reflection surface that could read it by name, and the program stores no property on the global object under a runtime key (`refinery.lib.scripts.js.analysis.model.SemanticModel.has_opaque_global_write`): such a write may target the binding's own name, which a relocation would no longer receive. """ model = liveness.model if model.has_reflection_surface() or model.has_opaque_global_write(): return None if binding.exported: return None if not all(isinstance(ref, JsIdentifier) for ref in (*binding.reads, *binding.writes)): return None function = liveness.pseudo_local_owner(binding) if function is None: return None if not liveness.is_dead_on_entry(binding, function): return None if not _dead_where_another_run_may_write(liveness, reaching, binding, function): return None return function def localizable_bindings(liveness, reaching)-
Every script-scope
varbinding that can be relocated into a function, each paired with that function, in the order the script declares them.Expand source code Browse git
def localizable_bindings( liveness: LivenessModel, reaching: ReachingModel, ) -> list[tuple[Binding, Node]]: """ Every script-scope `var` binding that can be relocated into a function, each paired with that function, in the order the script declares them. """ result: list[tuple[Binding, Node]] = [] for binding in liveness.model.root_scope.bindings.values(): function = localization_target(liveness, reaching, binding) if function is not None: result.append((binding, function)) return result