Module refinery.lib.scripts.ps1.data
The .NET type and PowerShell command database that the PowerShell analysis and deobfuscation
subsystems share: type accelerators and members, command names, aliases and parameters, WMI classes,
and the small lookup tables built from them. The data is collected by run-pwsh.ps1 on genuine
Windows PowerShell 5.1 and shipped as the compressed pwsh-*.json.xz resources.
Every question about a type is asked of the query API at the end — resolve_type() and the
functions built on it. resolve_type() is the only thing that mints a canonical Ps1TypeName, and
every table below is keyed by one, which is what makes two spellings of a type comparable at all:
char[], System.Char[] and [Char[]] are one name, and none of them is System.Char.
The remaining * tables (KNOWN_CMDLETS, CANONICAL_TYPE_NAMES, …) are about commands and
display, not about type identity. The lowercase-string type views that used to sit beside them —
TYPE_MEMBERS, PROPERTY_TYPES, MEMBER_LOOKUP, TYPE_ALIASES — are gone: they were a second
vocabulary in which an array suffix could not be written, so every member question about char[]
was answered off Char.
Expand source code Browse git
"""
The .NET type and PowerShell command database that the PowerShell analysis and deobfuscation
subsystems share: type accelerators and members, command names, aliases and parameters, WMI classes,
and the small lookup tables built from them. The data is collected by run-pwsh.ps1 on genuine
Windows PowerShell 5.1 and shipped as the compressed `pwsh-*.json.xz` resources.
Every question about a *type* is asked of the query API at the end — `resolve_type` and the
functions built on it. `resolve_type` is the only thing that mints a canonical `Ps1TypeName`, and
every table below is keyed by one, which is what makes two spellings of a type comparable at all:
`char[]`, `System.Char[]` and `[Char[]]` are one name, and none of them is `System.Char`.
The remaining `*` tables (`KNOWN_CMDLETS`, `CANONICAL_TYPE_NAMES`, ...) are about *commands* and
*display*, not about type identity. The lowercase-string type views that used to sit beside them —
`TYPE_MEMBERS`, `PROPERTY_TYPES`, `MEMBER_LOOKUP`, `TYPE_ALIASES` — are gone: they were a second
vocabulary in which an array suffix could not be written, so every member question about `char[]`
was answered off `Char`.
"""
from __future__ import annotations
import enum
import functools
import lzma
import operator
import re
import typing
from refinery.lib.json import loads
from refinery.lib.resources import datapath
from refinery.lib.scripts.ps1.dotnet import Ps1TypeName, parse_type_name
SCHEMA_VERSION = 1
def _load(name: str) -> dict:
"""
One captured table, as it ships: LZMA over compact JSON.
LZMA rather than gzip because these tables are long stretches of repeated key names and type
names, which is what a large window pays off on — it halves them, 657KB to 331KB, where gzip and
a binary encoding both do worse. Decompressing costs 16ms more across all six and reading them
through `refinery.lib.json` gives back three times that, because the backend it prefers parses
faster than the standard library. Where that backend is absent the fallback is the standard
library, and loading is then a few percent slower than it was — a cost, not a failure.
"""
with datapath(name).open('rb') as fp:
return loads(lzma.decompress(fp.read()))
_META = _load('pwsh-meta.json.xz')
_schema = _META['schema']['version']
if _schema != SCHEMA_VERSION:
raise ValueError(
F'pwsh metadata schema version {_schema} is not the expected {SCHEMA_VERSION}; '
F'the reader and the collected data are out of step.'
)
_TYPES = _load('pwsh-types.json.xz')
_COMMANDS = _load('pwsh-commands.json.xz')
_VARIABLES = _load('pwsh-variables.json.xz')
_WMI = _load('pwsh-wmi.json.xz')
#: Type names PowerShell's *parser* understands that are not registered type accelerators, so a
#: collection run on a real host does not report them however authoritative it is — `[ordered]` is
#: recognized in a cast position and denotes an `OrderedDictionary`, but it is absent from the 94
#: accelerators our capture reports because it never was one. Kept beside the collected table rather
#: than injected into it: the data describes a host's .NET surface and this describes the language,
#: and blurring the two would cost the collected table the provenance that makes it trustworthy.
_PARSER_TYPE_KEYWORDS: dict[str, str] = {
'ordered': 'System.Collections.Specialized.OrderedDictionary',
}
_ACCELERATORS: dict[str, str] = {
**_PARSER_TYPE_KEYWORDS,
**{_alias.lower(): _full for _alias, _full in _TYPES['accelerators'].items()},
}
_TYPE_TABLE: dict[str, dict] = _TYPES['types']
#: Commands the capture reports that the host does not have. `Format-Hex` is described here exactly
#: as PowerShell 7 describes it — a `Raw` switch and a `System.String` `Encoding` — and the
#: `Microsoft.PowerShell.Utility` a 5.1 host loads reports version 7.0.0.0, so the collector read it
#: from a shadowing 7.0 module rather than from the host it declares itself authoritative for.
#: Measured three ways on 5.1: `Get-Command Format-Hex` reports CommandNotFoundException, `'ab' |
#: Format-Hex` throws, and the loaded Utility module exports only `Format-Custom`, `Format-List`,
#: `Format-Table` and `Format-Wide`. A probe of thirteen commands only PowerShell 6 or 7 ships found
#: this to be the one that leaked.
#:
#: A record for a command the host cannot run is not inert: the wildcard resolver draws its
#: candidate universe from `KNOWN_CMDLETS`, so `Get-Command Format-H*` had a unique match and was
#: rewritten into a call to a command 5.1 reports as not found — the same defect the `fhx` alias was
#: removed for. Withheld from the derived table rather than deleted from the capture, which stays as
#: collected the way `_PARSER_TYPE_KEYWORDS` above is kept beside it rather than injected into it.
_MISCOLLECTED_COMMANDS = frozenset({'format-hex'})
#: The bare nouns that name a program Windows itself ships, so that 5.1 runs the program rather than
#: retrying the name with a `Get-` prefix — the Application tier, which sits above that retry and
#: which no capture of the session's tables can describe. `tpm` opens `C:\Windows\system32\tpm.msc`,
#: and `Get-Tpm` is what we answered for it.
#:
#: Derived by intersecting every PATHEXT-matching file in `System32` and `Windows` with the 523 bare
#: nouns `refinery.lib.scripts.ps1.ast.implicit_get_retry` rewrites; `tpm` is the whole intersection.
#: A program the analyst installed cannot be here and is the declared residual: `date` resolves to
#: Git's `date.exe` on the development box and to `Get-Date` on one without it, and nothing readable
#: from a script says which. This is a floor under that residual rather than a fix for it.
PROGRAM_NAMES = frozenset({'tpm'})
_COMMAND_TABLE: dict[str, dict] = {
_name: _record for _name, _record in _COMMANDS['commands'].items()
if _name.lower() not in _MISCOLLECTED_COMMANDS
}
#: Member kinds that reflection reports and that the historical views expose. Fields and every
#: Extended Type System member (`ets_*`) are collected but withheld from these views, because the
#: prior database never saw them and the transforms that read the views were written against a
#: reflection-only member set. The query API exposes them for the migration that consumes them.
_VIEW_MEMBER_KINDS = frozenset({'method', 'property'})
def _view_members(record: dict) -> dict[str, dict]:
return {
name: member
for name, member in record['members'].items()
if member['source'] in ('reflection', 'wmi') and member['kind'] in _VIEW_MEMBER_KINDS
}
VARIABLE_TYPES: dict[str, str] = {
_name.lower(): _info['type'].lower()
for _name, _info in _VARIABLES['variables'].items()
if _info['type'] is not None
}
#: `$PSCmdlet` exists only inside an advanced function's scope, so the pristine `Get-Variable` the
#: generator runs never sees it. It is supplied here, as the previous database did, because it
#: cannot be collected rather than because it is absent.
VARIABLE_TYPES.setdefault('pscmdlet', 'system.management.automation.psscriptcmdlet')
#: The set of type-accelerator spellings, lowercased. An accelerator is already the shortest
#: readable name for its type, so display normalization leaves it as written rather than expanding
#: it to the verbose full name: `[ref]` and `[int]` stay, where `[System.Int32]` folds to `[Int32]`.
TYPE_ACCELERATORS: frozenset[str] = frozenset(_alias.lower() for _alias in _ACCELERATORS)
CANONICAL_TYPE_NAMES: dict[str, str] = {}
for _alias, _full in _ACCELERATORS.items():
_display = _full.removeprefix('System.')
CANONICAL_TYPE_NAMES[_alias.lower()] = _display
CANONICAL_TYPE_NAMES[_full.lower()] = _display
for _full in _TYPE_TABLE:
_display = _full.removeprefix('System.')
CANONICAL_TYPE_NAMES.setdefault(_full.lower(), _display)
CANONICAL_TYPE_NAMES.setdefault(_full.lower().removeprefix('system.'), _display)
WMI_CLASS_NAMES: dict[str, str] = {}
for _classes in _WMI['namespaces'].values():
for _cls in _classes:
WMI_CLASS_NAMES.setdefault(_cls.lower(), _cls)
CANONICAL_TYPE_NAMES.setdefault(_cls.lower(), _cls)
CANONICAL_TYPE_NAMES.setdefault(
'management.automation.sessionstateinternal',
'Management.Automation.SessionStateInternal',
)
def is_type(name: str, target: str) -> bool:
"""
Whether a type name as written in PowerShell source names the same type as `target`. Both sides
go through `resolve_type`, so no difference of spelling can make two names for one type answer
`False`: an accelerator, an omitted `System.` prefix, a difference of case, whitespace inside the
name, an assembly qualification and a generic argument list are all understood.
This used to compare a lowercased name against a lowercased full name, which meant it saw only
the three spellings that transformation happens to produce and answered `False` for the rest —
`[Byte[]]` among them, because an array suffix was not part of a name at all.
"""
resolved = resolve_type(name)
return resolved is not None and resolved == resolve_type(target)
#: The aliases the host binds, and nothing else. An entry here is not a harmless surplus: nothing in
#: ordinary name lookup beats an alias, so a name added here is taken away from whatever 5.1 would
#: have given it. `childitem`, `item`, `member` and `variable` were listed here once and are not
#: aliases at all — they are the engine's implicit `Get-` retry, which is a *last resort* reached
#: only once the alias, function and cmdlet tables have missed, so `function item { }` beats it. As
#: aliases they claimed the name before the function tier, and a call to the script's own function
#: was rewritten into a call to the cmdlet, deleting the body that ran. That rule lives in
#: `refinery.lib.scripts.ps1.ast.implicit_get_retry` instead, where it is a retry.
#:
#: `fhx` and `gerr` were listed too, and neither target is on the host: `Format-Hex` and `Get-Error`
#: were both measured absent from 5.1, so a bare `gerr` was rewritten into a name the script then
#: could not run, and the entry injected `Get-Error` as a cmdlet through the loop below on top of
#: that. `Format-Hex` had leaked into the collected command table on its own as well and is withheld
#: from it by `_MISCOLLECTED_COMMANDS` above.
#:
#: **A wrong record in either table is never merely surplus, because two things read them in a
#: direction it corrupts.** `refinery.lib.scripts.ps1.deobfuscation.wildcards` matches a wildcard
#: against `KNOWN_CMDLETS` and emits the unique hit as a command, which is what made a `Format-Hex`
#: record produce a call to a command the host cannot run — the `fhx` defect reached through the
#: other table. And `refinery.lib.scripts.ps1.ast.implicit_get_retry` refuses a retry for any name
#: a table claims, so a record for a command the host does not have suppresses a retry 5.1 performs.
#: The dash-free records were suspected of being that and are not: all 27 were measured on 5.1 and
#: every one resolves. Ten are the engine's own default session functions (`cd..`, `cd\`, `help`,
#: `importsystemmodules`, `mkdir`, `more`, `oss`, `pause`, `prompt`, `tabexpansion2`), fifteen are
#: Pester, PSDesiredStateConfiguration and PSReadLine — all shipped with Windows and autoloaded by
#: command name, so 5.1 really does resolve `describe` and `configuration` on a stock box — and
#: `powershell` and `powershell_ise.exe` are programs, where suppressing the retry is the answer.
#:
#: Anything added to either table has to be measured on a host first, and the ps1 oracle corpus is
#: where such a measurement is recorded.
KNOWN_ALIAS: dict[str, str] = {
_name.lower(): _definition for _name, _definition in _COMMANDS['aliases'].items()
}
#: The CimCmdlets short forms are module-provided aliases, and a bare pristine `Get-Alias` does not
#: list them even though their target cmdlets are collected. `run-pwsh.ps1` now imports the modules
#: before enumerating aliases, but the shipped data predates that fix, so they are restored here
#: until the next regeneration; each target is a canonical name already in `KNOWN_CMDLETS`.
for _cim_alias, _cim_command in {
'gcai' : 'Get-CimAssociatedInstance',
'gcim' : 'Get-CimInstance',
'gcls' : 'Get-CimClass',
'gcms' : 'Get-CimSession',
'icim' : 'Invoke-CimMethod',
'ncim' : 'New-CimInstance',
'ncms' : 'New-CimSession',
'ncso' : 'New-CimSessionOption',
'rcie' : 'Register-CimIndicationEvent',
'rcim' : 'Remove-CimInstance',
'rcms' : 'Remove-CimSession',
'scim' : 'Set-CimInstance',
}.items():
KNOWN_ALIAS.setdefault(_cim_alias, _cim_command)
KNOWN_PS_OPERATORS: dict[str, str] = {name.lower(): name for name in [
'-As',
'-BAnd',
'-BNot',
'-BOr',
'-BXor',
'-Contains',
'-CReplace',
'-Eq',
'-GE',
'-GT',
'-In',
'-IReplace',
'-Is',
'-IsNot',
'-Join',
'-LE',
'-Like',
'-LT',
'-Match',
'-NE',
'-Not',
'-NotContains',
'-NotIn',
'-NotLike',
'-NotMatch',
'-Replace',
'-Shl',
'-Shr',
'-Split',
'-XOr',
]}
KNOWN_PS_SWITCHES: dict[str, str] = {name.lower(): name for name in [
'-Command',
'-EncodedCommand',
'-Exec Bypass',
'-ExecutionPolicy',
'-File',
'-InputFormat',
'-NoExit',
'-NoLogo',
'-NoProfile',
'-NonInter',
'-OutputFormat',
'-Sta',
'-Version',
'-Windows Hidden',
'-WindowStyle',
]}
KNOWN_CMDLETS: dict[str, str] = {name.lower(): name for name in _COMMAND_TABLE}
KNOWN_CMDLETS.setdefault('convertfrom-base64', 'ConvertFrom-Base64')
KNOWN_CMDLETS.setdefault('powershell', 'PowerShell')
for _n in KNOWN_ALIAS.values():
KNOWN_CMDLETS.setdefault(_n.lower(), _n)
CMDLET_PARAMETERS: dict[str, list[str]] = {
_name.lower(): [
_param for _param, _info in _record['parameters'].items() if not _info['common']
]
for _name, _record in _COMMAND_TABLE.items()
}
ALL_PARAMETER_NAMES: dict[str, str] = {}
for _params in CMDLET_PARAMETERS.values():
for _p in _params:
ALL_PARAMETER_NAMES.setdefault(_p.lower(), _p)
#: The PowerShell common parameters, keyed by lowercased name and mapped to their lowercased
#: aliases. These are the parameters every advanced command shares (`-ErrorAction`, `-OutVariable`,
#: `-Verbose`, ...), which `CMDLET_PARAMETERS` and the views built from it deliberately exclude.
#: This is the one place they are surfaced, so a consumer reasoning about them — the out-variable
#: purity check does — reads them from the collected data rather than hardcoding the set. They are
#: identical on every command, so a single advanced command already determines the whole set; the
#: union is taken regardless so the first command that happens to lack one does not drop it.
COMMON_PARAMETERS: dict[str, tuple[str, ...]] = {}
for _record in _COMMAND_TABLE.values():
for _param, _info in _record['parameters'].items():
if _info['common']:
COMMON_PARAMETERS.setdefault(
_param.lower(),
tuple(_alias.lower() for _alias in _info['aliases']),
)
#: The common parameters that bind their argument as the *name* of a variable the command fills:
#: `-OutVariable`, `-ErrorVariable` and the rest, with their aliases (`ov`, `ev`, ...). A common
#: parameter names a variable exactly when its name ends in `Variable`, the convention the engine
#: defines them under, which `-OutBuffer` (a count) is the one common parameter to fail.
#:
#: These are the names a caller means when it asks which parameters address a variable by string,
#: and they are deliberately *not* the same set as the parameters that make a command impure:
#: `refinery.lib.scripts.ps1.analysis.effects` adds `-SetSeed` to that one, a `Get-Random` switch
#: that rewrites the generator state and names no variable at all. A consumer reading the impurity
#: set for names would take the `5` of `Get-Random -SetSeed 5` for a variable called `5`.
#: The out-variable parameters the derivation below must produce. Each is a fixed engine contract
#: whose loss silences a real write, so a collected surface that no longer carries one fails the
#: load rather than letting the set shrink silently.
REQUIRED_OUT_VARIABLE_PARAMETERS = frozenset({
'errorvariable',
'informationvariable',
'outvariable',
'pipelinevariable',
'warningvariable',
})
def _derive_out_variable_parameters(common: dict[str, tuple[str, ...]]) -> frozenset[str]:
"""
The out-variable parameters and their aliases, from a collected common-parameter surface.
Taken as an argument rather than read from the module so the floor below can be exercised
against a surface that has lost one; a check that only ever runs on the real data, at import,
cannot be shown to work.
"""
names: set[str] = set()
for parameter, aliases in common.items():
if parameter.endswith('variable'):
names.add(parameter)
names.update(aliases)
if missing := REQUIRED_OUT_VARIABLE_PARAMETERS - names:
raise ValueError(
F'the collected common parameters no longer surface the out-variable parameters '
F'{sorted(missing)!r}; every view built on them would silently stop treating those '
F'parameters as naming a variable, so the data and this module are out of step.'
)
return frozenset(names)
OUT_VARIABLE_PARAMETERS = _derive_out_variable_parameters(COMMON_PARAMETERS)
_VALUE_PARAMETERS: dict[str, frozenset[str]] = {}
_SCRIPTBLOCK_PARAMETERS: dict[str, frozenset[str]] = {}
_PARAMETER_SETS: dict[str, dict[str, frozenset[str]]] = {}
_POSITIONAL_SCRIPTBLOCK_SETS: dict[str, frozenset[str]] = {}
_COMMAND_RECORDS: dict[str, dict] = {
_name.lower(): _record for _name, _record in _COMMAND_TABLE.items()
}
def value_parameters(command: str) -> frozenset[str]:
"""
The lowercased parameter names and aliases of *command* that take a value, as opposed to the
switches that stand alone. Empty for a command the collected surface does not carry.
A caller reading a command's arguments needs this to tell `-Name x`, where `x` is the value of
`-Name`, from `-Recurse C:\\`, where the path is a positional argument of its own. The parser
renders both as a switch followed by a positional because it has no parameter metadata; this is
that metadata.
Looked up and memoized per command rather than built for every command at import: the table
carries thousands of commands and a caller asks about a handful.
"""
command = command.lower()
found = _VALUE_PARAMETERS.get(command)
if found is None:
names: set[str] = set()
for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items():
if _info['switch']:
continue
names.add(_parameter.lower())
names.update(_alias.lower() for _alias in _info['aliases'])
found = _VALUE_PARAMETERS[command] = frozenset(names)
return found
def scriptblock_parameters(command: str) -> frozenset[str]:
"""
The lowercased parameter names and aliases of *command* that are declared to take a script
block, as opposed to the ones that take a value of any other kind. Empty for a command the
collected surface does not carry.
A caller asking where a block written as an argument ends up needs this to tell
`ForEach-Object -Process { ... }`, which the command runs, from `ForEach-Object -InputObject
{ ... }`, which hands the block on as data and never runs it.
An array element type counts: `-Process` is declared `ScriptBlock[]` and `-Begin` is declared
`ScriptBlock`, and both are run.
Looked up and memoized per command for the reason `value_parameters` gives.
"""
command = command.lower()
found = _SCRIPTBLOCK_PARAMETERS.get(command)
if found is None:
names: set[str] = set()
for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items():
if _info['type'].rstrip('[]') != 'System.Management.Automation.ScriptBlock':
continue
names.add(_parameter.lower())
names.update(_alias.lower() for _alias in _info['aliases'])
found = _SCRIPTBLOCK_PARAMETERS[command] = frozenset(names)
return found
#: The set name the engine records for a parameter that belongs to every parameter set of its
#: command, rather than to any one of them. Every common parameter carries it.
EVERY_PARAMETER_SET = '__AllParameterSets'
def parameter_sets(command: str) -> dict[str, frozenset[str]]:
"""
The parameter sets each parameter of *command* belongs to, keyed by lowercased parameter name
and by each of its aliases. Empty for a command the collected surface does not carry.
5.1 chooses one set from the arguments a call writes, and the same position can mean a different
thing in each: `ForEach-Object` has a script block at position 0 of its `ScriptBlockSet` and a
member name at position 0 of its `PropertyAndMethodSet`. A caller reading what a positional
argument binds needs this to know which set the call is in.
Looked up and memoized per command for the reason `value_parameters` gives.
"""
command = command.lower()
found = _PARAMETER_SETS.get(command)
if found is None:
table: dict[str, frozenset[str]] = {}
for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items():
names = frozenset(_set['set'] for _set in _info['sets'])
table[_parameter.lower()] = names
for _alias in _info['aliases']:
table[_alias.lower()] = names
found = _PARAMETER_SETS[command] = table
return found
def positional_scriptblock_sets(command: str) -> frozenset[str]:
"""
The parameter sets of *command* in which the first positional argument binds a parameter
declared to take a script block. Empty where the command has no such parameter at all, and a
positional block then binds nothing this can name.
"""
command = command.lower()
found = _POSITIONAL_SCRIPTBLOCK_SETS.get(command)
if found is None:
names: set[str] = set()
for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items():
if _info['type'].rstrip('[]') != 'System.Management.Automation.ScriptBlock':
continue
names.update(_set['set'] for _set in _info['sets'] if _set['position'] == 0)
found = _POSITIONAL_SCRIPTBLOCK_SETS[command] = frozenset(names)
return found
SIMPLE_IDENTIFIER = re.compile(r'^[a-zA-Z_]\w*$')
OBJ_COMMANDS = frozenset({
'new-object',
})
WMI_COMMANDS = frozenset({
'get-ciminstance',
'get-wmiobject',
})
TYPE_ARG_COMMANDS = frozenset(OBJ_COMMANDS | WMI_COMMANDS)
GET_MEMBER_ALIASES = frozenset({'get-member', 'gm'})
GET_COMMAND_ALIASES = frozenset({'get-command', 'gcm'})
FOREACH_ALIASES = frozenset({'%', 'foreach', 'foreach-object'})
COMPARISON_OPS = {
'-eq': operator.eq,
'-ne': operator.ne,
'-lt': operator.lt,
'-le': operator.le,
'-gt': operator.gt,
'-ge': operator.ge,
}
ENCODING_MAP = {
'ascii' : 'ascii', # noqa
'bigendianunicode' : 'utf-16-be', # noqa
'default' : 'latin-1', # noqa
'unicode' : 'utf-16-le', # noqa
'utf7' : 'utf-7', # noqa
'utf8' : 'utf-8', # noqa
'utf32' : 'utf-32-le', # noqa
}
BUILTIN_VARIABLES = frozenset({'null', 'true', 'false'})
PS1_KNOWN_VARIABLES: dict[str, str] = {
name.lower(): name for name in [
'ConfirmPreference',
'ConsoleFileName',
'DebugPreference',
'Error',
'ErrorActionPreference',
'ExecutionContext',
'False',
'ForEach',
'FormatEnumerationLimit',
'HOME',
'Host',
'InformationPreference',
'Input',
'Matches',
'MaximumAliasCount',
'MaximumDriveCount',
'MaximumErrorCount',
'MaximumFunctionCount',
'MaximumHistoryCount',
'MaximumVariableCount',
'MyInvocation',
'NestedPromptLevel',
'Null',
'OutputEncoding',
'PID',
'PROFILE',
'ProgressPreference',
'PSCommandPath',
'PSCulture',
'PSDefaultParameterValues',
'PSEmailServer',
'PSHome',
'PSScriptRoot',
'PSSessionApplicationName',
'PSSessionConfigurationName',
'PSSessionOption',
'PSUICulture',
'PSVersionTable',
'PWD',
'ShellID',
'StackTrace',
'This',
'True',
'VerbosePreference',
'WarningPreference',
'WhatIfPreference',
]
}
FORMAT_PATTERN = re.compile(r'\{\{|\}\}|\{(\d+)(?:,(-?\d+))?(?::([^}]+))?\}')
def resolve_type(name: str | Ps1TypeName) -> Ps1TypeName | None:
"""
Resolve a .NET type name as written in PowerShell source to the one canonical `Ps1TypeName` the
collected metadata is keyed by, or `None` when the name is syntactically not a type or names a
type that was not collected. This understands the whole type-name grammar — accelerators, an
omitted `System.` prefix, generic arity, arrays — and it is the **only** thing that mints a
canonical name, which is what makes a canonical name comparable to another one.
A parsed name is not canonical: `System.Int32`, `system.int32` and `System.Int32, mscorlib` parse
to three unequal, differently hashing tuples, as do the two spellings of a list type. So the
result is *rebuilt* rather than returned — the collected record's own casing for the name, the
assembly qualification dropped because it does not distinguish a type here, and the arguments
resolved in turn. An argument that does not resolve makes the whole name unresolved, since a
name is only as understood as its least understood part.
The array suffixes are carried rather than dropped. `char[]` used to resolve to `System.Char`,
so every member question about an array of characters was answered off the element type; what
an array's members actually are is `_member_surface`'s question, asked of the name this returns.
"""
parsed = parse_type_name(name) if isinstance(name, str) else name
if parsed is None:
return None
return _canonical_type_name(parsed)
def named_type(name: str) -> Ps1TypeName:
"""
A type a *module* names, rather than one a script did: `resolve_type` with the unresolved case
treated as the defect it is. A module that writes a type name out and gets `None` back does not
receive a weaker answer, it receives a comparison that is silently false forever after, so this
raises at import instead of letting one through. Anything read out of a script goes through
`resolve_type`, where not being a type is an ordinary answer.
"""
resolved = resolve_type(name)
if resolved is None:
raise ValueError(F'the collected type table does not resolve {name}')
return resolved
def _canonical_type_name(parsed: Ps1TypeName) -> Ps1TypeName | None:
for candidate in _definition_candidates(parsed.definition):
if candidate not in _TYPE_TABLE and candidate not in _WMI_TYPES:
continue
arguments: list[Ps1TypeName] = []
for argument in parsed.arguments:
resolved = _canonical_type_name(argument)
if resolved is None:
return None
arguments.append(resolved)
base, _, arity = candidate.partition('`')
return Ps1TypeName(
name=base,
arity=int(arity) if arity else 0,
arguments=tuple(arguments),
ranks=parsed.ranks,
pointers=parsed.pointers,
byref=parsed.byref,
)
return None
def _definition_candidates(definition: str):
lower = definition.lower()
accel = _ACCELERATORS.get(lower)
if accel is not None:
yield accel
for key in _TYPE_LOOKUP.get(lower, ()):
yield key
_TYPE_LOOKUP: dict[str, list[str]] = {}
for _full in _TYPE_TABLE:
_TYPE_LOOKUP.setdefault(_full.lower(), []).append(_full)
_bare = _full.removeprefix('System.').lower()
if _bare != _full.lower():
_TYPE_LOOKUP.setdefault(_bare, []).append(_full)
#: The WMI classes, shaped like a collected type record so that one resolver answers for them too. A
#: WMI class is a type a PowerShell expression can have — `Get-WmiObject Win32_Process` yields one —
#: and before this it was a lowercase string in a namespace of its own, which is one of the type
#: vocabularies this module exists to have only one of.
#:
#: The capture records a class's property *names* and not their types, so each member says so with a
#: `type` of `None`: a chain through a WMI property stops resolving there, which is the honest
#: answer rather than a guess. `source` is its own tier for the same reason `engine` is — a caller
#: deciding about purity must be able to tell a WMI property from a reflected one.
_WMI_TYPES: dict[str, dict] = {}
for _classes in _WMI['namespaces'].values():
for _cls, _cls_info in _classes.items():
_WMI_TYPES.setdefault(_cls, {
'kind': 'wmi',
'sealed': False,
'members': {
_prop: {'kind': 'property', 'source': 'wmi', 'type': None}
for _prop in _cls_info['properties']
},
})
for _cls in _WMI_TYPES:
_TYPE_LOOKUP.setdefault(_cls.lower(), []).append(_cls)
def canonical_type(name: str) -> Ps1TypeName | None:
"""
The canonical `Ps1TypeName` of the type a source name refers to, or `None` when it does not
resolve to a collected type.
"""
return resolve_type(name)
def required_type_key(name: str) -> Ps1TypeName:
"""
Resolve a hand-kept table's type spelling to the lowercased canonical .NET `FullName` that table
keys on, raising when the collected metadata carries no such type. Building a table through this
at import time is a fail-loud floor: an entry naming a type the current data cannot resolve
stops the module from loading rather than going silently unmatched, which is how a stale table
used to fail open. A generic type is named by its arity-marked definition
(`collections.generic.list` `` `1 ``), the only spelling `resolve_type` resolves without its
type arguments.
"""
resolved = resolve_type(name)
if resolved is None:
raise ValueError(
F'a PowerShell analysis table names {name!r}, which the collected metadata does not '
F'resolve to a type; the data and the table are out of step.'
)
return resolved.generic_definition
def required_type_keys(names: set[str]) -> frozenset[Ps1TypeName]:
"""
A frozenset of canonical type keys built from readable source spellings through
`required_type_key`. Spellings that name the same type collapse to one entry, retiring the
dual-spelling entries (`int` beside `int32`) the allow-lists carried before the data could
resolve them.
"""
return frozenset(required_type_key(name) for name in names)
def required_member_keys(
entries: set[tuple[str, str]],
) -> frozenset[tuple[Ps1TypeName, str]]:
"""
A frozenset of `(canonical type key, lowercased member)` pairs, the form a member-keyed table
looks up. Only the type half is resolved through the data and floored by it; the member name is
matched against a `refinery.lib.scripts.ps1.model.Ps1InvokeMember.member` at its own casing.
"""
return frozenset(
(required_type_key(type_name), member.lower())
for type_name, member in entries
)
#: Where an array type's members come from. .NET gives every array the surface of `System.Array`
#: rather than one of its own, so `char[]` answers member questions off this and not off `Char`.
_ARRAY_SURFACE = 'System.Array'
def _type_record(key: str) -> dict | None:
"""
The collected record a canonical definition key names, from either the .NET type capture or the
WMI class capture. The two are separate captures of the same thing — what members a value of a
type has — so they are read through one accessor and every query below is written once.
"""
record = _TYPE_TABLE.get(key)
if record is None:
record = _WMI_TYPES.get(key)
return record
def _member_surface(name: str | Ps1TypeName) -> str | None:
"""
The key of the collected record whose members a value of this type carries, or `None` when the
type does not resolve. This is the one place the array rule lives: an array carries the members
of `System.Array` whatever it is an array *of*, so every member query — the table, the display
order, one record, a property's type, the static overloads — reaches the same surface through
here rather than each deciding for itself.
Sealedness is deliberately not asked here, because it is not a question about the member
surface: `System.Array` is not sealed, and every array type is.
"""
resolved = resolve_type(name)
if resolved is None:
return None
if resolved.ranks:
return _ARRAY_SURFACE
return resolved.definition
def type_members(name: str | Ps1TypeName) -> dict[str, dict] | None:
"""
The full member table of a type, keyed by member name, including the fields and Extended Type
System members the historical views omit. Each value carries at least `kind` and `source`.
Returns `None` when the type is not collected.
"""
key = _member_surface(name)
if key is None:
return None
record = _type_record(key)
return None if record is None else record['members']
def member_order(name: str | Ps1TypeName) -> list[str] | None:
"""
The order `Get-Member` displays a type's members in, as observed on a real instance, or `None`
when it was not collected for this type. This is the authentic display order, not a synthesis
from the member table, and is only present for the types the generator has an instance for.
"""
key = _member_surface(name)
if key is None:
return None
record = _type_record(key)
return None if record is None else record.get('member_order')
def type_is_sealed(name: str | Ps1TypeName) -> bool:
"""
Whether the named type is sealed — no subtype of it exists, so a value of the type carries
exactly the members reflection reports and nothing a subtype could add. `False` when the type
is not collected or is not sealed, so a caller that needs sealedness to justify a grant fails
closed on an unknown type rather than assuming it. The flag is read from the collected metadata,
which is what retires the hand-asserted sealedness the effect layer's pure-read allow-list used
to rest on.
An array type is sealed whatever its element type is — .NET derives no type from one — so it is
answered from the array rather than through `_member_surface`, which would answer it off
`System.Array` and call it unsealed.
"""
resolved = resolve_type(name)
if resolved is None:
return False
if resolved.ranks:
return True
record = _type_record(resolved.definition)
return record is not None and bool(record.get('sealed'))
class MemberLookup(enum.Enum):
"""
The two non-record outcomes of `member_record`. A member query has three outcomes a purity gate
must keep apart: the type was never collected, so nothing is known about its members; the type is
collected but carries no member of that name; or the member is present and its record is returned.
Collapsing the first two into a single `None` conflates an unknown surface with a known absence,
which a sound gate must treat oppositely — an unknown surface is unsafe, a known-absent read
yields `$null`.
"""
UNCOLLECTED = 'uncollected'
ABSENT = 'absent'
#: The members PowerShell's object adapter puts on every value, whatever its type. `Get-Member
#: -Force` reports them per instance rather than per type, so the capture — which walks types —
#: cannot hold them, and before this a read of one answered as though the member did not exist.
#:
#: Each is measured on a 5.1 host rather than reasoned about; see `TYPE_TRANSCRIPTS` in
#: `test.lib.scripts.ps1.test_oracle`. `Count` is 1 for a scalar, `PSTypeNames` is the type's own
#: name followed by its bases, and `PSObject` wraps the value.
#:
#: `Length` is deliberately absent even though a scalar answers 1 for it. Every type that carries a
#: real `Length` has it collected — `System.String`, `System.Array`, `System.IO.FileStream` — so the
#: tier would never be reached for one; putting it here would only add a way for the *next* type
#: whose capture is incomplete to have a live getter vouched for. The scalar case is left to the
#: value domain, which knows what a scalar is.
#:
#: `source` is neither `reflection` nor `ets`. Filing these as `ets` would make every read of one
#: impure and delete nothing that is deleted today; filing them as `reflection` would claim the
#: capture saw them. They are their own tier so that a gate can decide about them on purpose.
_ENGINE_MEMBERS: dict[str, dict] = {
'count': {'kind': 'property', 'source': 'engine', 'type': 'System.Int32'},
'pstypenames': {'kind': 'property', 'source': 'engine', 'type': 'System.String[]'},
'psobject': {
'kind': 'property',
'source': 'engine',
'type': 'System.Management.Automation.PSObject',
},
}
def engine_member(member: str) -> dict | None:
"""
The record for a member the object adapter adds to every value, or `None` for a name that is not
one. Callers consult this only where `member_record` answered `MemberLookup.ABSENT`: a collected
record is what the type really carries and is never overridden by this.
"""
return _ENGINE_MEMBERS.get(member.lower())
def type_names(name: str | Ps1TypeName) -> list[str] | None:
"""
The value a type's `PSTypeNames` enumerates: its own reflection `FullName` followed by the
`FullName` of every type it derives from, ending at `System.Object`. Measured on a 5.1 host; see
`TYPE_TRANSCRIPTS` in `test.lib.scripts.ps1.test_oracle`.
Returns `None` when the type does not resolve, is an array — whose chain is `System.Array` and
not that of its element type — or reaches a base that was not collected, so a caller folds a
chain that is known whole and never a truncated one.
"""
resolved = resolve_type(name)
if resolved is None or resolved.is_array:
return None
names: list[str] = []
current: str | None = resolved.definition
while current is not None:
record = _type_record(current)
if record is None:
return None
names.append(current)
current = record.get('base')
return names
def _unmodelled_for_type_test(kind: Ps1TypeName) -> bool:
"""
Whether a type carries a shape the assignability model below does not settle: a generic, a
pointer or a by-reference type. An array is modelled and is deliberately not one of these.
"""
return bool(kind.arity or kind.arguments or kind.pointers or kind.byref)
def _array_interfaces() -> frozenset[str]:
"""
The interfaces `System.Array` is recorded to implement, which every array type implements too.
Read on demand rather than at import so it does not depend on the type table's load order.
"""
record = _type_record('System.Array')
return frozenset(record['interfaces']) if record is not None else frozenset()
def _array_is_assignable_to(target: Ps1TypeName) -> bool | None:
"""
Whether an array satisfies `-is target`. An array derives only `System.Array` and `System.Object`
and implements the interfaces `System.Array` carries, so a concrete target that is neither is a
definite `False`. The two the model does not settle are covariance — a different array type — and
the generic collection interfaces an array implements beyond `System.Array`'s own; both are
declined rather than denied so a fold never answers one the way 5.1 would not.
"""
if target.is_array:
return None
if target.definition in ('System.Array', 'System.Object'):
return True
record = _type_record(target.definition)
if record is None:
return None
if record.get('kind') == 'interface':
return True if target.definition in _array_interfaces() else None
return False
def is_assignable_to(
value_type: str | Ps1TypeName,
target_type: str | Ps1TypeName,
) -> bool | None:
"""
Whether a value whose runtime type is `value_type` satisfies `value_type -is target_type`, the
test PowerShell's `-is` and `-isnot` operators perform. `True` when the runtime type is the
target, derives from it, or implements it; `False` when the collected model settles that it is
none of those; and `None` where the model does not settle it — an unresolved or generic type, an
array against a different array type, or an array against an interface `System.Array` is not
recorded to carry. A `None` is a fold declined, never a `False` guessed, so a caller never
answers a test 5.1 would answer the other way.
For a non-array value the class relation is read whole: the base chain `type_names` returns and
the interface set the collected record carries are both what reflection reports for the value's
own type, so a target that is neither an ancestor class nor a listed interface is a definite
`False`.
"""
value = resolve_type(value_type)
target = resolve_type(target_type)
if value is None or target is None:
return None
if _unmodelled_for_type_test(value) or _unmodelled_for_type_test(target):
return None
if value.is_array:
return True if value == target else _array_is_assignable_to(target)
if target.is_array:
return False
chain = type_names(value)
if chain is None:
return None
if target.definition in chain:
return True
record = _type_record(value.definition)
if record is None:
return None
return target.definition in record.get('interfaces', ())
def member_record(name: str | Ps1TypeName, member: str) -> dict | MemberLookup:
"""
The collected record for a single member of a type, or a `MemberLookup` sentinel explaining why
there is none. `MemberLookup.UNCOLLECTED` means the type has no member table at all;
`MemberLookup.ABSENT` means the type is collected and carries no member of that name. The member
is matched case-insensitively, as PowerShell resolves it, and the first match wins. The record
carries at least `kind` and `source`, which distinguish a plain reflection property or field from
a code-running Extended Type System member.
A member the capture holds wins over `engine_member`, which is why the adapter tier is consulted
here on the way out rather than by each caller: a caller that asked the tier first would answer
`Count` off it for `System.Array`, which carries a real one.
"""
members = type_members(name)
if members is None:
return MemberLookup.UNCOLLECTED
lower = member.lower()
for stored, record in members.items():
if stored.lower() == lower:
return record
engine = engine_member(member)
if engine is not None:
return engine
return MemberLookup.ABSENT
def view_members(name: str | Ps1TypeName) -> dict[str, dict] | None:
"""
The members of a type that `Get-Member` reports without `-Force`: the reflected methods and
properties, and for a WMI class its properties. Fields and Extended Type System members are
withheld, as they are from the historical `TYPE_MEMBERS` view this replaces — the difference is
that the type is resolved through `resolve_type`, so an array answers off `System.Array` and a
spelling that is not already the lowercased `FullName` resolves instead of missing.
An enum has no members in this sense and its named values stand in for them, which is what the
view has always done and what a caller listing what may follow a dot needs.
"""
key = _member_surface(name)
if key is None:
return None
record = _type_record(key)
if record is None:
return None
if record.get('kind') == 'enum':
return {value: {'kind': 'property', 'source': 'enum'} for value in record['enum_values'] or {}}
return _view_members(record)
def member_names(name: str | Ps1TypeName) -> list[str] | None:
"""
The names `view_members` reports, sorted, or `None` when the type does not resolve.
"""
members = view_members(name)
return None if members is None else sorted(members)
def canonical_member(name: str | Ps1TypeName, member: str) -> str | None:
"""
The casing the metadata records for a member, matched case-insensitively as PowerShell resolves
it, or `None` when the type or the member does not resolve.
"""
members = view_members(name)
if members is None:
return None
lower = member.lower()
for stored in members:
if stored.lower() == lower:
return stored
return None
def resolve_member_type(name: str | Ps1TypeName, member: str) -> Ps1TypeName | None:
"""
The type a property of a type holds, or `None` when the type does not resolve, carries no such
member, or the member is not a property. The answer is a canonical `Ps1TypeName`, so a chain of
reads composes: what `$x.Prop.Length` resolves to is this asked twice.
"""
record = member_record(name, member)
if isinstance(record, MemberLookup):
return None
if record.get('kind') != 'property':
return None
declared = record.get('type')
if not declared:
return None
return resolve_type(declared)
def static_overloads(name: str | Ps1TypeName, member: str) -> list[dict]:
"""
The static overloads of a method on a type, each a record carrying its `returns` and its
`parameters`, where every parameter records its `byref`/`out` direction, its `type` and its
`position`. Returns an empty list when the type is not collected or carries no static method of
that name. A caller asks this to reason about a `[Type]::Member(...)` call, whose reachable
surface is the static one.
"""
return _overloads(name, member, static=True)
def instance_overloads(name: str | Ps1TypeName, member: str) -> list[dict]:
"""
The overloads of a method that a *value* of the type carries, in the same shape
`static_overloads` returns. Returns an empty list when the type is not collected or carries no
instance method of that name, which is what says a call on a value of it cannot be made at all:
`System.Char` has a `ToUpper`, every overload of it is static, and `([char]65).ToUpper()`
reports `MethodNotFound` on 5.1 while `[char]::ToUpper('a')` answers.
"""
return _overloads(name, member, static=False)
def _overloads(name: str | Ps1TypeName, member: str, *, static: bool) -> list[dict]:
"""
One side of a method's collected surface. The member is matched case-insensitively, as
PowerShell resolves it, and the first match wins. A non-method member that case-collides with
the method name is skipped, not taken as the answer, so a real method behind it is still found.
"""
members = type_members(name)
if members is None:
return []
for stored, record in members.items():
if stored.lower() != member.lower():
continue
if record.get('kind') != 'method':
continue
return [
overload for overload in record.get('overloads') or ()
if bool(overload.get('static')) is static
]
return []
_COMMAND_LOOKUP: dict[str, dict] = {
_name.lower(): _record for _name, _record in _COMMAND_TABLE.items()
}
#: The operator grid's own version, which is what "its own version" below has to mean if it is to
#: mean anything: the two resources change for different reasons and are written by different
#: scripts, so a table added to the grid must not declare five host tables out of date when nothing
#: about them moved. Sharing `SCHEMA_VERSION` made exactly that the price of adding `unary` and
#: `type_tests`.
OPERATOR_SCHEMA_VERSION = 2
#: What the operator and conversion grids were captured from. Its own resource and its own version,
#: because its subject is different from the rest of this module: the five host tables describe what
#: one installation *has*, and this describes what the language *does*, which is fixed for a version
#: of PowerShell. They are regenerated by different scripts and adjudicated separately.
_OPERATORS = _load('pwsh-operators.json.xz')
_operator_schema = _OPERATORS['schema']['version']
if _operator_schema != OPERATOR_SCHEMA_VERSION:
raise ValueError(
F'pwsh operator grid schema version {_operator_schema} is not the expected '
F'{OPERATOR_SCHEMA_VERSION}; the reader and the collected data are out of step.'
)
#: The two outcomes the capture records that are not a type. A cell that threw for some pair of
#: operands is one the domain must be able to model as throwing, and a cell that yielded `$null` has
#: no type to report at all — `$null.GetType()` throws, so naming one would put a type in the grid
#: that no value has.
_THREW = 'throw'
_WAS_NULL = 'null'
class OperatorOutcome(typing.NamedTuple):
"""
Everything one grid cell was observed to produce, over several witness values per operand type.
A cell is deliberately not a type. `(operator, left type, right type)` does not determine one:
`512MB * 512MB` is a `Double` out of the same Int32-by-Int32 cell that gives an `Int32`
elsewhere, and `12 + '0xabc'` is `2760` where `16 + 'file'` throws. So `types` is a set, and a
caller may read a single type out of it only when there is exactly one and neither `may_throw`
nor `may_be_null` is set. Anything wider is decided by the values, and answering it needs a
kernel rather than the grid.
`may_throw` is on its own axis rather than being a member of `types` because throwing is not an
alternative to having a type: `[int]$x` over a string is *an Int32, or it throws*, which is the
commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing.
"""
types: frozenset[Ps1TypeName]
may_throw: bool
may_be_null: bool
@property
def single_type(self) -> Ps1TypeName | None:
"""
The one type this cell always produces, or `None` when it does not always produce one.
"""
if self.may_throw or self.may_be_null or len(self.types) != 1:
return None
return next(iter(self.types))
@property
def always_throws(self) -> bool:
"""
Whether every witnessed pair in this cell threw, so that no value was observed to come out
of it at all.
**It names no cause, and must not be read as naming one.** 220 of the binary grid's cells
answer `True` and at least 26 of those are a *value* reason rather than a missing method:
`2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for
an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so
every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the
control that shows it: same operator, same value reason, **not** selected, because `$true`
divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host.
**`may_throw` alone answers a different question, and reading it as this one is the mistake
the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split
that matters is inside those five: `$true * 2` throws with nothing witnessed, while
`$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading
the throw axis on its own put `/` and `%` into the emulator's refusal set, twice.
The claim is about the *cell* and never about one operand. Projecting it onto a side is a
separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that
and is sound only because its three members were measured operand-wise against a host, and
because it is used to refuse, where a wrong projection costs a fold and cannot invent a
value.
A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell
of either grid carries that shape today, which makes the clause inert rather than idle: it
is what the sentence above means, and a capture that ever recorded one would need it.
"""
return self.may_throw and not self.may_be_null and not self.types
def _outcome(recorded: list[str] | None) -> OperatorOutcome | None:
if recorded is None:
return None
types = set()
for entry in recorded:
if entry in (_THREW, _WAS_NULL):
continue
resolved = resolve_type(entry)
if resolved is None:
return None
types.add(resolved)
return OperatorOutcome(
types=frozenset(types),
may_throw=_THREW in recorded,
may_be_null=_WAS_NULL in recorded,
)
def operand_witnesses() -> dict[str, tuple[str, ...]]:
"""
The expressions each grid cell was measured over, keyed by the type they produce. This is the
capture's *method* rather than its result, and it is published because a cell is a lower bound:
a caller deciding how far to trust one has to know what was tried, and a caller that recorded
such a decision has to be able to tell that the ground under it moved.
"""
return {
name: tuple(texts) for name, texts in _OPERATORS['witnesses'].items()
}
def binary_operators() -> frozenset[str]:
"""
Every operator the binary grid has a table for, lower case as the capture wrote them. Published
so that a caller quantifying over the measured operators reads the set rather than carrying one
of its own, which would go stale the next time the capture is widened.
A test that writes the set out *is* keeping a copy, and does so on purpose: it is the ratchet
that makes a regeneration fail loudly instead of quietly covering more. This is for the other
kind of caller, the one that wants to iterate and does not want to be told the answer.
"""
return frozenset(_OPERATORS['binary'])
def binary_outcome(
operator: str,
left: str | Ps1TypeName,
right: str | Ps1TypeName,
) -> OperatorOutcome | None:
"""
What `left <operator> right` was observed to produce, or `None` when the grid does not cover the
operator or either type. An uncovered cell is not an empty one: a caller must read `None` as
*nothing is known here* and decline, never as *this produces nothing*.
"""
rows = _OPERATORS['binary'].get(operator.lower())
if rows is None:
return None
row = _axis_position(_operand_axis(), left)
column = _axis_position(_operand_axis(), right)
if row is None or column is None:
return None
return _outcomes()[rows[row][column]]
def unary_outcome(
operator: str,
operand: str | Ps1TypeName,
) -> OperatorOutcome | None:
"""
What `<operator> operand` was observed to produce, for the operators that take one operand.
Read the same way as `binary_outcome`.
"""
row = _OPERATORS['unary'].get(operator.lower())
if row is None:
return None
column = _axis_position(_operand_axis(), operand)
return None if column is None else _outcomes()[row[column]]
def type_test_outcome(
operator: str,
source: str | Ps1TypeName,
target: str | Ps1TypeName,
) -> OperatorOutcome | None:
"""
What `source <operator> [target]` was observed to produce, for the operators whose right operand
is a type rather than a value. Read the same way as `binary_outcome`.
"""
rows = _OPERATORS['type_tests'].get(operator.lower())
if rows is None:
return None
row = _axis_position(_operand_axis(), source)
column = _axis_position(_target_axis(), target)
if row is None or column is None:
return None
return _outcomes()[rows[row][column]]
def conversion_outcome(
target: str | Ps1TypeName,
source: str | Ps1TypeName,
) -> OperatorOutcome | None:
"""
What casting a value of `source` to `target` was observed to produce, or `None` when the grid
does not cover the cast. Read the same way as `binary_outcome`.
"""
key = _grid_key(target)
row = None if key is None else _conversions().get(key)
if row is None:
return None
column = _axis_position(_operand_axis(), source)
return None if column is None else _outcomes()[row[column]]
@functools.cache
def _outcomes() -> tuple[OperatorOutcome | None, ...]:
"""
Every distinct outcome the capture recorded, in the order the cells index them by.
A cell is a position in this table rather than a list of names, because the grids hold nine
thousand cells between them and seventy-two answers. Reading it that way also means the type
names are resolved seventy-two times when the module loads instead of once per lookup.
"""
return tuple(_outcome(recorded) for recorded in _OPERATORS['outcomes'])
@functools.cache
def _operand_axis() -> dict[str, int]:
"""
Where each operand type sits along the axis the cells are indexed by, keyed the way everything
else here is keyed. The order is written into the capture rather than assumed from any other
part of it, so that reading a cell never depends on two lists having stayed in step.
"""
return _axis_index(_OPERATORS['types'], 'operand')
@functools.cache
def _target_axis() -> dict[str, int]:
"""
Where each cast target sits along the axis the type-operator cells are indexed by.
The capture writes this axis as the accelerator a cast is *written* with — `int`, `long`,
`byte[]` — and the operand axis as the type a value *reported*. Those are two spellings of one
thing, and a caller naming a type would otherwise have to know which axis it was about to index.
"""
return _axis_index(_OPERATORS['targets'], 'cast target')
@functools.cache
def _conversions() -> dict[str, list[int]]:
"""
The conversion grid keyed by the target a caller names rather than the accelerator the capture
wrote, for the reason `_target_axis` re-keys the same spellings.
"""
keyed = {}
for target, row in _OPERATORS['conversions'].items():
key = _grid_key(target)
if key is None:
raise ValueError(F'the conversion grid names a target that is not a type: {target}')
keyed[key] = row
return keyed
def _axis_position(axis: dict[str, int], name: str | Ps1TypeName) -> int | None:
"""
Where a type sits along an axis, or `None` when the name resolves to no type at all or the axis
has no place for it. Both are the same answer to a caller: the grid does not cover this.
"""
key = _grid_key(name)
return None if key is None else axis.get(key)
def _axis_index(names: list[str], what: str) -> dict[str, int]:
"""
An axis as the position of each type along it. A spelling that does not resolve is an error
rather than a missing entry: it would silently make every cell indexed through it unknown.
"""
index = {}
for position, name in enumerate(names):
key = _grid_key(name)
if key is None:
raise ValueError(F'the {what} axis names something that is not a type: {name}')
index[key] = position
return index
def _grid_key(name: str | Ps1TypeName) -> str | None:
"""
The spelling the grid is keyed by, which is a rendered canonical name rather than a definition:
the grid has a row for `System.Object[]` and one for `System.Object`, and they are two types.
"""
resolved = resolve_type(name)
return None if resolved is None else str(resolved)
def command(name: str) -> dict | None:
"""
The collected record for a command, or `None` when the name is not a known cmdlet or function.
The record carries the command kind, its module, declared output types with an
`output_type_declared` flag, and the full parameter table including the common parameters.
Command names are matched case-insensitively, as PowerShell resolves them.
"""
return _COMMAND_LOOKUP.get(name.lower())
def command_output_types(name: str) -> frozenset[str] | None:
"""
The declared output types of a command, lowercased, or `None` when the command declares none. A
set is returned only when the command carries an `[OutputType]` attribute — the
`output_type_declared` flag — because an `output_types` list without it records what was observed,
not what the author promised, and an empty one means the declaration was never made rather than
that the command emits nothing. An unknown command is `None` for the same reason. Names are
matched case-insensitively.
"""
record = _COMMAND_LOOKUP.get(name.lower())
if record is None or not record.get('output_type_declared'):
return None
return frozenset(_type.lower() for _type in record['output_types'])
Global variables
var PROGRAM_NAMES-
Derived by intersecting every PATHEXT-matching file in
System32andWindowswith the 523 bare nounsimplicit_get_retry()rewrites;tpmis the whole intersection. A program the analyst installed cannot be here and is the declared residual:dateresolves to Git'sdate.exeon the development box and toGet-Dateon one without it, and nothing readable from a script says which. This is a floor under that residual rather than a fix for it. var TYPE_ACCELERATORS-
The set of type-accelerator spellings, lowercased. An accelerator is already the shortest readable name for its type, so display normalization leaves it as written rather than expanding it to the verbose full name:
[ref]and[int]stay, where[System.Int32]folds to[Int32]. var KNOWN_ALIAS-
Anything added to either table has to be measured on a host first, and the ps1 oracle corpus is where such a measurement is recorded.
var COMMON_PARAMETERS-
The PowerShell common parameters, keyed by lowercased name and mapped to their lowercased aliases. These are the parameters every advanced command shares (
-ErrorAction,-OutVariable,-Verbose, …), whichCMDLET_PARAMETERSand the views built from it deliberately exclude. This is the one place they are surfaced, so a consumer reasoning about them — the out-variable purity check does — reads them from the collected data rather than hardcoding the set. They are identical on every command, so a single advanced command already determines the whole set; the union is taken regardless so the first command that happens to lack one does not drop it. var REQUIRED_OUT_VARIABLE_PARAMETERS-
These are the names a caller means when it asks which parameters address a variable by string, and they are deliberately not the same set as the parameters that make a command impure:
refinery.lib.scripts.ps1.analysis.effectsadds-SetSeedto that one, aGet-Randomswitch that rewrites the generator state and names no variable at all. A consumer reading the impurity set for names would take the5ofGet-Random -SetSeed 5for a variable called5. The out-variable parameters the derivation below must produce. Each is a fixed engine contract whose loss silences a real write, so a collected surface that no longer carries one fails the load rather than letting the set shrink silently. var EVERY_PARAMETER_SET-
The set name the engine records for a parameter that belongs to every parameter set of its command, rather than to any one of them. Every common parameter carries it.
var OPERATOR_SCHEMA_VERSION-
The operator grid's own version, which is what "its own version" below has to mean if it is to mean anything: the two resources change for different reasons and are written by different scripts, so a table added to the grid must not declare five host tables out of date when nothing about them moved. Sharing
SCHEMA_VERSIONmade exactly that the price of addingunaryandtype_tests.
Functions
def is_type(name, target)-
Whether a type name as written in PowerShell source names the same type as
target. Both sides go throughresolve_type(), so no difference of spelling can make two names for one type answerFalse: an accelerator, an omittedSystem.prefix, a difference of case, whitespace inside the name, an assembly qualification and a generic argument list are all understood.This used to compare a lowercased name against a lowercased full name, which meant it saw only the three spellings that transformation happens to produce and answered
Falsefor the rest —[Byte[]]among them, because an array suffix was not part of a name at all.Expand source code Browse git
def is_type(name: str, target: str) -> bool: """ Whether a type name as written in PowerShell source names the same type as `target`. Both sides go through `resolve_type`, so no difference of spelling can make two names for one type answer `False`: an accelerator, an omitted `System.` prefix, a difference of case, whitespace inside the name, an assembly qualification and a generic argument list are all understood. This used to compare a lowercased name against a lowercased full name, which meant it saw only the three spellings that transformation happens to produce and answered `False` for the rest — `[Byte[]]` among them, because an array suffix was not part of a name at all. """ resolved = resolve_type(name) return resolved is not None and resolved == resolve_type(target) def value_parameters(command)-
The lowercased parameter names and aliases of command that take a value, as opposed to the switches that stand alone. Empty for a command the collected surface does not carry.
A caller reading a command's arguments needs this to tell
-Name x, wherexis the value of-Name, from-Recurse C:\, where the path is a positional argument of its own. The parser renders both as a switch followed by a positional because it has no parameter metadata; this is that metadata.Looked up and memoized per command rather than built for every command at import: the table carries thousands of commands and a caller asks about a handful.
Expand source code Browse git
def value_parameters(command: str) -> frozenset[str]: """ The lowercased parameter names and aliases of *command* that take a value, as opposed to the switches that stand alone. Empty for a command the collected surface does not carry. A caller reading a command's arguments needs this to tell `-Name x`, where `x` is the value of `-Name`, from `-Recurse C:\\`, where the path is a positional argument of its own. The parser renders both as a switch followed by a positional because it has no parameter metadata; this is that metadata. Looked up and memoized per command rather than built for every command at import: the table carries thousands of commands and a caller asks about a handful. """ command = command.lower() found = _VALUE_PARAMETERS.get(command) if found is None: names: set[str] = set() for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items(): if _info['switch']: continue names.add(_parameter.lower()) names.update(_alias.lower() for _alias in _info['aliases']) found = _VALUE_PARAMETERS[command] = frozenset(names) return found def scriptblock_parameters(command)-
The lowercased parameter names and aliases of command that are declared to take a script block, as opposed to the ones that take a value of any other kind. Empty for a command the collected surface does not carry.
A caller asking where a block written as an argument ends up needs this to tell
ForEach-Object -Process { ... }, which the command runs, fromForEach-Object -InputObject { ... }, which hands the block on as data and never runs it.An array element type counts:
-Processis declaredScriptBlock[]and-Beginis declaredScriptBlock, and both are run.Looked up and memoized per command for the reason
value_parameters()gives.Expand source code Browse git
def scriptblock_parameters(command: str) -> frozenset[str]: """ The lowercased parameter names and aliases of *command* that are declared to take a script block, as opposed to the ones that take a value of any other kind. Empty for a command the collected surface does not carry. A caller asking where a block written as an argument ends up needs this to tell `ForEach-Object -Process { ... }`, which the command runs, from `ForEach-Object -InputObject { ... }`, which hands the block on as data and never runs it. An array element type counts: `-Process` is declared `ScriptBlock[]` and `-Begin` is declared `ScriptBlock`, and both are run. Looked up and memoized per command for the reason `value_parameters` gives. """ command = command.lower() found = _SCRIPTBLOCK_PARAMETERS.get(command) if found is None: names: set[str] = set() for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items(): if _info['type'].rstrip('[]') != 'System.Management.Automation.ScriptBlock': continue names.add(_parameter.lower()) names.update(_alias.lower() for _alias in _info['aliases']) found = _SCRIPTBLOCK_PARAMETERS[command] = frozenset(names) return found def parameter_sets(command)-
The parameter sets each parameter of command belongs to, keyed by lowercased parameter name and by each of its aliases. Empty for a command the collected surface does not carry.
5.1 chooses one set from the arguments a call writes, and the same position can mean a different thing in each:
ForEach-Objecthas a script block at position 0 of itsScriptBlockSetand a member name at position 0 of itsPropertyAndMethodSet. A caller reading what a positional argument binds needs this to know which set the call is in.Looked up and memoized per command for the reason
value_parameters()gives.Expand source code Browse git
def parameter_sets(command: str) -> dict[str, frozenset[str]]: """ The parameter sets each parameter of *command* belongs to, keyed by lowercased parameter name and by each of its aliases. Empty for a command the collected surface does not carry. 5.1 chooses one set from the arguments a call writes, and the same position can mean a different thing in each: `ForEach-Object` has a script block at position 0 of its `ScriptBlockSet` and a member name at position 0 of its `PropertyAndMethodSet`. A caller reading what a positional argument binds needs this to know which set the call is in. Looked up and memoized per command for the reason `value_parameters` gives. """ command = command.lower() found = _PARAMETER_SETS.get(command) if found is None: table: dict[str, frozenset[str]] = {} for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items(): names = frozenset(_set['set'] for _set in _info['sets']) table[_parameter.lower()] = names for _alias in _info['aliases']: table[_alias.lower()] = names found = _PARAMETER_SETS[command] = table return found def positional_scriptblock_sets(command)-
The parameter sets of command in which the first positional argument binds a parameter declared to take a script block. Empty where the command has no such parameter at all, and a positional block then binds nothing this can name.
Expand source code Browse git
def positional_scriptblock_sets(command: str) -> frozenset[str]: """ The parameter sets of *command* in which the first positional argument binds a parameter declared to take a script block. Empty where the command has no such parameter at all, and a positional block then binds nothing this can name. """ command = command.lower() found = _POSITIONAL_SCRIPTBLOCK_SETS.get(command) if found is None: names: set[str] = set() for _parameter, _info in _COMMAND_RECORDS.get(command, {}).get('parameters', {}).items(): if _info['type'].rstrip('[]') != 'System.Management.Automation.ScriptBlock': continue names.update(_set['set'] for _set in _info['sets'] if _set['position'] == 0) found = _POSITIONAL_SCRIPTBLOCK_SETS[command] = frozenset(names) return found def resolve_type(name)-
Resolve a .NET type name as written in PowerShell source to the one canonical
Ps1TypeNamethe collected metadata is keyed by, orNonewhen the name is syntactically not a type or names a type that was not collected. This understands the whole type-name grammar — accelerators, an omittedSystem.prefix, generic arity, arrays — and it is the only thing that mints a canonical name, which is what makes a canonical name comparable to another one.A parsed name is not canonical:
System.Int32,system.int32andSystem.Int32, mscorlibparse to three unequal, differently hashing tuples, as do the two spellings of a list type. So the result is rebuilt rather than returned — the collected record's own casing for the name, the assembly qualification dropped because it does not distinguish a type here, and the arguments resolved in turn. An argument that does not resolve makes the whole name unresolved, since a name is only as understood as its least understood part.The array suffixes are carried rather than dropped.
char[]used to resolve toSystem.Char, so every member question about an array of characters was answered off the element type; what an array's members actually are is_member_surface's question, asked of the name this returns.Expand source code Browse git
def resolve_type(name: str | Ps1TypeName) -> Ps1TypeName | None: """ Resolve a .NET type name as written in PowerShell source to the one canonical `Ps1TypeName` the collected metadata is keyed by, or `None` when the name is syntactically not a type or names a type that was not collected. This understands the whole type-name grammar — accelerators, an omitted `System.` prefix, generic arity, arrays — and it is the **only** thing that mints a canonical name, which is what makes a canonical name comparable to another one. A parsed name is not canonical: `System.Int32`, `system.int32` and `System.Int32, mscorlib` parse to three unequal, differently hashing tuples, as do the two spellings of a list type. So the result is *rebuilt* rather than returned — the collected record's own casing for the name, the assembly qualification dropped because it does not distinguish a type here, and the arguments resolved in turn. An argument that does not resolve makes the whole name unresolved, since a name is only as understood as its least understood part. The array suffixes are carried rather than dropped. `char[]` used to resolve to `System.Char`, so every member question about an array of characters was answered off the element type; what an array's members actually are is `_member_surface`'s question, asked of the name this returns. """ parsed = parse_type_name(name) if isinstance(name, str) else name if parsed is None: return None return _canonical_type_name(parsed) def named_type(name)-
A type a module names, rather than one a script did:
resolve_type()with the unresolved case treated as the defect it is. A module that writes a type name out and getsNoneback does not receive a weaker answer, it receives a comparison that is silently false forever after, so this raises at import instead of letting one through. Anything read out of a script goes throughresolve_type(), where not being a type is an ordinary answer.Expand source code Browse git
def named_type(name: str) -> Ps1TypeName: """ A type a *module* names, rather than one a script did: `resolve_type` with the unresolved case treated as the defect it is. A module that writes a type name out and gets `None` back does not receive a weaker answer, it receives a comparison that is silently false forever after, so this raises at import instead of letting one through. Anything read out of a script goes through `resolve_type`, where not being a type is an ordinary answer. """ resolved = resolve_type(name) if resolved is None: raise ValueError(F'the collected type table does not resolve {name}') return resolved def canonical_type(name)-
The canonical
Ps1TypeNameof the type a source name refers to, orNonewhen it does not resolve to a collected type.Expand source code Browse git
def canonical_type(name: str) -> Ps1TypeName | None: """ The canonical `Ps1TypeName` of the type a source name refers to, or `None` when it does not resolve to a collected type. """ return resolve_type(name) def required_type_key(name)-
Resolve a hand-kept table's type spelling to the lowercased canonical .NET
FullNamethat table keys on, raising when the collected metadata carries no such type. Building a table through this at import time is a fail-loud floor: an entry naming a type the current data cannot resolve stops the module from loading rather than going silently unmatched, which is how a stale table used to fail open. A generic type is named by its arity-marked definition (collections.generic.list`1), the only spellingresolve_type()resolves without its type arguments.Expand source code Browse git
def required_type_key(name: str) -> Ps1TypeName: """ Resolve a hand-kept table's type spelling to the lowercased canonical .NET `FullName` that table keys on, raising when the collected metadata carries no such type. Building a table through this at import time is a fail-loud floor: an entry naming a type the current data cannot resolve stops the module from loading rather than going silently unmatched, which is how a stale table used to fail open. A generic type is named by its arity-marked definition (`collections.generic.list` `` `1 ``), the only spelling `resolve_type` resolves without its type arguments. """ resolved = resolve_type(name) if resolved is None: raise ValueError( F'a PowerShell analysis table names {name!r}, which the collected metadata does not ' F'resolve to a type; the data and the table are out of step.' ) return resolved.generic_definition def required_type_keys(names)-
A frozenset of canonical type keys built from readable source spellings through
required_type_key(). Spellings that name the same type collapse to one entry, retiring the dual-spelling entries (intbesideint32) the allow-lists carried before the data could resolve them.Expand source code Browse git
def required_type_keys(names: set[str]) -> frozenset[Ps1TypeName]: """ A frozenset of canonical type keys built from readable source spellings through `required_type_key`. Spellings that name the same type collapse to one entry, retiring the dual-spelling entries (`int` beside `int32`) the allow-lists carried before the data could resolve them. """ return frozenset(required_type_key(name) for name in names) def required_member_keys(entries)-
A frozenset of
(canonical type key, lowercased member)pairs, the form a member-keyed table looks up. Only the type half is resolved through the data and floored by it; the member name is matched against aPs1InvokeMember.memberat its own casing.Expand source code Browse git
def required_member_keys( entries: set[tuple[str, str]], ) -> frozenset[tuple[Ps1TypeName, str]]: """ A frozenset of `(canonical type key, lowercased member)` pairs, the form a member-keyed table looks up. Only the type half is resolved through the data and floored by it; the member name is matched against a `refinery.lib.scripts.ps1.model.Ps1InvokeMember.member` at its own casing. """ return frozenset( (required_type_key(type_name), member.lower()) for type_name, member in entries ) def type_members(name)-
The full member table of a type, keyed by member name, including the fields and Extended Type System members the historical views omit. Each value carries at least
kindandsource. ReturnsNonewhen the type is not collected.Expand source code Browse git
def type_members(name: str | Ps1TypeName) -> dict[str, dict] | None: """ The full member table of a type, keyed by member name, including the fields and Extended Type System members the historical views omit. Each value carries at least `kind` and `source`. Returns `None` when the type is not collected. """ key = _member_surface(name) if key is None: return None record = _type_record(key) return None if record is None else record['members'] def member_order(name)-
The order
Get-Memberdisplays a type's members in, as observed on a real instance, orNonewhen it was not collected for this type. This is the authentic display order, not a synthesis from the member table, and is only present for the types the generator has an instance for.Expand source code Browse git
def member_order(name: str | Ps1TypeName) -> list[str] | None: """ The order `Get-Member` displays a type's members in, as observed on a real instance, or `None` when it was not collected for this type. This is the authentic display order, not a synthesis from the member table, and is only present for the types the generator has an instance for. """ key = _member_surface(name) if key is None: return None record = _type_record(key) return None if record is None else record.get('member_order') def type_is_sealed(name)-
Whether the named type is sealed — no subtype of it exists, so a value of the type carries exactly the members reflection reports and nothing a subtype could add.
Falsewhen the type is not collected or is not sealed, so a caller that needs sealedness to justify a grant fails closed on an unknown type rather than assuming it. The flag is read from the collected metadata, which is what retires the hand-asserted sealedness the effect layer's pure-read allow-list used to rest on.An array type is sealed whatever its element type is — .NET derives no type from one — so it is answered from the array rather than through
_member_surface, which would answer it offSystem.Arrayand call it unsealed.Expand source code Browse git
def type_is_sealed(name: str | Ps1TypeName) -> bool: """ Whether the named type is sealed — no subtype of it exists, so a value of the type carries exactly the members reflection reports and nothing a subtype could add. `False` when the type is not collected or is not sealed, so a caller that needs sealedness to justify a grant fails closed on an unknown type rather than assuming it. The flag is read from the collected metadata, which is what retires the hand-asserted sealedness the effect layer's pure-read allow-list used to rest on. An array type is sealed whatever its element type is — .NET derives no type from one — so it is answered from the array rather than through `_member_surface`, which would answer it off `System.Array` and call it unsealed. """ resolved = resolve_type(name) if resolved is None: return False if resolved.ranks: return True record = _type_record(resolved.definition) return record is not None and bool(record.get('sealed')) def engine_member(member)-
The record for a member the object adapter adds to every value, or
Nonefor a name that is not one. Callers consult this only wheremember_record()answeredMemberLookup.ABSENT: a collected record is what the type really carries and is never overridden by this.Expand source code Browse git
def engine_member(member: str) -> dict | None: """ The record for a member the object adapter adds to every value, or `None` for a name that is not one. Callers consult this only where `member_record` answered `MemberLookup.ABSENT`: a collected record is what the type really carries and is never overridden by this. """ return _ENGINE_MEMBERS.get(member.lower()) def type_names(name)-
The value a type's
PSTypeNamesenumerates: its own reflectionFullNamefollowed by theFullNameof every type it derives from, ending atSystem.Object. Measured on a 5.1 host; seeTYPE_TRANSCRIPTSintest.lib.scripts.ps1.test_oracle.Returns
Nonewhen the type does not resolve, is an array — whose chain isSystem.Arrayand not that of its element type — or reaches a base that was not collected, so a caller folds a chain that is known whole and never a truncated one.Expand source code Browse git
def type_names(name: str | Ps1TypeName) -> list[str] | None: """ The value a type's `PSTypeNames` enumerates: its own reflection `FullName` followed by the `FullName` of every type it derives from, ending at `System.Object`. Measured on a 5.1 host; see `TYPE_TRANSCRIPTS` in `test.lib.scripts.ps1.test_oracle`. Returns `None` when the type does not resolve, is an array — whose chain is `System.Array` and not that of its element type — or reaches a base that was not collected, so a caller folds a chain that is known whole and never a truncated one. """ resolved = resolve_type(name) if resolved is None or resolved.is_array: return None names: list[str] = [] current: str | None = resolved.definition while current is not None: record = _type_record(current) if record is None: return None names.append(current) current = record.get('base') return names def is_assignable_to(value_type, target_type)-
Whether a value whose runtime type is
value_typesatisfiesvalue_type -is target_type, the test PowerShell's-isand-isnotoperators perform.Truewhen the runtime type is the target, derives from it, or implements it;Falsewhen the collected model settles that it is none of those; andNonewhere the model does not settle it — an unresolved or generic type, an array against a different array type, or an array against an interfaceSystem.Arrayis not recorded to carry. ANoneis a fold declined, never aFalseguessed, so a caller never answers a test 5.1 would answer the other way.For a non-array value the class relation is read whole: the base chain
type_names()returns and the interface set the collected record carries are both what reflection reports for the value's own type, so a target that is neither an ancestor class nor a listed interface is a definiteFalse.Expand source code Browse git
def is_assignable_to( value_type: str | Ps1TypeName, target_type: str | Ps1TypeName, ) -> bool | None: """ Whether a value whose runtime type is `value_type` satisfies `value_type -is target_type`, the test PowerShell's `-is` and `-isnot` operators perform. `True` when the runtime type is the target, derives from it, or implements it; `False` when the collected model settles that it is none of those; and `None` where the model does not settle it — an unresolved or generic type, an array against a different array type, or an array against an interface `System.Array` is not recorded to carry. A `None` is a fold declined, never a `False` guessed, so a caller never answers a test 5.1 would answer the other way. For a non-array value the class relation is read whole: the base chain `type_names` returns and the interface set the collected record carries are both what reflection reports for the value's own type, so a target that is neither an ancestor class nor a listed interface is a definite `False`. """ value = resolve_type(value_type) target = resolve_type(target_type) if value is None or target is None: return None if _unmodelled_for_type_test(value) or _unmodelled_for_type_test(target): return None if value.is_array: return True if value == target else _array_is_assignable_to(target) if target.is_array: return False chain = type_names(value) if chain is None: return None if target.definition in chain: return True record = _type_record(value.definition) if record is None: return None return target.definition in record.get('interfaces', ()) def member_record(name, member)-
The collected record for a single member of a type, or a
MemberLookupsentinel explaining why there is none.MemberLookup.UNCOLLECTEDmeans the type has no member table at all;MemberLookup.ABSENTmeans the type is collected and carries no member of that name. The member is matched case-insensitively, as PowerShell resolves it, and the first match wins. The record carries at leastkindandsource, which distinguish a plain reflection property or field from a code-running Extended Type System member.A member the capture holds wins over
engine_member(), which is why the adapter tier is consulted here on the way out rather than by each caller: a caller that asked the tier first would answerCountoff it forSystem.Array, which carries a real one.Expand source code Browse git
def member_record(name: str | Ps1TypeName, member: str) -> dict | MemberLookup: """ The collected record for a single member of a type, or a `MemberLookup` sentinel explaining why there is none. `MemberLookup.UNCOLLECTED` means the type has no member table at all; `MemberLookup.ABSENT` means the type is collected and carries no member of that name. The member is matched case-insensitively, as PowerShell resolves it, and the first match wins. The record carries at least `kind` and `source`, which distinguish a plain reflection property or field from a code-running Extended Type System member. A member the capture holds wins over `engine_member`, which is why the adapter tier is consulted here on the way out rather than by each caller: a caller that asked the tier first would answer `Count` off it for `System.Array`, which carries a real one. """ members = type_members(name) if members is None: return MemberLookup.UNCOLLECTED lower = member.lower() for stored, record in members.items(): if stored.lower() == lower: return record engine = engine_member(member) if engine is not None: return engine return MemberLookup.ABSENT def view_members(name)-
The members of a type that
Get-Memberreports without-Force: the reflected methods and properties, and for a WMI class its properties. Fields and Extended Type System members are withheld, as they are from the historicalTYPE_MEMBERSview this replaces — the difference is that the type is resolved throughresolve_type(), so an array answers offSystem.Arrayand a spelling that is not already the lowercasedFullNameresolves instead of missing.An enum has no members in this sense and its named values stand in for them, which is what the view has always done and what a caller listing what may follow a dot needs.
Expand source code Browse git
def view_members(name: str | Ps1TypeName) -> dict[str, dict] | None: """ The members of a type that `Get-Member` reports without `-Force`: the reflected methods and properties, and for a WMI class its properties. Fields and Extended Type System members are withheld, as they are from the historical `TYPE_MEMBERS` view this replaces — the difference is that the type is resolved through `resolve_type`, so an array answers off `System.Array` and a spelling that is not already the lowercased `FullName` resolves instead of missing. An enum has no members in this sense and its named values stand in for them, which is what the view has always done and what a caller listing what may follow a dot needs. """ key = _member_surface(name) if key is None: return None record = _type_record(key) if record is None: return None if record.get('kind') == 'enum': return {value: {'kind': 'property', 'source': 'enum'} for value in record['enum_values'] or {}} return _view_members(record) def member_names(name)-
The names
view_members()reports, sorted, orNonewhen the type does not resolve.Expand source code Browse git
def member_names(name: str | Ps1TypeName) -> list[str] | None: """ The names `view_members` reports, sorted, or `None` when the type does not resolve. """ members = view_members(name) return None if members is None else sorted(members) def canonical_member(name, member)-
The casing the metadata records for a member, matched case-insensitively as PowerShell resolves it, or
Nonewhen the type or the member does not resolve.Expand source code Browse git
def canonical_member(name: str | Ps1TypeName, member: str) -> str | None: """ The casing the metadata records for a member, matched case-insensitively as PowerShell resolves it, or `None` when the type or the member does not resolve. """ members = view_members(name) if members is None: return None lower = member.lower() for stored in members: if stored.lower() == lower: return stored return None def resolve_member_type(name, member)-
The type a property of a type holds, or
Nonewhen the type does not resolve, carries no such member, or the member is not a property. The answer is a canonicalPs1TypeName, so a chain of reads composes: what$x.Prop.Lengthresolves to is this asked twice.Expand source code Browse git
def resolve_member_type(name: str | Ps1TypeName, member: str) -> Ps1TypeName | None: """ The type a property of a type holds, or `None` when the type does not resolve, carries no such member, or the member is not a property. The answer is a canonical `Ps1TypeName`, so a chain of reads composes: what `$x.Prop.Length` resolves to is this asked twice. """ record = member_record(name, member) if isinstance(record, MemberLookup): return None if record.get('kind') != 'property': return None declared = record.get('type') if not declared: return None return resolve_type(declared) def static_overloads(name, member)-
The static overloads of a method on a type, each a record carrying its
returnsand itsparameters, where every parameter records itsbyref/outdirection, itstypeand itsposition. Returns an empty list when the type is not collected or carries no static method of that name. A caller asks this to reason about a[Type]::Member(...)call, whose reachable surface is the static one.Expand source code Browse git
def static_overloads(name: str | Ps1TypeName, member: str) -> list[dict]: """ The static overloads of a method on a type, each a record carrying its `returns` and its `parameters`, where every parameter records its `byref`/`out` direction, its `type` and its `position`. Returns an empty list when the type is not collected or carries no static method of that name. A caller asks this to reason about a `[Type]::Member(...)` call, whose reachable surface is the static one. """ return _overloads(name, member, static=True) def instance_overloads(name, member)-
The overloads of a method that a value of the type carries, in the same shape
static_overloads()returns. Returns an empty list when the type is not collected or carries no instance method of that name, which is what says a call on a value of it cannot be made at all:System.Charhas aToUpper, every overload of it is static, and([char]65).ToUpper()reportsMethodNotFoundon 5.1 while[char]::ToUpper('a')answers.Expand source code Browse git
def instance_overloads(name: str | Ps1TypeName, member: str) -> list[dict]: """ The overloads of a method that a *value* of the type carries, in the same shape `static_overloads` returns. Returns an empty list when the type is not collected or carries no instance method of that name, which is what says a call on a value of it cannot be made at all: `System.Char` has a `ToUpper`, every overload of it is static, and `([char]65).ToUpper()` reports `MethodNotFound` on 5.1 while `[char]::ToUpper('a')` answers. """ return _overloads(name, member, static=False) def operand_witnesses()-
The expressions each grid cell was measured over, keyed by the type they produce. This is the capture's method rather than its result, and it is published because a cell is a lower bound: a caller deciding how far to trust one has to know what was tried, and a caller that recorded such a decision has to be able to tell that the ground under it moved.
Expand source code Browse git
def operand_witnesses() -> dict[str, tuple[str, ...]]: """ The expressions each grid cell was measured over, keyed by the type they produce. This is the capture's *method* rather than its result, and it is published because a cell is a lower bound: a caller deciding how far to trust one has to know what was tried, and a caller that recorded such a decision has to be able to tell that the ground under it moved. """ return { name: tuple(texts) for name, texts in _OPERATORS['witnesses'].items() } def binary_operators()-
Every operator the binary grid has a table for, lower case as the capture wrote them. Published so that a caller quantifying over the measured operators reads the set rather than carrying one of its own, which would go stale the next time the capture is widened.
A test that writes the set out is keeping a copy, and does so on purpose: it is the ratchet that makes a regeneration fail loudly instead of quietly covering more. This is for the other kind of caller, the one that wants to iterate and does not want to be told the answer.
Expand source code Browse git
def binary_operators() -> frozenset[str]: """ Every operator the binary grid has a table for, lower case as the capture wrote them. Published so that a caller quantifying over the measured operators reads the set rather than carrying one of its own, which would go stale the next time the capture is widened. A test that writes the set out *is* keeping a copy, and does so on purpose: it is the ratchet that makes a regeneration fail loudly instead of quietly covering more. This is for the other kind of caller, the one that wants to iterate and does not want to be told the answer. """ return frozenset(_OPERATORS['binary']) def binary_outcome(operator, left, right)-
What
left <operator> rightwas observed to produce, orNonewhen the grid does not cover the operator or either type. An uncovered cell is not an empty one: a caller must readNoneas nothing is known here and decline, never as this produces nothing.Expand source code Browse git
def binary_outcome( operator: str, left: str | Ps1TypeName, right: str | Ps1TypeName, ) -> OperatorOutcome | None: """ What `left <operator> right` was observed to produce, or `None` when the grid does not cover the operator or either type. An uncovered cell is not an empty one: a caller must read `None` as *nothing is known here* and decline, never as *this produces nothing*. """ rows = _OPERATORS['binary'].get(operator.lower()) if rows is None: return None row = _axis_position(_operand_axis(), left) column = _axis_position(_operand_axis(), right) if row is None or column is None: return None return _outcomes()[rows[row][column]] def unary_outcome(operator, operand)-
What
<operator> operandwas observed to produce, for the operators that take one operand. Read the same way asbinary_outcome().Expand source code Browse git
def unary_outcome( operator: str, operand: str | Ps1TypeName, ) -> OperatorOutcome | None: """ What `<operator> operand` was observed to produce, for the operators that take one operand. Read the same way as `binary_outcome`. """ row = _OPERATORS['unary'].get(operator.lower()) if row is None: return None column = _axis_position(_operand_axis(), operand) return None if column is None else _outcomes()[row[column]] def type_test_outcome(operator, source, target)-
What
source <operator> [target]was observed to produce, for the operators whose right operand is a type rather than a value. Read the same way asbinary_outcome().Expand source code Browse git
def type_test_outcome( operator: str, source: str | Ps1TypeName, target: str | Ps1TypeName, ) -> OperatorOutcome | None: """ What `source <operator> [target]` was observed to produce, for the operators whose right operand is a type rather than a value. Read the same way as `binary_outcome`. """ rows = _OPERATORS['type_tests'].get(operator.lower()) if rows is None: return None row = _axis_position(_operand_axis(), source) column = _axis_position(_target_axis(), target) if row is None or column is None: return None return _outcomes()[rows[row][column]] def conversion_outcome(target, source)-
What casting a value of
sourcetotargetwas observed to produce, orNonewhen the grid does not cover the cast. Read the same way asbinary_outcome().Expand source code Browse git
def conversion_outcome( target: str | Ps1TypeName, source: str | Ps1TypeName, ) -> OperatorOutcome | None: """ What casting a value of `source` to `target` was observed to produce, or `None` when the grid does not cover the cast. Read the same way as `binary_outcome`. """ key = _grid_key(target) row = None if key is None else _conversions().get(key) if row is None: return None column = _axis_position(_operand_axis(), source) return None if column is None else _outcomes()[row[column]] def command(name)-
The collected record for a command, or
Nonewhen the name is not a known cmdlet or function. The record carries the command kind, its module, declared output types with anoutput_type_declaredflag, and the full parameter table including the common parameters. Command names are matched case-insensitively, as PowerShell resolves them.Expand source code Browse git
def command(name: str) -> dict | None: """ The collected record for a command, or `None` when the name is not a known cmdlet or function. The record carries the command kind, its module, declared output types with an `output_type_declared` flag, and the full parameter table including the common parameters. Command names are matched case-insensitively, as PowerShell resolves them. """ return _COMMAND_LOOKUP.get(name.lower()) def command_output_types(name)-
The declared output types of a command, lowercased, or
Nonewhen the command declares none. A set is returned only when the command carries an[OutputType]attribute — theoutput_type_declaredflag — because anoutput_typeslist without it records what was observed, not what the author promised, and an empty one means the declaration was never made rather than that the command emits nothing. An unknown command isNonefor the same reason. Names are matched case-insensitively.Expand source code Browse git
def command_output_types(name: str) -> frozenset[str] | None: """ The declared output types of a command, lowercased, or `None` when the command declares none. A set is returned only when the command carries an `[OutputType]` attribute — the `output_type_declared` flag — because an `output_types` list without it records what was observed, not what the author promised, and an empty one means the declaration was never made rather than that the command emits nothing. An unknown command is `None` for the same reason. Names are matched case-insensitively. """ record = _COMMAND_LOOKUP.get(name.lower()) if record is None or not record.get('output_type_declared'): return None return frozenset(_type.lower() for _type in record['output_types'])
Classes
class MemberLookup (*args, **kwds)-
The two non-record outcomes of
member_record(). A member query has three outcomes a purity gate must keep apart: the type was never collected, so nothing is known about its members; the type is collected but carries no member of that name; or the member is present and its record is returned. Collapsing the first two into a singleNoneconflates an unknown surface with a known absence, which a sound gate must treat oppositely — an unknown surface is unsafe, a known-absent read yields$null.Expand source code Browse git
class MemberLookup(enum.Enum): """ The two non-record outcomes of `member_record`. A member query has three outcomes a purity gate must keep apart: the type was never collected, so nothing is known about its members; the type is collected but carries no member of that name; or the member is present and its record is returned. Collapsing the first two into a single `None` conflates an unknown surface with a known absence, which a sound gate must treat oppositely — an unknown surface is unsafe, a known-absent read yields `$null`. """ UNCOLLECTED = 'uncollected' ABSENT = 'absent'Ancestors
- enum.Enum
Class variables
var UNCOLLECTED-
The type of the None singleton.
var ABSENT-
The type of the None singleton.
class OperatorOutcome (types, may_throw, may_be_null)-
Everything one grid cell was observed to produce, over several witness values per operand type.
A cell is deliberately not a type.
(operator, left type, right type)does not determine one:512MB * 512MBis aDoubleout of the same Int32-by-Int32 cell that gives anInt32elsewhere, and12 + '0xabc'is2760where16 + 'file'throws. Sotypesis a set, and a caller may read a single type out of it only when there is exactly one and neithermay_thrownormay_be_nullis set. Anything wider is decided by the values, and answering it needs a kernel rather than the grid.may_throwis on its own axis rather than being a member oftypesbecause throwing is not an alternative to having a type:[int]$xover a string is an Int32, or it throws, which is the commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing.Expand source code Browse git
class OperatorOutcome(typing.NamedTuple): """ Everything one grid cell was observed to produce, over several witness values per operand type. A cell is deliberately not a type. `(operator, left type, right type)` does not determine one: `512MB * 512MB` is a `Double` out of the same Int32-by-Int32 cell that gives an `Int32` elsewhere, and `12 + '0xabc'` is `2760` where `16 + 'file'` throws. So `types` is a set, and a caller may read a single type out of it only when there is exactly one and neither `may_throw` nor `may_be_null` is set. Anything wider is decided by the values, and answering it needs a kernel rather than the grid. `may_throw` is on its own axis rather than being a member of `types` because throwing is not an alternative to having a type: `[int]$x` over a string is *an Int32, or it throws*, which is the commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing. """ types: frozenset[Ps1TypeName] may_throw: bool may_be_null: bool @property def single_type(self) -> Ps1TypeName | None: """ The one type this cell always produces, or `None` when it does not always produce one. """ if self.may_throw or self.may_be_null or len(self.types) != 1: return None return next(iter(self.types)) @property def always_throws(self) -> bool: """ Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all. **It names no cause, and must not be read as naming one.** 220 of the binary grid's cells answer `True` and at least 26 of those are a *value* reason rather than a missing method: `2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the control that shows it: same operator, same value reason, **not** selected, because `$true` divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host. **`may_throw` alone answers a different question, and reading it as this one is the mistake the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five: `$true * 2` throws with nothing witnessed, while `$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put `/` and `%` into the emulator's refusal set, twice. The claim is about the *cell* and never about one operand. Projecting it onto a side is a separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value. A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it. """ return self.may_throw and not self.may_be_null and not self.typesAncestors
- builtins.tuple
Instance variables
var types-
Alias for field number 0
Expand source code Browse git
class OperatorOutcome(typing.NamedTuple): """ Everything one grid cell was observed to produce, over several witness values per operand type. A cell is deliberately not a type. `(operator, left type, right type)` does not determine one: `512MB * 512MB` is a `Double` out of the same Int32-by-Int32 cell that gives an `Int32` elsewhere, and `12 + '0xabc'` is `2760` where `16 + 'file'` throws. So `types` is a set, and a caller may read a single type out of it only when there is exactly one and neither `may_throw` nor `may_be_null` is set. Anything wider is decided by the values, and answering it needs a kernel rather than the grid. `may_throw` is on its own axis rather than being a member of `types` because throwing is not an alternative to having a type: `[int]$x` over a string is *an Int32, or it throws*, which is the commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing. """ types: frozenset[Ps1TypeName] may_throw: bool may_be_null: bool @property def single_type(self) -> Ps1TypeName | None: """ The one type this cell always produces, or `None` when it does not always produce one. """ if self.may_throw or self.may_be_null or len(self.types) != 1: return None return next(iter(self.types)) @property def always_throws(self) -> bool: """ Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all. **It names no cause, and must not be read as naming one.** 220 of the binary grid's cells answer `True` and at least 26 of those are a *value* reason rather than a missing method: `2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the control that shows it: same operator, same value reason, **not** selected, because `$true` divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host. **`may_throw` alone answers a different question, and reading it as this one is the mistake the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five: `$true * 2` throws with nothing witnessed, while `$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put `/` and `%` into the emulator's refusal set, twice. The claim is about the *cell* and never about one operand. Projecting it onto a side is a separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value. A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it. """ return self.may_throw and not self.may_be_null and not self.types var may_throw-
Alias for field number 1
Expand source code Browse git
class OperatorOutcome(typing.NamedTuple): """ Everything one grid cell was observed to produce, over several witness values per operand type. A cell is deliberately not a type. `(operator, left type, right type)` does not determine one: `512MB * 512MB` is a `Double` out of the same Int32-by-Int32 cell that gives an `Int32` elsewhere, and `12 + '0xabc'` is `2760` where `16 + 'file'` throws. So `types` is a set, and a caller may read a single type out of it only when there is exactly one and neither `may_throw` nor `may_be_null` is set. Anything wider is decided by the values, and answering it needs a kernel rather than the grid. `may_throw` is on its own axis rather than being a member of `types` because throwing is not an alternative to having a type: `[int]$x` over a string is *an Int32, or it throws*, which is the commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing. """ types: frozenset[Ps1TypeName] may_throw: bool may_be_null: bool @property def single_type(self) -> Ps1TypeName | None: """ The one type this cell always produces, or `None` when it does not always produce one. """ if self.may_throw or self.may_be_null or len(self.types) != 1: return None return next(iter(self.types)) @property def always_throws(self) -> bool: """ Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all. **It names no cause, and must not be read as naming one.** 220 of the binary grid's cells answer `True` and at least 26 of those are a *value* reason rather than a missing method: `2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the control that shows it: same operator, same value reason, **not** selected, because `$true` divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host. **`may_throw` alone answers a different question, and reading it as this one is the mistake the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five: `$true * 2` throws with nothing witnessed, while `$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put `/` and `%` into the emulator's refusal set, twice. The claim is about the *cell* and never about one operand. Projecting it onto a side is a separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value. A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it. """ return self.may_throw and not self.may_be_null and not self.types var may_be_null-
Alias for field number 2
Expand source code Browse git
class OperatorOutcome(typing.NamedTuple): """ Everything one grid cell was observed to produce, over several witness values per operand type. A cell is deliberately not a type. `(operator, left type, right type)` does not determine one: `512MB * 512MB` is a `Double` out of the same Int32-by-Int32 cell that gives an `Int32` elsewhere, and `12 + '0xabc'` is `2760` where `16 + 'file'` throws. So `types` is a set, and a caller may read a single type out of it only when there is exactly one and neither `may_throw` nor `may_be_null` is set. Anything wider is decided by the values, and answering it needs a kernel rather than the grid. `may_throw` is on its own axis rather than being a member of `types` because throwing is not an alternative to having a type: `[int]$x` over a string is *an Int32, or it throws*, which is the commonest shape in a byte-decoder loader, and a sum would have to answer that it knows nothing. """ types: frozenset[Ps1TypeName] may_throw: bool may_be_null: bool @property def single_type(self) -> Ps1TypeName | None: """ The one type this cell always produces, or `None` when it does not always produce one. """ if self.may_throw or self.may_be_null or len(self.types) != 1: return None return next(iter(self.types)) @property def always_throws(self) -> bool: """ Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all. **It names no cause, and must not be read as naming one.** 220 of the binary grid's cells answer `True` and at least 26 of those are a *value* reason rather than a missing method: `2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the control that shows it: same operator, same value reason, **not** selected, because `$true` divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host. **`may_throw` alone answers a different question, and reading it as this one is the mistake the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five: `$true * 2` throws with nothing witnessed, while `$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put `/` and `%` into the emulator's refusal set, twice. The claim is about the *cell* and never about one operand. Projecting it onto a side is a separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value. A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it. """ return self.may_throw and not self.may_be_null and not self.types var single_type-
The one type this cell always produces, or
Nonewhen it does not always produce one.Expand source code Browse git
@property def single_type(self) -> Ps1TypeName | None: """ The one type this cell always produces, or `None` when it does not always produce one. """ if self.may_throw or self.may_be_null or len(self.types) != 1: return None return next(iter(self.types)) var always_throws-
Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all.
It names no cause, and must not be read as naming one. 220 of the binary grid's cells answer
Trueand at least 26 of those are a value reason rather than a missing method:2 / $nullisAttempted to divide by zero, and division has a perfectly good method for an Int32. They fill the cell only becauseSystem.Voidhas exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type.Int32 / Booleanis the control that shows it: same operator, same value reason, not selected, because$truedivides fine and leavestypesnon-empty. A caller wanting to know why asks the host.may_throwalone answers a different question, and reading it as this one is the mistake the grid invites. Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five:$true * 2throws with nothing witnessed, while$true / 2is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put/and%into the emulator's refusal set, twice.The claim is about the cell and never about one operand. Projecting it onto a side is a separate step needing its own evidence —
_NO_OPERATOR_METHOD_ON_BOOLEANdoes exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value.A cell that produced
$nulldid produce something, somay_be_nullexcludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it.Expand source code Browse git
@property def always_throws(self) -> bool: """ Whether every witnessed pair in this cell threw, so that no value was observed to come out of it at all. **It names no cause, and must not be read as naming one.** 220 of the binary grid's cells answer `True` and at least 26 of those are a *value* reason rather than a missing method: `2 / $null` is `Attempted to divide by zero`, and division has a perfectly good method for an Int32. They fill the cell only because `System.Void` has exactly one inhabitant, so every witnessed pair threw and nothing survived to be a type. `Int32 / Boolean` is the control that shows it: same operator, same value reason, **not** selected, because `$true` divides fine and leaves `types` non-empty. A caller wanting to know *why* asks the host. **`may_throw` alone answers a different question, and reading it as this one is the mistake the grid invites.** Only five of the ten Boolean-left cells throw at all, and the split that matters is inside those five: `$true * 2` throws with nothing witnessed, while `$true / 2` is 0.5 and throws only over a divisor the left operand has no part in. Reading the throw axis on its own put `/` and `%` into the emulator's refusal set, twice. The claim is about the *cell* and never about one operand. Projecting it onto a side is a separate step needing its own evidence — `_NO_OPERATOR_METHOD_ON_BOOLEAN` does exactly that and is sound only because its three members were measured operand-wise against a host, and because it is used to refuse, where a wrong projection costs a fold and cannot invent a value. A cell that produced `$null` did produce something, so `may_be_null` excludes it. No cell of either grid carries that shape today, which makes the clause inert rather than idle: it is what the sentence above means, and a capture that ever recorded one would need it. """ return self.may_throw and not self.may_be_null and not self.types