Module refinery.lib.scripts.au3.analysis.scopes
AutoIt3 scoping: the documented rules that decide which storage a write or a read targets and
which storage an occurrence may reach, and the script's declared functions. This module is the
single owner of that question — the constant-binding walk in
refinery.lib.scripts.au3.analysis.propagation consults it and never re-derives the answer.
The rules, per the AutoIt3 documentation:
- at script level, any declaration or assignment writes the global of that name regardless of
keyword — an unseen
#includecannot change what a script-level write targets; - inside a function,
Local,Global, andStaticforce their storage, and a bareConstintroduces a constant of function scope; - a bare assignment,
Dim, orReDiminside a function writes the global when a same-named global exists and a local otherwise — but a same-named global in an unseen#includeis indistinguishable from no global at all, so such a write proves nothing and is reported asBinding.NONE. It proves a local instead when the written name is a parameter of the function, or aLocal,Static, or localConst/Enumstatement declaring it stands directly in the function body before the statement holding the write: a top-level statement before the write runs on every path that reaches it, because AutoIt has no jumps, and an existing local takes the write (probe4 23). AFor/For…Inloop claiming the name stands the same way — the loop creates its variable local before its first test, a zero trip included (probe4 31), so a write in its body or after it finds the local. AGlobaldeclaration of the name anywhere in the function still proves the global and keeps the writeNONE.
The model also owns the declared-name question: whether a declaration backs a plain write, the
refusal MustDeclareVars answers a plain write with when none does. A parameter, a claim the
function's body precedes the write with, or a Global declaration standing directly in the body
the write stands in before it backs the write; a claim a branch holds runs only when the branch
does, a declaration in another body runs when that body runs, and a Dim claim names a storage
the option does not recognize, so none of the three backs anything.
The model also holds the tree's constant names — a Const or Enum declaration or a Const
parameter claims one — because no scope placement softens that fact: writing a constant is an
interpreter refusal, so a caller that flattens a dynamic write over a name asks the model first.
It answers whether the tree holds an include the same way — the node, or the directive an
include line with trailing text becomes — because an unseen included file may declare storage
and constants no tree declaration covers, so every caller that rewrites around the tree's own
declarations asks the model first. It answers whether the parser recovered an error node
anywhere for the same reason: text no walk reads may hide a declaration the same way.
Expand source code Browse git
"""
AutoIt3 scoping: the documented rules that decide which storage a write or a read targets and
which storage an occurrence may reach, and the script's declared functions. This module is the
single owner of that question — the constant-binding walk in
`refinery.lib.scripts.au3.analysis.propagation` consults it and never re-derives the answer.
The rules, per the AutoIt3 documentation:
- at script level, any declaration or assignment writes the global of that name regardless of
keyword — an unseen `#include` cannot change what a script-level write targets;
- inside a function, `Local`, `Global`, and `Static` force their storage, and a bare `Const`
introduces a constant of function scope;
- a bare assignment, `Dim`, or `ReDim` inside a function writes the global when a same-named
global exists and a local otherwise — but a same-named global in an unseen `#include` is
indistinguishable from no global at all, so such a write proves nothing and is reported as
`Binding.NONE`. It proves a local instead when the written name is a parameter of the function,
or a `Local`, `Static`, or local `Const`/`Enum` statement declaring it stands directly in the
function body before the statement holding the write: a top-level statement before the write
runs on every path that reaches it, because AutoIt has no jumps, and an existing local takes
the write (probe4 23). A `For`/`For…In` loop claiming the name stands the same way — the loop
creates its variable local before its first test, a zero trip included (probe4 31), so a write
in its body or after it finds the local. A `Global` declaration of the name anywhere in the
function still proves the global and keeps the write `NONE`.
The model also owns the declared-name question: whether a declaration backs a plain write, the
refusal `MustDeclareVars` answers a plain write with when none does. A parameter, a claim the
function's body precedes the write with, or a `Global` declaration standing directly in the body
the write stands in before it backs the write; a claim a branch holds runs only when the branch
does, a declaration in another body runs when that body runs, and a `Dim` claim names a storage
the option does not recognize, so none of the three backs anything.
The model also holds the tree's constant names — a `Const` or `Enum` declaration or a `Const`
parameter claims one — because no scope placement softens that fact: writing a constant is an
interpreter refusal, so a caller that flattens a dynamic write over a name asks the model first.
It answers whether the tree holds an include the same way — the node, or the directive an
include line with trailing text becomes — because an unseen included file may declare storage
and constants no tree declaration covers, so every caller that rewrites around the tree's own
declarations asks the model first. It answers whether the parser recovered an error node
anywhere for the same reason: text no walk reads may hide a declaration the same way.
"""
from __future__ import annotations
import enum
import re
from dataclasses import dataclass, field
from typing import TypeGuard, cast
from refinery.lib.scripts import Node, Statement
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.model import (
Au3Directive,
Au3EnumDeclaration,
Au3ErrorNode,
Au3ForInStatement,
Au3ForStatement,
Au3FunctionDeclaration,
Au3Include,
Au3IncludeOnce,
Au3Parameter,
Au3ScopeKind,
Au3Script,
Au3Variable,
Au3VariableDeclaration,
)
class Binding(enum.Enum):
"""
The storage a write provably targets: the one global of that name, one function's local, or
nothing at all.
"""
GLOBAL = enum.auto()
LOCAL = enum.auto()
NONE = enum.auto()
#: An include spelled as a directive: the parser reads an include line only as the include node
#: when the line ends after the path, so one with trailing text — the comment form the official
#: documentation's own examples write — stays a directive that names the same unseen file.
INCLUDE_SPELLING = re.compile(r'\s*#\s*include', re.IGNORECASE)
@dataclass(frozen=True)
class Au3LocalFacts:
"""
What one function body proves about a name: whether it is a parameter, and a `ByRef` one,
whether any `Global` declaration in the function claims it and the body position where it
first takes one standing directly in the body, the names any local `Static` declaration in
it claims — a static's local survives the call that creates it — the body position where it
first takes a declaration that forces the local storage, and the body positions of every
statement holding such a declaration or claim at any depth.
"""
parameters: frozenset[str]
reference_parameters: frozenset[str]
global_names: frozenset[str]
first_global: dict[str, int]
static_names: frozenset[str]
first_local: dict[str, int]
local_positions: dict[str, frozenset[int]]
def _declared_names(declaration: Au3VariableDeclaration | Au3EnumDeclaration) -> set[str]:
names = set()
for declarator in declaration.declarations:
if declarator.variable is not None:
names.add(declarator.variable.name)
return names
def _declares_local(
statement: Statement,
) -> TypeGuard[Au3VariableDeclaration | Au3EnumDeclaration]:
"""
Whether *statement* declares its names into the local storage: `Local`, `Static`, a local
`Const`, or an `Enum` no `Global` keyword leads — an enum's entries are constants of the scope
the declaration stands in. A `Dim` forces nothing.
"""
if isinstance(statement, Au3VariableDeclaration):
if statement.scope is Au3ScopeKind.GLOBAL:
return False
return (
statement.scope is Au3ScopeKind.LOCAL
or statement.is_static
or statement.is_const
)
if isinstance(statement, Au3EnumDeclaration):
return statement.scope is not Au3ScopeKind.GLOBAL
return False
def _local_claim_names(node: Node) -> set[str] | None:
"""
The names *node* claims into the local storage — a local-forcing declaration's declared names
or a `For`/`For…In` loop's variable — or `None` when it claims none.
"""
if isinstance(node, (Au3VariableDeclaration, Au3EnumDeclaration)):
return _declared_names(node) if _declares_local(node) else None
if isinstance(node, (Au3ForStatement, Au3ForInStatement)):
return None if node.variable is None else {node.variable.name}
return None
def _first_global_claims(body: list[Statement]) -> dict[str, int]:
"""
The body position where each name first takes a `Global` claim standing directly in *body*: a
top-level claim runs on every path that reaches a later statement, because AutoIt has no
jumps, while one a branch holds runs only when the branch does.
"""
first: dict[str, int] = {}
for index, statement in enumerate(body):
if (
isinstance(statement, (Au3VariableDeclaration, Au3EnumDeclaration))
and statement.scope is Au3ScopeKind.GLOBAL
):
for name in _declared_names(statement):
first.setdefault(name, index)
return first
def _local_facts(function: Au3FunctionDeclaration) -> Au3LocalFacts:
"""
The local facts of *function*'s body: parameter names and the `ByRef` ones among them, the
names any `Global` or local `Static` declaration in it claims — at any depth, but not inside
a nested function, which is a scope of its own — the first body position where each name
takes a `Global` claim standing directly in the body, the first body position at which each
name takes a local-forcing declaration or becomes the variable of a `For`/`For…In` loop, and
the body positions of every statement holding such a declaration or claim at any depth.
"""
parameters = {
parameter.variable.name
for parameter in function.params
if parameter.variable is not None
}
reference_parameters = {
parameter.variable.name
for parameter in function.params
if parameter.variable is not None and parameter.by_ref
}
global_names = set()
static_names = set()
for node in ast.scope_nodes(function.body):
if (
isinstance(node, (Au3VariableDeclaration, Au3EnumDeclaration))
and node.scope is Au3ScopeKind.GLOBAL
):
global_names |= _declared_names(node)
elif isinstance(node, Au3VariableDeclaration) and node.is_static:
static_names |= _declared_names(node)
first_global = _first_global_claims(function.body)
first_local: dict[str, int] = {}
for index, statement in enumerate(function.body):
names = _local_claim_names(statement)
if names is None:
continue
for name in names:
first_local.setdefault(name, index)
local_positions: dict[str, set[int]] = {}
for node in ast.scope_nodes(function.body):
names = _local_claim_names(node)
if names is None:
continue
index = _top_level_index(function, node)
for name in names:
local_positions.setdefault(name, set()).add(index)
return Au3LocalFacts(
frozenset(parameters),
frozenset(reference_parameters),
frozenset(global_names),
first_global,
frozenset(static_names),
first_local,
{name: frozenset(indices) for name, indices in local_positions.items()},
)
def _top_level_index(scope: Au3FunctionDeclaration, write: Node) -> int:
"""
The position in *scope*'s body of the top-level statement that holds *write*, or `-1` when
the chain leaves the body: the write stands in no statement the body owns — a parameter
default, the function's own name — so no declaration before it is pinned.
"""
cursor = write
while cursor.parent is not None and cursor.parent is not scope:
cursor = cursor.parent
if cursor.parent is not scope or not isinstance(cursor, Statement):
return -1
return scope.body.index(cast(Statement, cursor))
def _script_level_index(write: Node) -> int:
"""
The position in the script body of the top-level statement that holds *write*, or `-1` when
the chain leaves the body — a write inside a function, which stands in a body of its own.
"""
cursor = write
while cursor.parent is not None and cursor.parent.parent is not None:
cursor = cursor.parent
if not isinstance(cursor, Statement) or not isinstance(cursor.parent, Au3Script):
return -1
return cursor.parent.body.index(cursor)
@dataclass(frozen=True)
class Au3ScopeModel:
"""
The script's declared functions, the answers to which storage a write or a read targets and
which storage an occurrence may reach, whether a write reaches a caller's storage, whether a
declaration backs a plain write, the names the tree declares constants, whether the tree
holds an include, and the error nodes the parser recovered. Built once per tree version by
`build_scope_model`.
"""
functions: dict[str, Au3FunctionDeclaration]
local_facts: dict[Au3FunctionDeclaration, Au3LocalFacts] = field(default_factory=dict)
script_first_global: dict[str, int] = field(default_factory=dict)
constant_names: frozenset[str] = frozenset()
holds_an_include: bool = False
error_nodes: tuple[Au3ErrorNode, ...] = ()
@property
def function_names(self) -> frozenset[str]:
return frozenset(self.functions)
@property
def holds_a_parse_error(self) -> bool:
"""
Whether the parser recovered an error node anywhere in the tree: live source text or a
repaired construct no walk reads, which may hold any declaration.
"""
return bool(self.error_nodes)
def binding(
self,
scope: Au3FunctionDeclaration | None,
write: Node,
name: str,
) -> Binding:
"""
The storage the write *write* performs on *name* targets when it stands in *scope*: the
whole script when `None`, one function's body otherwise. The write is the declaration,
assignment, loop, or ReDim statement that performs it —
`refinery.lib.scripts.au3.ast.owning_statement` names it from any node inside. A bare,
compound, `Dim`, or `ReDim` write whose name is a parameter of the function, whose name a
local-forcing declaration in the body precedes, or whose name a `For`/`For…In` loop in
the body claims at or before it, targets that local — unless a `Global` declaration of
the name anywhere in the function claims the global.
"""
if scope is None:
return Binding.GLOBAL
if isinstance(write, (Au3ForStatement, Au3ForInStatement)):
return Binding.LOCAL
if isinstance(write, (Au3VariableDeclaration, Au3EnumDeclaration)):
if write.scope is Au3ScopeKind.GLOBAL:
return Binding.GLOBAL
if write.scope is Au3ScopeKind.LOCAL:
return Binding.LOCAL
if isinstance(write, Au3VariableDeclaration) and write.is_static:
return Binding.LOCAL
if write.is_const:
return Binding.LOCAL
facts = self.local_facts.get(scope)
if facts is None or name in facts.global_names:
return Binding.NONE
if name in facts.parameters:
return Binding.LOCAL
first = facts.first_local.get(name)
if first is None:
return Binding.NONE
if first <= _top_level_index(scope, write):
return Binding.LOCAL
return Binding.NONE
def writes_caller_storage(self, scope: Au3FunctionDeclaration | None, name: str) -> bool:
"""
Whether a write of *name* in *scope* may reach storage the caller holds: the name is a
`ByRef` parameter of the function. `binding` answers such a write `Binding.LOCAL` — the
parameter is the function's own name for the variable — so a caller that removes a write
because no read in the function observes it asks this as well. A function the model holds
no facts for may write any caller's storage.
"""
if scope is None:
return False
facts = self.local_facts.get(scope)
return facts is None or name in facts.reference_parameters
def declaration_backs_write(
self,
scope: Au3FunctionDeclaration | None,
write: Node,
name: str,
) -> bool:
"""
Whether a declaration of *name* backs the plain write *write* standing in *scope*: a
parameter, or a claim the function's body precedes the write with — `binding` answers
both — or a `Global` declaration standing directly in the body the write stands in, the
function's own or the script's, before the statement that holds it. A claim a branch
holds runs only when the branch does, and a declaration in another body runs when that
body runs, which the caller cannot order, so neither backs a write. A `Dim` claim backs
nothing — the option names `Local` and `Global` alone — and a `Local` standing at script
level backs nothing either, the storage it creates there being unpinned.
"""
if scope is not None:
if self.binding(scope, write, name) is Binding.LOCAL:
return True
facts = self.local_facts.get(scope)
first = facts.first_global.get(name) if facts is not None else None
if first is None:
return False
return first <= _top_level_index(scope, write)
first = self.script_first_global.get(name)
if first is None:
return False
return first <= _script_level_index(write)
def read_binding(
self,
scope: Au3FunctionDeclaration | None,
read: Node,
name: str,
) -> Binding:
"""
The storage the read *read* of *name* targets when it stands in *scope*: the whole script
when `None`, one function's body otherwise. A read before a same-named local-forcing
declaration in the body sees the global and a read after it the local (probe4 22-23), and
a function that declares the name `Global` reads the global throughout. A name a `Dim`
in the function declares forces no storage, so a read of it keeps the global: the
function created no local the read could see. A parameter read inside the body targets
the local, but a read inside a parameter default runs before any parameter is bound, so
it keeps the global — the only storage that could exist where it stands.
"""
if scope is None:
return Binding.GLOBAL
facts = self.local_facts.get(scope)
if facts is None or name in facts.global_names:
return Binding.GLOBAL
if name in facts.parameters:
if _top_level_index(scope, read) < 0:
return Binding.GLOBAL
return Binding.LOCAL
first = facts.first_local.get(name)
if first is None:
return Binding.GLOBAL
if first <= _top_level_index(scope, read):
return Binding.LOCAL
return Binding.GLOBAL
def local_precedes_read(
self,
scope: Au3FunctionDeclaration | None,
read: Node,
name: str,
) -> bool:
"""
Whether the read *read* of *name* in *scope* targets a local that exists when the read
runs and whose claim strictly precedes it. `read_binding` counts the statement that
claims a local as covering a read inside it, but the local does not exist while that
statement's own initializer runs — `Local $x = $x` is an interpreter refusal (probe9g)
where the dynamic read of the same name answers `""` — so a caller that replaces a
dynamic read with the plain one asks this instead. A parameter needs no claim: it exists
once the function's parameters are bound, and the defaults that fill them run before any
is, so a read inside a parameter default — of the very parameter being defaulted no less
— targets nothing the plain read may name and counts only inside the body.
"""
if scope is None:
return False
facts = self.local_facts.get(scope)
if facts is None or name in facts.global_names:
return False
if name in facts.parameters:
return _top_level_index(scope, read) >= 0
first = facts.first_local.get(name)
if first is None:
return False
return first < _top_level_index(scope, read)
def read_storages(
self,
scope: Au3FunctionDeclaration | None,
occurrence: Au3Variable,
name: str,
) -> frozenset[Binding]:
"""
The storages the occurrence *occurrence* of *name* standing in *scope* may read: the
whole script when `None`, one function's body otherwise. A write occurrence resolves
through `binding` and a pure read through `read_binding`, each occurrence by exactly one
call — the two do not coincide everywhere, a `For` loop's variable being local to its own
write and global to a read inside a function declaring the name `Global`, so the
classifier never compares them. A resolved global reads the global alone, except where
the scope claims the name `Static`: a static's local survives the call that created it,
so a read the claim does not cover — one standing before it — may still reach the local
on a later call, and both storages stay protected. A resolved local reads the local alone
only where its claim strictly precedes the occurrence; where the claim shares the
occurrence's top-level statement, the local does not exist while that statement's own
initializer or loop header runs, so the read may reach the global as well and both
storages stay protected. A write occurrence that proves nothing reads the global or the
implicit local its own write creates.
"""
if ast.is_write_occurrence(occurrence):
statement = ast.owning_statement(occurrence)
assert statement is not None
resolved = self.binding(scope, statement, name)
else:
resolved = self.read_binding(scope, occurrence, name)
if resolved is Binding.GLOBAL:
facts = self.local_facts.get(scope) if scope is not None else None
if facts is not None and name in facts.static_names:
return frozenset({Binding.LOCAL, Binding.GLOBAL})
return frozenset({Binding.GLOBAL})
if resolved is Binding.LOCAL and self.local_precedes_read(scope, occurrence, name):
return frozenset({Binding.LOCAL})
return frozenset({Binding.LOCAL, Binding.GLOBAL})
def may_bind_local(
self,
scope: Au3FunctionDeclaration | None,
occurrence: Node,
name: str,
) -> bool:
"""
Whether the occurrence *occurrence* of *name* may bind a local of *scope*: the name is a
parameter of the function, or a local-forcing declaration or `For`/`For…In` claim of it
stands in a top-level statement at or before the occurrence's. A declaration inside a
branch creates its local the moment the branch runs it, and a loop's second iteration
sees one an earlier iteration created, so the occurrence and the claim sharing their
top-level statement counts as preceding it. A read that provably precedes every claim —
`read_binding` answering `Binding.GLOBAL` with no claim behind it — is one no local can
reach. An occurrence inside a parameter default may bind no local at all: no parameter
is bound and no body statement has run where it stands.
"""
if scope is None:
return False
facts = self.local_facts.get(scope)
if facts is None or name in facts.global_names:
return False
if name in facts.parameters:
return _top_level_index(scope, occurrence) >= 0
index = _top_level_index(scope, occurrence)
return any(position <= index for position in facts.local_positions.get(name, ()))
def _constant_names(root: Au3Script) -> frozenset[str]:
"""
Every name the tree declares a constant: the names of a `Const` declaration, of any `Enum` —
an enum's entries are constants of the scope it stands in — and of a `Const` parameter.
"""
names: set[str] = set()
for node in root.walk():
if isinstance(node, Au3VariableDeclaration):
if node.is_const:
names |= _declared_names(node)
elif isinstance(node, Au3EnumDeclaration):
names |= _declared_names(node)
elif isinstance(node, Au3Parameter):
if node.is_const and node.variable is not None:
names.add(node.variable.name)
return frozenset(names)
def build_scope_model(root: Au3Script) -> Au3ScopeModel:
"""
Collect the function declarations of *root*, the local facts of each body, the script body's
first `Global` claims, the names the tree declares constants, whether the tree holds an
include — the node the parser reads an include line without trailing text into, or the
directive one with trailing text becomes — and every error node the parser recovered, in
walk order. Two functions of one name never coexist in a script AutoIt runs, so the mapping
keeps the last of them without comment.
"""
functions: dict[str, Au3FunctionDeclaration] = {}
holds_an_include = False
error_nodes: list[Au3ErrorNode] = []
for node in root.walk():
if isinstance(node, Au3FunctionDeclaration):
if node.name is not None:
functions[node.name.name] = node
elif isinstance(node, (Au3Include, Au3IncludeOnce)):
holds_an_include = True
elif isinstance(node, Au3Directive) and INCLUDE_SPELLING.match(node.text):
holds_an_include = True
elif isinstance(node, Au3ErrorNode):
error_nodes.append(node)
local_facts = {function: _local_facts(function) for function in functions.values()}
return Au3ScopeModel(
functions,
local_facts,
_first_global_claims(root.body),
_constant_names(root),
holds_an_include,
tuple(error_nodes),
)
Global variables
var INCLUDE_SPELLING-
An include spelled as a directive: the parser reads an include line only as the include node when the line ends after the path, so one with trailing text — the comment form the official documentation's own examples write — stays a directive that names the same unseen file.
Functions
def build_scope_model(root)-
Collect the function declarations of root, the local facts of each body, the script body's first
Globalclaims, the names the tree declares constants, whether the tree holds an include — the node the parser reads an include line without trailing text into, or the directive one with trailing text becomes — and every error node the parser recovered, in walk order. Two functions of one name never coexist in a script AutoIt runs, so the mapping keeps the last of them without comment.Expand source code Browse git
def build_scope_model(root: Au3Script) -> Au3ScopeModel: """ Collect the function declarations of *root*, the local facts of each body, the script body's first `Global` claims, the names the tree declares constants, whether the tree holds an include — the node the parser reads an include line without trailing text into, or the directive one with trailing text becomes — and every error node the parser recovered, in walk order. Two functions of one name never coexist in a script AutoIt runs, so the mapping keeps the last of them without comment. """ functions: dict[str, Au3FunctionDeclaration] = {} holds_an_include = False error_nodes: list[Au3ErrorNode] = [] for node in root.walk(): if isinstance(node, Au3FunctionDeclaration): if node.name is not None: functions[node.name.name] = node elif isinstance(node, (Au3Include, Au3IncludeOnce)): holds_an_include = True elif isinstance(node, Au3Directive) and INCLUDE_SPELLING.match(node.text): holds_an_include = True elif isinstance(node, Au3ErrorNode): error_nodes.append(node) local_facts = {function: _local_facts(function) for function in functions.values()} return Au3ScopeModel( functions, local_facts, _first_global_claims(root.body), _constant_names(root), holds_an_include, tuple(error_nodes), )
Classes
class Binding (*args, **kwds)-
The storage a write provably targets: the one global of that name, one function's local, or nothing at all.
Expand source code Browse git
class Binding(enum.Enum): """ The storage a write provably targets: the one global of that name, one function's local, or nothing at all. """ GLOBAL = enum.auto() LOCAL = enum.auto() NONE = enum.auto()Ancestors
- enum.Enum
Class variables
var GLOBAL-
The type of the None singleton.
var LOCAL-
The type of the None singleton.
var NONE-
The type of the None singleton.
class Au3LocalFacts (parameters, reference_parameters, global_names, first_global, static_names, first_local, local_positions)-
What one function body proves about a name: whether it is a parameter, and a
ByRefone, whether anyGlobaldeclaration in the function claims it and the body position where it first takes one standing directly in the body, the names any localStaticdeclaration in it claims — a static's local survives the call that creates it — the body position where it first takes a declaration that forces the local storage, and the body positions of every statement holding such a declaration or claim at any depth.Expand source code Browse git
@dataclass(frozen=True) class Au3LocalFacts: """ What one function body proves about a name: whether it is a parameter, and a `ByRef` one, whether any `Global` declaration in the function claims it and the body position where it first takes one standing directly in the body, the names any local `Static` declaration in it claims — a static's local survives the call that creates it — the body position where it first takes a declaration that forces the local storage, and the body positions of every statement holding such a declaration or claim at any depth. """ parameters: frozenset[str] reference_parameters: frozenset[str] global_names: frozenset[str] first_global: dict[str, int] static_names: frozenset[str] first_local: dict[str, int] local_positions: dict[str, frozenset[int]]Instance variables
var parameters-
The type of the None singleton.
var reference_parameters-
The type of the None singleton.
var global_names-
The type of the None singleton.
var first_global-
The type of the None singleton.
var static_names-
The type of the None singleton.
var first_local-
The type of the None singleton.
var local_positions-
The type of the None singleton.
class Au3ScopeModel (functions, local_facts=<factory>, script_first_global=<factory>, constant_names=frozenset(), holds_an_include=False, error_nodes=())-
The script's declared functions, the answers to which storage a write or a read targets and which storage an occurrence may reach, whether a write reaches a caller's storage, whether a declaration backs a plain write, the names the tree declares constants, whether the tree holds an include, and the error nodes the parser recovered. Built once per tree version by
build_scope_model().Expand source code Browse git
@dataclass(frozen=True) class Au3ScopeModel: """ The script's declared functions, the answers to which storage a write or a read targets and which storage an occurrence may reach, whether a write reaches a caller's storage, whether a declaration backs a plain write, the names the tree declares constants, whether the tree holds an include, and the error nodes the parser recovered. Built once per tree version by `build_scope_model`. """ functions: dict[str, Au3FunctionDeclaration] local_facts: dict[Au3FunctionDeclaration, Au3LocalFacts] = field(default_factory=dict) script_first_global: dict[str, int] = field(default_factory=dict) constant_names: frozenset[str] = frozenset() holds_an_include: bool = False error_nodes: tuple[Au3ErrorNode, ...] = () @property def function_names(self) -> frozenset[str]: return frozenset(self.functions) @property def holds_a_parse_error(self) -> bool: """ Whether the parser recovered an error node anywhere in the tree: live source text or a repaired construct no walk reads, which may hold any declaration. """ return bool(self.error_nodes) def binding( self, scope: Au3FunctionDeclaration | None, write: Node, name: str, ) -> Binding: """ The storage the write *write* performs on *name* targets when it stands in *scope*: the whole script when `None`, one function's body otherwise. The write is the declaration, assignment, loop, or ReDim statement that performs it — `refinery.lib.scripts.au3.ast.owning_statement` names it from any node inside. A bare, compound, `Dim`, or `ReDim` write whose name is a parameter of the function, whose name a local-forcing declaration in the body precedes, or whose name a `For`/`For…In` loop in the body claims at or before it, targets that local — unless a `Global` declaration of the name anywhere in the function claims the global. """ if scope is None: return Binding.GLOBAL if isinstance(write, (Au3ForStatement, Au3ForInStatement)): return Binding.LOCAL if isinstance(write, (Au3VariableDeclaration, Au3EnumDeclaration)): if write.scope is Au3ScopeKind.GLOBAL: return Binding.GLOBAL if write.scope is Au3ScopeKind.LOCAL: return Binding.LOCAL if isinstance(write, Au3VariableDeclaration) and write.is_static: return Binding.LOCAL if write.is_const: return Binding.LOCAL facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return Binding.NONE if name in facts.parameters: return Binding.LOCAL first = facts.first_local.get(name) if first is None: return Binding.NONE if first <= _top_level_index(scope, write): return Binding.LOCAL return Binding.NONE def writes_caller_storage(self, scope: Au3FunctionDeclaration | None, name: str) -> bool: """ Whether a write of *name* in *scope* may reach storage the caller holds: the name is a `ByRef` parameter of the function. `binding` answers such a write `Binding.LOCAL` — the parameter is the function's own name for the variable — so a caller that removes a write because no read in the function observes it asks this as well. A function the model holds no facts for may write any caller's storage. """ if scope is None: return False facts = self.local_facts.get(scope) return facts is None or name in facts.reference_parameters def declaration_backs_write( self, scope: Au3FunctionDeclaration | None, write: Node, name: str, ) -> bool: """ Whether a declaration of *name* backs the plain write *write* standing in *scope*: a parameter, or a claim the function's body precedes the write with — `binding` answers both — or a `Global` declaration standing directly in the body the write stands in, the function's own or the script's, before the statement that holds it. A claim a branch holds runs only when the branch does, and a declaration in another body runs when that body runs, which the caller cannot order, so neither backs a write. A `Dim` claim backs nothing — the option names `Local` and `Global` alone — and a `Local` standing at script level backs nothing either, the storage it creates there being unpinned. """ if scope is not None: if self.binding(scope, write, name) is Binding.LOCAL: return True facts = self.local_facts.get(scope) first = facts.first_global.get(name) if facts is not None else None if first is None: return False return first <= _top_level_index(scope, write) first = self.script_first_global.get(name) if first is None: return False return first <= _script_level_index(write) def read_binding( self, scope: Au3FunctionDeclaration | None, read: Node, name: str, ) -> Binding: """ The storage the read *read* of *name* targets when it stands in *scope*: the whole script when `None`, one function's body otherwise. A read before a same-named local-forcing declaration in the body sees the global and a read after it the local (probe4 22-23), and a function that declares the name `Global` reads the global throughout. A name a `Dim` in the function declares forces no storage, so a read of it keeps the global: the function created no local the read could see. A parameter read inside the body targets the local, but a read inside a parameter default runs before any parameter is bound, so it keeps the global — the only storage that could exist where it stands. """ if scope is None: return Binding.GLOBAL facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return Binding.GLOBAL if name in facts.parameters: if _top_level_index(scope, read) < 0: return Binding.GLOBAL return Binding.LOCAL first = facts.first_local.get(name) if first is None: return Binding.GLOBAL if first <= _top_level_index(scope, read): return Binding.LOCAL return Binding.GLOBAL def local_precedes_read( self, scope: Au3FunctionDeclaration | None, read: Node, name: str, ) -> bool: """ Whether the read *read* of *name* in *scope* targets a local that exists when the read runs and whose claim strictly precedes it. `read_binding` counts the statement that claims a local as covering a read inside it, but the local does not exist while that statement's own initializer runs — `Local $x = $x` is an interpreter refusal (probe9g) where the dynamic read of the same name answers `""` — so a caller that replaces a dynamic read with the plain one asks this instead. A parameter needs no claim: it exists once the function's parameters are bound, and the defaults that fill them run before any is, so a read inside a parameter default — of the very parameter being defaulted no less — targets nothing the plain read may name and counts only inside the body. """ if scope is None: return False facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return False if name in facts.parameters: return _top_level_index(scope, read) >= 0 first = facts.first_local.get(name) if first is None: return False return first < _top_level_index(scope, read) def read_storages( self, scope: Au3FunctionDeclaration | None, occurrence: Au3Variable, name: str, ) -> frozenset[Binding]: """ The storages the occurrence *occurrence* of *name* standing in *scope* may read: the whole script when `None`, one function's body otherwise. A write occurrence resolves through `binding` and a pure read through `read_binding`, each occurrence by exactly one call — the two do not coincide everywhere, a `For` loop's variable being local to its own write and global to a read inside a function declaring the name `Global`, so the classifier never compares them. A resolved global reads the global alone, except where the scope claims the name `Static`: a static's local survives the call that created it, so a read the claim does not cover — one standing before it — may still reach the local on a later call, and both storages stay protected. A resolved local reads the local alone only where its claim strictly precedes the occurrence; where the claim shares the occurrence's top-level statement, the local does not exist while that statement's own initializer or loop header runs, so the read may reach the global as well and both storages stay protected. A write occurrence that proves nothing reads the global or the implicit local its own write creates. """ if ast.is_write_occurrence(occurrence): statement = ast.owning_statement(occurrence) assert statement is not None resolved = self.binding(scope, statement, name) else: resolved = self.read_binding(scope, occurrence, name) if resolved is Binding.GLOBAL: facts = self.local_facts.get(scope) if scope is not None else None if facts is not None and name in facts.static_names: return frozenset({Binding.LOCAL, Binding.GLOBAL}) return frozenset({Binding.GLOBAL}) if resolved is Binding.LOCAL and self.local_precedes_read(scope, occurrence, name): return frozenset({Binding.LOCAL}) return frozenset({Binding.LOCAL, Binding.GLOBAL}) def may_bind_local( self, scope: Au3FunctionDeclaration | None, occurrence: Node, name: str, ) -> bool: """ Whether the occurrence *occurrence* of *name* may bind a local of *scope*: the name is a parameter of the function, or a local-forcing declaration or `For`/`For…In` claim of it stands in a top-level statement at or before the occurrence's. A declaration inside a branch creates its local the moment the branch runs it, and a loop's second iteration sees one an earlier iteration created, so the occurrence and the claim sharing their top-level statement counts as preceding it. A read that provably precedes every claim — `read_binding` answering `Binding.GLOBAL` with no claim behind it — is one no local can reach. An occurrence inside a parameter default may bind no local at all: no parameter is bound and no body statement has run where it stands. """ if scope is None: return False facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return False if name in facts.parameters: return _top_level_index(scope, occurrence) >= 0 index = _top_level_index(scope, occurrence) return any(position <= index for position in facts.local_positions.get(name, ()))Instance variables
var functions-
The type of the None singleton.
var local_facts-
The type of the None singleton.
var script_first_global-
The type of the None singleton.
var constant_names-
The type of the None singleton.
var holds_an_include-
The type of the None singleton.
var error_nodes-
The type of the None singleton.
var function_names-
Expand source code Browse git
@property def function_names(self) -> frozenset[str]: return frozenset(self.functions) var holds_a_parse_error-
Whether the parser recovered an error node anywhere in the tree: live source text or a repaired construct no walk reads, which may hold any declaration.
Expand source code Browse git
@property def holds_a_parse_error(self) -> bool: """ Whether the parser recovered an error node anywhere in the tree: live source text or a repaired construct no walk reads, which may hold any declaration. """ return bool(self.error_nodes)
Methods
def binding(self, scope, write, name)-
The storage the write write performs on name targets when it stands in scope: the whole script when
None, one function's body otherwise. The write is the declaration, assignment, loop, or ReDim statement that performs it —owning_statement()names it from any node inside. A bare, compound,Dim, orReDimwrite whose name is a parameter of the function, whose name a local-forcing declaration in the body precedes, or whose name aFor/For…Inloop in the body claims at or before it, targets that local — unless aGlobaldeclaration of the name anywhere in the function claims the global.Expand source code Browse git
def binding( self, scope: Au3FunctionDeclaration | None, write: Node, name: str, ) -> Binding: """ The storage the write *write* performs on *name* targets when it stands in *scope*: the whole script when `None`, one function's body otherwise. The write is the declaration, assignment, loop, or ReDim statement that performs it — `refinery.lib.scripts.au3.ast.owning_statement` names it from any node inside. A bare, compound, `Dim`, or `ReDim` write whose name is a parameter of the function, whose name a local-forcing declaration in the body precedes, or whose name a `For`/`For…In` loop in the body claims at or before it, targets that local — unless a `Global` declaration of the name anywhere in the function claims the global. """ if scope is None: return Binding.GLOBAL if isinstance(write, (Au3ForStatement, Au3ForInStatement)): return Binding.LOCAL if isinstance(write, (Au3VariableDeclaration, Au3EnumDeclaration)): if write.scope is Au3ScopeKind.GLOBAL: return Binding.GLOBAL if write.scope is Au3ScopeKind.LOCAL: return Binding.LOCAL if isinstance(write, Au3VariableDeclaration) and write.is_static: return Binding.LOCAL if write.is_const: return Binding.LOCAL facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return Binding.NONE if name in facts.parameters: return Binding.LOCAL first = facts.first_local.get(name) if first is None: return Binding.NONE if first <= _top_level_index(scope, write): return Binding.LOCAL return Binding.NONE def writes_caller_storage(self, scope, name)-
Whether a write of name in scope may reach storage the caller holds: the name is a
ByRefparameter of the function.bindinganswers such a writeBinding.LOCAL— the parameter is the function's own name for the variable — so a caller that removes a write because no read in the function observes it asks this as well. A function the model holds no facts for may write any caller's storage.Expand source code Browse git
def writes_caller_storage(self, scope: Au3FunctionDeclaration | None, name: str) -> bool: """ Whether a write of *name* in *scope* may reach storage the caller holds: the name is a `ByRef` parameter of the function. `binding` answers such a write `Binding.LOCAL` — the parameter is the function's own name for the variable — so a caller that removes a write because no read in the function observes it asks this as well. A function the model holds no facts for may write any caller's storage. """ if scope is None: return False facts = self.local_facts.get(scope) return facts is None or name in facts.reference_parameters def declaration_backs_write(self, scope, write, name)-
Whether a declaration of name backs the plain write write standing in scope: a parameter, or a claim the function's body precedes the write with —
bindinganswers both — or aGlobaldeclaration standing directly in the body the write stands in, the function's own or the script's, before the statement that holds it. A claim a branch holds runs only when the branch does, and a declaration in another body runs when that body runs, which the caller cannot order, so neither backs a write. ADimclaim backs nothing — the option namesLocalandGlobalalone — and aLocalstanding at script level backs nothing either, the storage it creates there being unpinned.Expand source code Browse git
def declaration_backs_write( self, scope: Au3FunctionDeclaration | None, write: Node, name: str, ) -> bool: """ Whether a declaration of *name* backs the plain write *write* standing in *scope*: a parameter, or a claim the function's body precedes the write with — `binding` answers both — or a `Global` declaration standing directly in the body the write stands in, the function's own or the script's, before the statement that holds it. A claim a branch holds runs only when the branch does, and a declaration in another body runs when that body runs, which the caller cannot order, so neither backs a write. A `Dim` claim backs nothing — the option names `Local` and `Global` alone — and a `Local` standing at script level backs nothing either, the storage it creates there being unpinned. """ if scope is not None: if self.binding(scope, write, name) is Binding.LOCAL: return True facts = self.local_facts.get(scope) first = facts.first_global.get(name) if facts is not None else None if first is None: return False return first <= _top_level_index(scope, write) first = self.script_first_global.get(name) if first is None: return False return first <= _script_level_index(write) def read_binding(self, scope, read, name)-
The storage the read read of name targets when it stands in scope: the whole script when
None, one function's body otherwise. A read before a same-named local-forcing declaration in the body sees the global and a read after it the local (probe4 22-23), and a function that declares the nameGlobalreads the global throughout. A name aDimin the function declares forces no storage, so a read of it keeps the global: the function created no local the read could see. A parameter read inside the body targets the local, but a read inside a parameter default runs before any parameter is bound, so it keeps the global — the only storage that could exist where it stands.Expand source code Browse git
def read_binding( self, scope: Au3FunctionDeclaration | None, read: Node, name: str, ) -> Binding: """ The storage the read *read* of *name* targets when it stands in *scope*: the whole script when `None`, one function's body otherwise. A read before a same-named local-forcing declaration in the body sees the global and a read after it the local (probe4 22-23), and a function that declares the name `Global` reads the global throughout. A name a `Dim` in the function declares forces no storage, so a read of it keeps the global: the function created no local the read could see. A parameter read inside the body targets the local, but a read inside a parameter default runs before any parameter is bound, so it keeps the global — the only storage that could exist where it stands. """ if scope is None: return Binding.GLOBAL facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return Binding.GLOBAL if name in facts.parameters: if _top_level_index(scope, read) < 0: return Binding.GLOBAL return Binding.LOCAL first = facts.first_local.get(name) if first is None: return Binding.GLOBAL if first <= _top_level_index(scope, read): return Binding.LOCAL return Binding.GLOBAL def local_precedes_read(self, scope, read, name)-
Whether the read read of name in scope targets a local that exists when the read runs and whose claim strictly precedes it.
read_bindingcounts the statement that claims a local as covering a read inside it, but the local does not exist while that statement's own initializer runs —Local $x = $xis an interpreter refusal (probe9g) where the dynamic read of the same name answers""— so a caller that replaces a dynamic read with the plain one asks this instead. A parameter needs no claim: it exists once the function's parameters are bound, and the defaults that fill them run before any is, so a read inside a parameter default — of the very parameter being defaulted no less — targets nothing the plain read may name and counts only inside the body.Expand source code Browse git
def local_precedes_read( self, scope: Au3FunctionDeclaration | None, read: Node, name: str, ) -> bool: """ Whether the read *read* of *name* in *scope* targets a local that exists when the read runs and whose claim strictly precedes it. `read_binding` counts the statement that claims a local as covering a read inside it, but the local does not exist while that statement's own initializer runs — `Local $x = $x` is an interpreter refusal (probe9g) where the dynamic read of the same name answers `""` — so a caller that replaces a dynamic read with the plain one asks this instead. A parameter needs no claim: it exists once the function's parameters are bound, and the defaults that fill them run before any is, so a read inside a parameter default — of the very parameter being defaulted no less — targets nothing the plain read may name and counts only inside the body. """ if scope is None: return False facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return False if name in facts.parameters: return _top_level_index(scope, read) >= 0 first = facts.first_local.get(name) if first is None: return False return first < _top_level_index(scope, read) def read_storages(self, scope, occurrence, name)-
The storages the occurrence occurrence of name standing in scope may read: the whole script when
None, one function's body otherwise. A write occurrence resolves throughbindingand a pure read throughread_binding, each occurrence by exactly one call — the two do not coincide everywhere, aForloop's variable being local to its own write and global to a read inside a function declaring the nameGlobal, so the classifier never compares them. A resolved global reads the global alone, except where the scope claims the nameStatic: a static's local survives the call that created it, so a read the claim does not cover — one standing before it — may still reach the local on a later call, and both storages stay protected. A resolved local reads the local alone only where its claim strictly precedes the occurrence; where the claim shares the occurrence's top-level statement, the local does not exist while that statement's own initializer or loop header runs, so the read may reach the global as well and both storages stay protected. A write occurrence that proves nothing reads the global or the implicit local its own write creates.Expand source code Browse git
def read_storages( self, scope: Au3FunctionDeclaration | None, occurrence: Au3Variable, name: str, ) -> frozenset[Binding]: """ The storages the occurrence *occurrence* of *name* standing in *scope* may read: the whole script when `None`, one function's body otherwise. A write occurrence resolves through `binding` and a pure read through `read_binding`, each occurrence by exactly one call — the two do not coincide everywhere, a `For` loop's variable being local to its own write and global to a read inside a function declaring the name `Global`, so the classifier never compares them. A resolved global reads the global alone, except where the scope claims the name `Static`: a static's local survives the call that created it, so a read the claim does not cover — one standing before it — may still reach the local on a later call, and both storages stay protected. A resolved local reads the local alone only where its claim strictly precedes the occurrence; where the claim shares the occurrence's top-level statement, the local does not exist while that statement's own initializer or loop header runs, so the read may reach the global as well and both storages stay protected. A write occurrence that proves nothing reads the global or the implicit local its own write creates. """ if ast.is_write_occurrence(occurrence): statement = ast.owning_statement(occurrence) assert statement is not None resolved = self.binding(scope, statement, name) else: resolved = self.read_binding(scope, occurrence, name) if resolved is Binding.GLOBAL: facts = self.local_facts.get(scope) if scope is not None else None if facts is not None and name in facts.static_names: return frozenset({Binding.LOCAL, Binding.GLOBAL}) return frozenset({Binding.GLOBAL}) if resolved is Binding.LOCAL and self.local_precedes_read(scope, occurrence, name): return frozenset({Binding.LOCAL}) return frozenset({Binding.LOCAL, Binding.GLOBAL}) def may_bind_local(self, scope, occurrence, name)-
Whether the occurrence occurrence of name may bind a local of scope: the name is a parameter of the function, or a local-forcing declaration or
For/For…Inclaim of it stands in a top-level statement at or before the occurrence's. A declaration inside a branch creates its local the moment the branch runs it, and a loop's second iteration sees one an earlier iteration created, so the occurrence and the claim sharing their top-level statement counts as preceding it. A read that provably precedes every claim —read_bindingansweringBinding.GLOBALwith no claim behind it — is one no local can reach. An occurrence inside a parameter default may bind no local at all: no parameter is bound and no body statement has run where it stands.Expand source code Browse git
def may_bind_local( self, scope: Au3FunctionDeclaration | None, occurrence: Node, name: str, ) -> bool: """ Whether the occurrence *occurrence* of *name* may bind a local of *scope*: the name is a parameter of the function, or a local-forcing declaration or `For`/`For…In` claim of it stands in a top-level statement at or before the occurrence's. A declaration inside a branch creates its local the moment the branch runs it, and a loop's second iteration sees one an earlier iteration created, so the occurrence and the claim sharing their top-level statement counts as preceding it. A read that provably precedes every claim — `read_binding` answering `Binding.GLOBAL` with no claim behind it — is one no local can reach. An occurrence inside a parameter default may bind no local at all: no parameter is bound and no body statement has run where it stands. """ if scope is None: return False facts = self.local_facts.get(scope) if facts is None or name in facts.global_names: return False if name in facts.parameters: return _top_level_index(scope, occurrence) >= 0 index = _top_level_index(scope, occurrence) return any(position <= index for position in facts.local_positions.get(name, ()))