Module refinery.lib.scripts.au3.analysis.existence
The existence model: the names a global may take when the tree runs. The may-write summary refinement asks it — a bare write inside a function creates a local when no global of the name exists, so a summary may drop the write only when the census proves no channel can create that global — and the interpreter's frame relies on the same fact the other way: the summary the refinement emptied is what makes the frame's implicit local one AutoIt itself creates.
The census walks every channel that can create a global: the command-line constants AutoIt
defines before the first statement runs, a write at script level, a Global declaration
anywhere in the tree, an Assign with a literal name, an Execute payload or a computed Call
name resolved to the string it spells — a literal shape, or a call with literal arguments to a
script function probed through Au3Interpreter.probe()
— the variable names every call hands to a ByRef parameter, and the registrations whose
registered function receives arguments, which must name a script function literally. Source text
the parser could not read — an error node in the tree, or a payload this parser reads as
nothing — is read through read_unparsed(), and every
variable it spells may be a global. The parser's record list is not an input: its texts also
live in the tree — an error node is its own record, and a repair record names a token of a line
the parser went on to read — while the list itself outlives the edits that remove those lines,
so a census over it would stay poisoned over a tree its own transforms had cleaned. A channel
the census cannot read — an include it cannot see, a computed Assign name, an unresolvable
payload or Call name, an indirect callee, a forwarding Call of Call, a registration whose
name is not a literal script function, a registration or an option set through a Call whose
sole argument may be a CallArgArray expansion, an option setting that may expand variables,
macros, or
environment strings inside string literals, unread text that may call a channel or declare a
function, or a probe budget the tree exhausts — answers that any name may be a global, and that
the tree may set MustDeclareVars, which the unread channel may do as well. Every other callee
is covered by one closure claim: a name none of the channels above owns is an AutoIt builtin
that creates no script variable or a call AutoIt refuses to run, which is the one
documentation-sourced boundary of the census.
An Opt that may set MustDeclareVars — tree-level or inside a payload the census examines —
makes every implicit write fatal, so the model reports it and the summary refinement refuses to
lean on the census while it holds. The census does not see the order in which the settings run,
so a zero elsewhere clears nothing.
This module also owns what a dynamic-name channel writes: execute_kills() and assign_kills(),
the answers the constant-binding walk, the alias map, and the usage model read, over the one
payload parse they share — and call_target(), the callee a computed Call invocation resolves
to, which the usage model's read-side channel walk reads beside it.
A census runs once per tree version, and a run holds one Au3ProbeMemo across its versions, so
a census asks the interpreter only for the probes no earlier census made.
Expand source code Browse git
"""
The existence model: the names a global may take when the tree runs. The may-write summary
refinement asks it — a bare write inside a function creates a local when no global of the name
exists, so a summary may drop the write only when the census proves no channel can create that
global — and the interpreter's frame relies on the same fact the other way: the summary the
refinement emptied is what makes the frame's implicit local one AutoIt itself creates.
The census walks every channel that can create a global: the command-line constants AutoIt
defines before the first statement runs, a write at script level, a `Global` declaration
anywhere in the tree, an `Assign` with a literal name, an `Execute` payload or a computed `Call`
name resolved to the string it spells — a literal shape, or a call with literal arguments to a
script function probed through `refinery.lib.scripts.au3.analysis.interpreter.Au3Interpreter.probe`
— the variable names every call hands to a `ByRef` parameter, and the registrations whose
registered function receives arguments, which must name a script function literally. Source text
the parser could not read — an error node in the tree, or a payload this parser reads as
nothing — is read through `refinery.lib.scripts.au3.analysis.unparsed.read_unparsed`, and every
variable it spells may be a global. The parser's record list is not an input: its texts also
live in the tree — an error node is its own record, and a repair record names a token of a line
the parser went on to read — while the list itself outlives the edits that remove those lines,
so a census over it would stay poisoned over a tree its own transforms had cleaned. A channel
the census cannot read — an include it cannot see, a computed `Assign` name, an unresolvable
payload or `Call` name, an indirect callee, a forwarding `Call` of `Call`, a registration whose
name is not a literal script function, a registration or an option set through a `Call` whose
sole argument may be a `CallArgArray` expansion, an option setting that may expand variables,
macros, or
environment strings inside string literals, unread text that may call a channel or declare a
function, or a probe budget the tree exhausts — answers that any name may be a global, and that
the tree may set `MustDeclareVars`, which the unread channel may do as well. Every other callee
is covered by one closure claim: a name none of the channels above owns is an AutoIt builtin
that creates no script variable or a call AutoIt refuses to run, which is the one
documentation-sourced boundary of the census.
An `Opt` that may set `MustDeclareVars` — tree-level or inside a payload the census examines —
makes every implicit write fatal, so the model reports it and the summary refinement refuses to
lean on the census while it holds. The census does not see the order in which the settings run,
so a zero elsewhere clears nothing.
This module also owns what a dynamic-name channel writes: `execute_kills` and `assign_kills`,
the answers the constant-binding walk, the alias map, and the usage model read, over the one
payload parse they share — and `call_target`, the callee a computed `Call` invocation resolves
to, which the usage model's read-side channel walk reads beside it.
A census runs once per tree version, and a run holds one `Au3ProbeMemo` across its versions, so
a census asks the interpreter only for the probes no earlier census made.
"""
from __future__ import annotations
import functools
from collections.abc import Sequence
from dataclasses import dataclass
from typing import cast
from refinery.lib.scripts import UnspellableNode
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.analysis.builtins import OPTION_SETTERS
from refinery.lib.scripts.au3.analysis.interpreter import Au3Interpreter, Au3Probe
from refinery.lib.scripts.au3.analysis.scopes import Au3ScopeModel, Binding
from refinery.lib.scripts.au3.analysis.unparsed import read_unparsed, spelled_name
from refinery.lib.scripts.au3.analysis.values import (
Au3Value,
Kind,
literal_value,
to_number,
truthy,
)
from refinery.lib.scripts.au3.model import (
Au3Call,
Au3EnumDeclaration,
Au3ErrorNode,
Au3FunctionDeclaration,
Au3Identifier,
Au3MemberAccess,
Au3ScopeKind,
Au3Script,
Au3StringLiteral,
Au3Variable,
Au3VariableDeclaration,
Expression,
)
from refinery.lib.scripts.au3.parser import Au3Parser
from refinery.lib.scripts.au3.synth import Au3Synthesizer
#: The globals every script holds before its first statement runs: the command-line constants
#: exist whether or not parameters are passed, so a bare write of either inside a function
#: reaches the constant — a fatal error — instead of creating a local.
_PREDEFINED_GLOBALS = frozenset({'cmdline', 'cmdlineraw'})
#: The registration builtins whose registered function receives arguments the registration
#: passes through, each mapped to the argument positions that hold the registered name: a name
#: registered there that is not a literal naming a script function may reach the builtin
#: `Assign` with arguments, so the census answers that any name may be a global. The
#: zero-argument registrations invoke an `Assign` shape with no arguments, which creates
#: nothing. `DllCallbackRegister` names its function first; its other leading position is the
#: return type, which names no function.
_REGISTRATION_NAME_POSITIONS: dict[str, frozenset[int]] = {
'guiregistermsg' : frozenset({1}),
'guictrlregisterlistviewsort' : frozenset({1}),
'dllcallbackregister' : frozenset({0}),
'objevent' : frozenset({1}),
}
#: The options under which a string literal expands the variables, macros, or environment
#: strings it spells when the tree runs: a literal the census reads as a name may then spell any
#: other, so a tree that may set one answers that any name may be a global.
_EXPANDING_OPTIONS = frozenset({'expandvarstrings', 'expandenvstrings'})
#: Every builtin whose call the census reads as a channel: source text the parser could not
#: read that names one may make a call the census can read only from a parsed tree.
_CHANNEL_BUILTINS = frozenset({
'assign',
'call',
'execute',
*_REGISTRATION_NAME_POSITIONS,
*OPTION_SETTERS,
})
#: The total steps every probe the census makes may spend together: a tree that exhausts them
#: answers that any name may be a global. Roughly forty times what the a3x sample this model
#: was built over spends across all of its probes.
_MAX_CENSUS_PROBE_STEPS = 1_000_000
@functools.lru_cache(maxsize=None)
def parse_payload(text: str) -> Expression | None:
"""
The expression an `Execute` payload spells, or `None` where the payload parses to nothing:
AutoIt answers `""` there (probe3), and `""` writes nothing — only a call inside the parsed
expression can. Every reader walks the parse, so one payload text parses once.
"""
return Au3Parser(text).parse_expression()
def execute_kills(call: Au3Call) -> set[str] | None:
"""
The names an `Execute` call may write: a constant argument spells one expression, so only a
call inside it can write anything; a parse failure may still be a payload AutoIt runs — this
parser's refusal is not AutoIt's — so it answers any name except the empty payload AutoIt
answers `""` for (probe3). Any other argument can execute arbitrary code.
"""
if len(call.arguments) != 1:
return None
literal = ast.unwrap_parens(call.arguments[0])
if not isinstance(literal, Au3StringLiteral):
return None
expression = parse_payload(literal.value)
if expression is None:
return set() if not literal.value.strip() else None
if ast.contains_a_call(expression):
return None
return set()
def assign_kills(call: Au3Call) -> set[str] | None:
"""
The names an `Assign` call may write: a constant first argument names one variable, anything
else may name any.
"""
if not call.arguments:
return set()
literal = ast.unwrap_parens(call.arguments[0])
if not isinstance(literal, Au3StringLiteral):
return None
return {spelled_name(literal.value)}
def call_target(
name: str,
passed: Sequence[Expression],
function_names: frozenset[str],
) -> str | None:
"""
The callee a computed `Call` invocation of *name* resolves to over *passed*, the arguments it
forwards: the name the string spells, or `None` where the resolution refuses — a forwarding
`Call` of `Call`, and a registration or option setter whose sole forwarded argument may be
the array a `CallArgArray` expansion unpacks. Both read a position past the first, an element
no walk sees.
"""
target = spelled_name(name)
if target == 'call':
return None
if (
len(passed) == 1
and (target in _REGISTRATION_NAME_POSITIONS or target in OPTION_SETTERS)
and _may_expand(passed[0], function_names)
):
return None
return target
def _may_expand(argument: Expression, function_names: frozenset[str]) -> bool:
"""
Whether *argument*, the sole one a computed `Call` passes on, may be the array a
`CallArgArray` expansion unpacks: only a literal scalar is not.
"""
value = literal_value(argument, function_names)
return value is None or value.kind is Kind.ARRAY
@dataclass(frozen=True)
class Au3ExistenceModel:
"""
What the census learned about one tree version: the names a global may take — `None` when
any name may — and whether the tree may set `MustDeclareVars`, which makes every implicit
write fatal; with both, which writes inside a function may target a global. Built once per
tree version by `build_existence_model`.
"""
possible_globals: frozenset[str] | None
implicit_writes_are_fatal: bool
def global_may_exist(self, name: str) -> bool:
"""
Whether a global of *name* may exist when the tree runs.
"""
return self.possible_globals is None or name in self.possible_globals
def write_may_target_global(self, binding: Binding, name: str, creates_storage: bool) -> bool:
"""
Whether a write of *name* inside a function, whose storage *binding* answers, may target
a global. A provable local never does and a provable global always does. A write that
proves nothing creates a local where no global of the name exists, so it may target one
only where the census answers that one may exist, or where the tree may make every
implicit write fatal. A write that creates no storage — a `ReDim`, which resizes an array
that exists or ends the script — is outside that refinement and may target a global
wherever no local provably holds the name.
"""
if binding is Binding.LOCAL:
return False
if binding is Binding.GLOBAL or not creates_storage:
return True
return self.implicit_writes_are_fatal or self.global_may_exist(name)
def write_is_fatal(self, name: str) -> bool:
"""
Whether a store of *name* is a fatal error the tree runs into: one of the command-line
constants every script starts with is read-only, so its store can never leave.
"""
return name in _PREDEFINED_GLOBALS
_ProbeKey = tuple[str, frozenset[str], str]
class Au3ProbeMemo:
"""
The probes the censuses of one run made, so a census over a later tree version answers a
probe it asks again without running it. A probe reads the spelling of the function it runs,
the names of the script functions — a nested call into one refuses — and its arguments, so
its key holds all three, each argument by the exact spelling of its kind and payload; a
function no text spells is probed afresh. A census charges the steps a remembered probe spent
against its quota exactly as it would a fresh one, which keeps its answer a function of the
tree alone. Only the probes the latest two censuses asked are held, so a function that a fold
rewrites in every version costs no more memory than one census holds.
"""
def __init__(self):
self._held: dict[_ProbeKey, Au3Probe] = {}
self._asked: dict[_ProbeKey, Au3Probe] = {}
self._spellings: dict[int, str | None] = {}
def renew(self) -> None:
"""
Begin a census: the probes the previous census asked stay available, older ones drop.
"""
self._held = self._asked
self._asked = {}
self._spellings = {}
def probe(
self,
interpreter: Au3Interpreter,
function: Au3FunctionDeclaration,
arguments: Sequence[Au3Value],
) -> Au3Probe:
spelling = self._spelling(function)
if spelling is None:
return interpreter.probe(function, arguments)
key = (spelling, interpreter.function_names, repr(tuple(arguments)))
probe = self._asked.get(key)
if probe is None:
probe = self._held.get(key)
if probe is None:
probe = interpreter.probe(function, arguments)
self._asked[key] = probe
return probe
def _spelling(self, function: Au3FunctionDeclaration) -> str | None:
"""
The source text of *function*, synthesized once per census: the tree does not change
while a census runs.
"""
key = id(function)
if key not in self._spellings:
try:
self._spellings[key] = Au3Synthesizer().convert(function)
except (UnspellableNode, RecursionError):
self._spellings[key] = None
return self._spellings[key]
class _Poison(Exception):
"""
The signal that unwinds the census: a channel whose global creation the tree may spell
without spelling its name, so the answer is that any name may be a global — and that the
tree may set `MustDeclareVars`: the walk ends there, so a setter in that channel or past it
goes unread.
"""
class _Census:
"""
One walk over every global-creation channel of a tree, collecting the names a global may
take. A channel the walk cannot read raises `_Poison`, and the answer becomes that any name
may be a global and every implicit write may be fatal.
"""
def __init__(self, scope_model: Au3ScopeModel, memo: Au3ProbeMemo):
self.scope_model = scope_model
self.functions = scope_model.functions
self.function_names = scope_model.function_names
self.interpreter = Au3Interpreter(scope_model, {})
self.memo = memo
self.probe_steps = 0
self.names: set[str] = set(_PREDEFINED_GLOBALS)
self.implicit_writes_are_fatal = False
def collect(self, root: Au3Script) -> Au3ExistenceModel:
try:
if self.scope_model.holds_an_include:
raise _Poison
for node in root.walk_in_order():
if isinstance(node, (Au3VariableDeclaration, Au3EnumDeclaration)):
if node.scope is Au3ScopeKind.GLOBAL:
for declarator in node.declarations:
if declarator.variable is not None:
self.names.add(declarator.variable.name)
continue
if isinstance(node, Au3ErrorNode):
self._examine_unparsed(node.text)
elif isinstance(node, Au3Call):
self._examine_call(node)
for node in ast.scope_nodes(root.body):
if isinstance(node, Au3Variable) and ast.is_write_occurrence(node):
self.names.add(node.name)
except (_Poison, RecursionError):
return Au3ExistenceModel(None, True)
return Au3ExistenceModel(frozenset(self.names), self.implicit_writes_are_fatal)
def _examine_call(self, call: Au3Call) -> None:
callee = ast.unwrap_parens(call.callee)
if isinstance(callee, Au3MemberAccess):
return
if not isinstance(callee, Au3Identifier):
raise _Poison
self._examine_target(callee.name, call.arguments)
def _examine_target(self, name: str, arguments: Sequence[Expression]) -> None:
"""
One call of *name* over *arguments*, as the tree spells it or as a computed `Call`
resolves it: a script function receives its `ByRef` arguments, and a channel builtin is
read over the arguments that reach it — after the name, where a `Call` spells one.
"""
if name in self.functions:
self._collect_byref(self.functions[name], arguments)
elif name == 'assign':
self._examine_assign(arguments)
elif name == 'execute':
self._examine_execute(arguments)
elif name == 'call':
self._examine_computed_call(arguments)
elif name in _REGISTRATION_NAME_POSITIONS:
for index in _REGISTRATION_NAME_POSITIONS[name]:
if index < len(arguments):
self._examine_registered_name(arguments[index])
elif name in OPTION_SETTERS:
self._examine_option(arguments)
def _collect_byref(
self,
function: Au3FunctionDeclaration,
arguments: Sequence[Expression],
) -> None:
"""
The variable names *arguments* hand into the `ByRef` parameters of *function*. Whether an
undeclared name there is auto-created, fatal, or dead is unpinned, and spelling it is
sound under every answer: an extra spelled name only keeps more writes in the summaries.
"""
for variable in ast.byref_arguments(function, arguments):
self.names.add(variable.name)
def _examine_assign(self, arguments: Sequence[Expression]) -> None:
if not arguments:
return
literal = ast.unwrap_parens(arguments[0])
if not isinstance(literal, Au3StringLiteral):
raise _Poison
self.names.add(spelled_name(literal.value))
def _examine_execute(self, arguments: Sequence[Expression]) -> None:
if not arguments:
return
payload = self._resolve_string(arguments[0])
if payload is None:
raise _Poison
self._examine_payload(payload)
def _examine_payload(self, payload: str) -> None:
"""
One resolved `Execute` payload: only a call inside it can write (probe3), so every call
in the parsed expression is examined by the same rules the tree-level calls follow — a
nested payload resolving the same way, the structural recursion bounded by the
`RecursionError` the collect walk catches. A payload this parser reads as nothing may
still be one AutoIt runs, so its text is read token by token.
"""
expression = parse_payload(payload)
if expression is None:
self._examine_unparsed(payload)
return
for node in expression.walk_in_order():
if isinstance(node, Au3Call):
self._examine_call(node)
def _examine_computed_call(self, arguments: Sequence[Expression]) -> None:
"""
A `Call` over a computed name: the name resolves to what it spells, and the target is
examined over the arguments that follow it. A resolution `call_target` refuses poisons,
and so does a name the census cannot read.
"""
if not arguments:
return
name = self._resolve_string(arguments[0])
if name is None:
raise _Poison
target = call_target(name, arguments[1:], self.function_names)
if target is None:
raise _Poison
self._examine_target(target, arguments[1:])
def _examine_registered_name(self, argument: Expression) -> None:
literal = ast.unwrap_parens(argument)
if (
not isinstance(literal, Au3StringLiteral)
or spelled_name(literal.value) not in self.functions
):
raise _Poison
def _examine_option(self, arguments: Sequence[Expression]) -> None:
"""
One option setter: an option that may expand string literals is a channel the census
cannot read, `MustDeclareVars` makes every implicit write fatal, and an option name the
census cannot read may be either. A value may set an option unless it reads as zero both
as a number and as a condition — which reading AutoIt takes is unpinned, and the two
split on a non-empty string whose number is zero, such as `"0"`. The census does not see
the order in which the settings run, so one that may set an option holds for the whole
tree and no zero clears it. A setter of one argument only reads the option.
"""
if len(arguments) < 2:
return
option = self._resolve_string(arguments[0])
name = None if option is None else spelled_name(option)
if name is not None and name not in _EXPANDING_OPTIONS and name != 'mustdeclarevars':
return
value = literal_value(arguments[1], self.function_names)
if value is not None and to_number(value) == 0 and truthy(value) is False:
return
if name != 'mustdeclarevars':
raise _Poison
self.implicit_writes_are_fatal = True
def _examine_unparsed(self, text: str) -> None:
"""
Source text the parser could not read, which AutoIt may still run, read through
`read_unparsed`: every variable it spells may be a global, and text that names a channel
builtin, opens a call on a computed callee, or declares a function — whose `ByRef`
positions only a parsed declaration shows — is a channel the census cannot read.
"""
unparsed = read_unparsed(text)
if (
unparsed.computed_callee
or unparsed.declares_function
or unparsed.identifiers & _CHANNEL_BUILTINS
):
raise _Poison
self.names.update(unparsed.variables)
def _resolve_string(self, expression: Expression | None) -> str | None:
"""
The string *expression* spells where the census reads a payload or a name: a literal
shape or the pieces one renders — `Chr(0)` standing alone spells the same string its `&`
chain does — or a call with literal arguments to a script function, probed over the
value domain. Anything else the census cannot read.
"""
node = ast.unwrap_parens(expression)
value: Au3Value | None = None
if isinstance(node, Au3Call):
callee = ast.direct_callee(node)
if callee is not None and callee.name in self.functions:
arguments: list[Au3Value] = []
for argument in node.arguments:
value = literal_value(argument, self.function_names)
if value is None:
return None
arguments.append(value)
probe = self.memo.probe(self.interpreter, self.functions[callee.name], arguments)
self.probe_steps += probe.steps
if self.probe_steps > _MAX_CENSUS_PROBE_STEPS:
raise _Poison
value = probe.value
if value is None:
value = literal_value(cast(Expression | None, node), self.function_names)
if value is not None and value.kind is Kind.STRING:
return cast(str, value.payload)
return None
def build_existence_model(
root: Au3Script,
scope_model: Au3ScopeModel,
memo: Au3ProbeMemo | None = None,
) -> Au3ExistenceModel:
"""
Walk every global-creation channel of *root* and collect the names a global may take, over
the functions and the include answer of *scope_model*. A *memo* the run holds across its tree
versions answers the probes an earlier census already made.
"""
if memo is None:
memo = Au3ProbeMemo()
memo.renew()
return _Census(scope_model, memo).collect(root)
Functions
def parse_payload(text)-
The expression an
Executepayload spells, orNonewhere the payload parses to nothing: AutoIt answers""there (probe3), and""writes nothing — only a call inside the parsed expression can. Every reader walks the parse, so one payload text parses once.Expand source code Browse git
@functools.lru_cache(maxsize=None) def parse_payload(text: str) -> Expression | None: """ The expression an `Execute` payload spells, or `None` where the payload parses to nothing: AutoIt answers `""` there (probe3), and `""` writes nothing — only a call inside the parsed expression can. Every reader walks the parse, so one payload text parses once. """ return Au3Parser(text).parse_expression() def execute_kills(call)-
The names an
Executecall may write: a constant argument spells one expression, so only a call inside it can write anything; a parse failure may still be a payload AutoIt runs — this parser's refusal is not AutoIt's — so it answers any name except the empty payload AutoIt answers""for (probe3). Any other argument can execute arbitrary code.Expand source code Browse git
def execute_kills(call: Au3Call) -> set[str] | None: """ The names an `Execute` call may write: a constant argument spells one expression, so only a call inside it can write anything; a parse failure may still be a payload AutoIt runs — this parser's refusal is not AutoIt's — so it answers any name except the empty payload AutoIt answers `""` for (probe3). Any other argument can execute arbitrary code. """ if len(call.arguments) != 1: return None literal = ast.unwrap_parens(call.arguments[0]) if not isinstance(literal, Au3StringLiteral): return None expression = parse_payload(literal.value) if expression is None: return set() if not literal.value.strip() else None if ast.contains_a_call(expression): return None return set() def assign_kills(call)-
The names an
Assigncall may write: a constant first argument names one variable, anything else may name any.Expand source code Browse git
def assign_kills(call: Au3Call) -> set[str] | None: """ The names an `Assign` call may write: a constant first argument names one variable, anything else may name any. """ if not call.arguments: return set() literal = ast.unwrap_parens(call.arguments[0]) if not isinstance(literal, Au3StringLiteral): return None return {spelled_name(literal.value)} def call_target(name, passed, function_names)-
The callee a computed
Callinvocation of name resolves to over passed, the arguments it forwards: the name the string spells, orNonewhere the resolution refuses — a forwardingCallofCall, and a registration or option setter whose sole forwarded argument may be the array aCallArgArrayexpansion unpacks. Both read a position past the first, an element no walk sees.Expand source code Browse git
def call_target( name: str, passed: Sequence[Expression], function_names: frozenset[str], ) -> str | None: """ The callee a computed `Call` invocation of *name* resolves to over *passed*, the arguments it forwards: the name the string spells, or `None` where the resolution refuses — a forwarding `Call` of `Call`, and a registration or option setter whose sole forwarded argument may be the array a `CallArgArray` expansion unpacks. Both read a position past the first, an element no walk sees. """ target = spelled_name(name) if target == 'call': return None if ( len(passed) == 1 and (target in _REGISTRATION_NAME_POSITIONS or target in OPTION_SETTERS) and _may_expand(passed[0], function_names) ): return None return target def build_existence_model(root, scope_model, memo=None)-
Walk every global-creation channel of root and collect the names a global may take, over the functions and the include answer of scope_model. A memo the run holds across its tree versions answers the probes an earlier census already made.
Expand source code Browse git
def build_existence_model( root: Au3Script, scope_model: Au3ScopeModel, memo: Au3ProbeMemo | None = None, ) -> Au3ExistenceModel: """ Walk every global-creation channel of *root* and collect the names a global may take, over the functions and the include answer of *scope_model*. A *memo* the run holds across its tree versions answers the probes an earlier census already made. """ if memo is None: memo = Au3ProbeMemo() memo.renew() return _Census(scope_model, memo).collect(root)
Classes
class Au3ExistenceModel (possible_globals, implicit_writes_are_fatal)-
What the census learned about one tree version: the names a global may take —
Nonewhen any name may — and whether the tree may setMustDeclareVars, which makes every implicit write fatal; with both, which writes inside a function may target a global. Built once per tree version bybuild_existence_model().Expand source code Browse git
@dataclass(frozen=True) class Au3ExistenceModel: """ What the census learned about one tree version: the names a global may take — `None` when any name may — and whether the tree may set `MustDeclareVars`, which makes every implicit write fatal; with both, which writes inside a function may target a global. Built once per tree version by `build_existence_model`. """ possible_globals: frozenset[str] | None implicit_writes_are_fatal: bool def global_may_exist(self, name: str) -> bool: """ Whether a global of *name* may exist when the tree runs. """ return self.possible_globals is None or name in self.possible_globals def write_may_target_global(self, binding: Binding, name: str, creates_storage: bool) -> bool: """ Whether a write of *name* inside a function, whose storage *binding* answers, may target a global. A provable local never does and a provable global always does. A write that proves nothing creates a local where no global of the name exists, so it may target one only where the census answers that one may exist, or where the tree may make every implicit write fatal. A write that creates no storage — a `ReDim`, which resizes an array that exists or ends the script — is outside that refinement and may target a global wherever no local provably holds the name. """ if binding is Binding.LOCAL: return False if binding is Binding.GLOBAL or not creates_storage: return True return self.implicit_writes_are_fatal or self.global_may_exist(name) def write_is_fatal(self, name: str) -> bool: """ Whether a store of *name* is a fatal error the tree runs into: one of the command-line constants every script starts with is read-only, so its store can never leave. """ return name in _PREDEFINED_GLOBALSInstance variables
var possible_globals-
The type of the None singleton.
var implicit_writes_are_fatal-
The type of the None singleton.
Methods
def global_may_exist(self, name)-
Whether a global of name may exist when the tree runs.
Expand source code Browse git
def global_may_exist(self, name: str) -> bool: """ Whether a global of *name* may exist when the tree runs. """ return self.possible_globals is None or name in self.possible_globals def write_may_target_global(self, binding, name, creates_storage)-
Whether a write of name inside a function, whose storage binding answers, may target a global. A provable local never does and a provable global always does. A write that proves nothing creates a local where no global of the name exists, so it may target one only where the census answers that one may exist, or where the tree may make every implicit write fatal. A write that creates no storage — a
ReDim, which resizes an array that exists or ends the script — is outside that refinement and may target a global wherever no local provably holds the name.Expand source code Browse git
def write_may_target_global(self, binding: Binding, name: str, creates_storage: bool) -> bool: """ Whether a write of *name* inside a function, whose storage *binding* answers, may target a global. A provable local never does and a provable global always does. A write that proves nothing creates a local where no global of the name exists, so it may target one only where the census answers that one may exist, or where the tree may make every implicit write fatal. A write that creates no storage — a `ReDim`, which resizes an array that exists or ends the script — is outside that refinement and may target a global wherever no local provably holds the name. """ if binding is Binding.LOCAL: return False if binding is Binding.GLOBAL or not creates_storage: return True return self.implicit_writes_are_fatal or self.global_may_exist(name) def write_is_fatal(self, name)-
Whether a store of name is a fatal error the tree runs into: one of the command-line constants every script starts with is read-only, so its store can never leave.
Expand source code Browse git
def write_is_fatal(self, name: str) -> bool: """ Whether a store of *name* is a fatal error the tree runs into: one of the command-line constants every script starts with is read-only, so its store can never leave. """ return name in _PREDEFINED_GLOBALS
class Au3ProbeMemo-
The probes the censuses of one run made, so a census over a later tree version answers a probe it asks again without running it. A probe reads the spelling of the function it runs, the names of the script functions — a nested call into one refuses — and its arguments, so its key holds all three, each argument by the exact spelling of its kind and payload; a function no text spells is probed afresh. A census charges the steps a remembered probe spent against its quota exactly as it would a fresh one, which keeps its answer a function of the tree alone. Only the probes the latest two censuses asked are held, so a function that a fold rewrites in every version costs no more memory than one census holds.
Expand source code Browse git
class Au3ProbeMemo: """ The probes the censuses of one run made, so a census over a later tree version answers a probe it asks again without running it. A probe reads the spelling of the function it runs, the names of the script functions — a nested call into one refuses — and its arguments, so its key holds all three, each argument by the exact spelling of its kind and payload; a function no text spells is probed afresh. A census charges the steps a remembered probe spent against its quota exactly as it would a fresh one, which keeps its answer a function of the tree alone. Only the probes the latest two censuses asked are held, so a function that a fold rewrites in every version costs no more memory than one census holds. """ def __init__(self): self._held: dict[_ProbeKey, Au3Probe] = {} self._asked: dict[_ProbeKey, Au3Probe] = {} self._spellings: dict[int, str | None] = {} def renew(self) -> None: """ Begin a census: the probes the previous census asked stay available, older ones drop. """ self._held = self._asked self._asked = {} self._spellings = {} def probe( self, interpreter: Au3Interpreter, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], ) -> Au3Probe: spelling = self._spelling(function) if spelling is None: return interpreter.probe(function, arguments) key = (spelling, interpreter.function_names, repr(tuple(arguments))) probe = self._asked.get(key) if probe is None: probe = self._held.get(key) if probe is None: probe = interpreter.probe(function, arguments) self._asked[key] = probe return probe def _spelling(self, function: Au3FunctionDeclaration) -> str | None: """ The source text of *function*, synthesized once per census: the tree does not change while a census runs. """ key = id(function) if key not in self._spellings: try: self._spellings[key] = Au3Synthesizer().convert(function) except (UnspellableNode, RecursionError): self._spellings[key] = None return self._spellings[key]Methods
def renew(self)-
Begin a census: the probes the previous census asked stay available, older ones drop.
Expand source code Browse git
def renew(self) -> None: """ Begin a census: the probes the previous census asked stay available, older ones drop. """ self._held = self._asked self._asked = {} self._spellings = {} def probe(self, interpreter, function, arguments)-
Expand source code Browse git
def probe( self, interpreter: Au3Interpreter, function: Au3FunctionDeclaration, arguments: Sequence[Au3Value], ) -> Au3Probe: spelling = self._spelling(function) if spelling is None: return interpreter.probe(function, arguments) key = (spelling, interpreter.function_names, repr(tuple(arguments))) probe = self._asked.get(key) if probe is None: probe = self._held.get(key) if probe is None: probe = interpreter.probe(function, arguments) self._asked[key] = probe return probe