Module refinery.lib.scripts.ps1.analysis.values

What a PowerShell expression evaluates to and what type that value carries: the value when the source pins it, the .NET type when the static surface determines it, and nothing known when this module cannot say. These are language semantics rather than deobfuscation policy — the truth value of '' and the type of 'abc' are properties of PowerShell, not of any pass — so they sit in the analysis layer where both the effect substrate and the transforms can read them without either importing the other. This is the only module in refinery.lib.scripts.ps1.analysis that answers either question.

A value and its type are one fact, not two. Ps1Fact is that fact, and it is what makes a Char and a one-character String different things: both carry the Python string 'A', and only the type they are stamped with tells them apart. Nothing here dispatches on the Python type of a payload — the Ps1TypeName decides, always, because the Python type is the erasure this module exists to undo. The four elements are nothing is known, $null, a value of this type, and this exact value of this type. When an interval or a known-bits refinement is built it becomes a field on Ps1Typed, not a fifth element.

Throwing is a separate axis, which is why an operation answers a Ps1Outcome rather than a fact: [int] $s over a String is an Int32, or it throws, and a domain that had to fold that into one element could only answer that it knows nothing. may_throw is False only where this module claims an operation cannot throw; not knowing is Ps1Outcome(True, UNKNOWN).

read() is what the source pins an expression to, convert() a cast, apply() an operator and render() the way back, and each of those answers about one step. evaluate() is the composition over a whole expression and the entry a caller with a tree wants: it refuses wherever a step does, and it never answers something a step would have answered differently. It is also the one thing here that keeps state — what it last answered for a node, discarded as soon as any tree is mutated, so that being the entry a caller with a tree wants does not cost that caller a walk per node. Nothing else here remembers anything, and remembering does not make evaluate() answer differently; see there for why.

The type side has two views over one engine. resolve_expression_type() is the single-type core: one expression, one Ps1TypeName or None. candidate_types() is the set-valued view the effect layer reasons over, and it is a strict superset — it additionally resolves a static method call, a cmdlet whose declared output is closed, and the $_ such a cmdlet binds downstream of it, any of which can name several types. The set is the primitive and the single type the derived view, because a caller reasoning about a value must have its conclusion hold for every type the value could carry.

Expand source code Browse git
"""
What a PowerShell expression evaluates to and what type that value carries: the value when the
source pins it, the .NET type when the static surface determines it, and *nothing known* when this
module cannot say. These are language semantics rather than deobfuscation policy — the truth value
of `''` and the type of `'abc'` are properties of PowerShell, not of any pass — so they sit in the
analysis layer where both the effect substrate and the transforms can read them without either
importing the other. This is the only module in `refinery.lib.scripts.ps1.analysis` that answers
either question.

**A value and its type are one fact, not two.** `Ps1Fact` is that fact, and it is what makes a Char
and a one-character String different things: both carry the Python string `'A'`, and only the type
they are stamped with tells them apart. Nothing here dispatches on the Python type of a payload —
the `Ps1TypeName` decides, always, because the Python type is the erasure this module exists to
undo. The four elements are *nothing is known*, *`$null`*, *a value of this type*, and *this exact
value of this type*. When an interval or a known-bits refinement is built it becomes a field on
`Ps1Typed`, not a fifth element.

Throwing is a separate axis, which is why an operation answers a `Ps1Outcome` rather than a fact:
`[int] $s` over a String is *an Int32, or it throws*, and a domain that had to fold that into one
element could only answer that it knows nothing. `may_throw` is `False` only where this module
claims an operation cannot throw; not knowing is `Ps1Outcome(True, UNKNOWN)`.

`read` is what the source pins an expression to, `convert` a cast, `apply` an operator and `render`
the way back, and each of those answers about one step. `evaluate` is the composition over a whole
expression and the entry a caller with a tree wants: it refuses wherever a step does, and it never
answers something a step would have answered differently. It is also the one thing here that keeps
state — what it last answered for a node, discarded as soon as any tree is mutated, so that being
the entry a caller with a tree wants does not cost that caller a walk per node. Nothing else here
remembers anything, and remembering does not make `evaluate` answer differently; see there for why.

The type side has two views over one engine. `resolve_expression_type` is the single-type core: one
expression, one `refinery.lib.scripts.ps1.dotnet.Ps1TypeName` or `None`. `candidate_types` is the
set-valued view the effect layer reasons over, and it is a strict superset — it additionally
resolves a static method call, a cmdlet whose declared output is closed, and the `$_` such a cmdlet
binds downstream of it, any of which can name several types. The set is the primitive and the single type the derived view, because a caller
reasoning about a value must have its conclusion hold for every type the value could carry.
"""
from __future__ import annotations

import dataclasses
import decimal
import functools
import math
import operator as operator_module
import re
import typing

from typing import Callable, TypeAlias, TypeVar
from weakref import WeakKeyDictionary

from refinery.lib.scripts import Node, _clone_node, mutation_epoch
from refinery.lib.scripts.ps1.ast import (
    extract_first_positional_string,
    get_command_name,
    get_member_name,
    is_builtin_variable,
    unwrap_parens,
)
from refinery.lib.scripts.ps1.analysis.blocks import binds_the_pipeline_variable
from refinery.lib.scripts.ps1.analysis.worldflow import Ps1WorldReach
from refinery.lib.scripts.ps1.data import (
    OBJ_COMMANDS,
    TYPE_ARG_COMMANDS,
    VARIABLE_TYPES,
    WMI_COMMANDS,
    binary_outcome,
    command_output_types,
    conversion_outcome,
    is_assignable_to,
    named_type,
    operand_witnesses,
    resolve_member_type,
    resolve_type,
    static_overloads,
    unary_outcome,
)
from refinery.lib.scripts.ps1.dotnet import Ps1TypeName
from refinery.lib.scripts.ps1.model import (
    MULTIPLIERS,
    Expression,
    Ps1AccessKind,
    Ps1ArrayExpression,
    Ps1ArrayLiteral,
    Ps1BinaryExpression,
    Ps1CastExpression,
    Ps1CommandInvocation,
    Ps1ExpandableHereString,
    Ps1ExpandableString,
    Ps1ExpressionStatement,
    Ps1HereString,
    Ps1IntegerLiteral,
    Ps1InvokeMember,
    Ps1MemberAccess,
    Ps1ParenExpression,
    Ps1Pipeline,
    Ps1PipelineElement,
    Ps1RealLiteral,
    Ps1ScopeModifier,
    Ps1ScriptBlock,
    Ps1StringLiteral,
    Ps1SubExpression,
    Ps1TypeExpression,
    Ps1UnaryExpression,
    Ps1Variable,
)
from refinery.lib.scripts.ps1.token import BACKTICK_ENCODE

#: What a caller knows about the type of a variable *occurrence*. A function and not a table keyed
#: by name, because a name is not a variable: two bodies may write the same name, and which write a
#: read observes is a question only the caller's flow model can answer.
#: `refinery.lib.scripts.ps1.analysis.variable_types.type_at` is the one implementation of it, and
#: it is what keeps this module free of flow while still letting a pass answer per occurrence.
Ps1VariableTyping: TypeAlias = Callable[['Ps1Variable'], 'Ps1TypeName | None']

_T = TypeVar('_T')

#: The characters a literal cannot carry verbatim, which is every one the backtick table escapes
#: except the newline: a newline is what a here-string exists to hold.
_NONPRINT_CONTROL = frozenset(BACKTICK_ENCODE) - {'\n'}


def is_truthy(node: Node | None) -> bool | None:
    """
    Whether an expression counts as true to PowerShell, or `None` where this cannot say.

    This is 5.1's conversion to a `Boolean` and nothing else, which is the one predicate `if`,
    `while`, `for`, `do`, `-and`, `-or`, `-xor`, `-not` and a `[bool]` cast all reach. It is
    answered by reading the value the expression names and converting *that*, rather than by a rule
    spelled out here, so that a spelling this module has never seen is refused instead of being
    given a truth of its own invention.

    A rule spelled out here is what this used to be, and `- '0'` is the case that ended it: a minus
    sign in front of a String converts the String to a number, so the text `'0'` is true while the
    number it negates to is false. The rule read the minus as leaving truth alone — which holds for
    a number and for nothing else — and answered `True` where a host answers `False`.

    **Both throws are refused, and they are two.** `evaluate` says whether reaching the value may
    throw and `convert` says whether making a Boolean of it may, and the second does not carry the
    first: `$null * [int]'abc'` evaluates to a definite `$null` that may throw, and converting that
    `$null` alone is a `$False` that cannot. Reading only the conversion would report a truth for an
    expression a host never finishes, and a caller that prunes on it would delete the throw.
    """
    if node is None:
        return None
    outcome = evaluate(node, None)
    if outcome.may_throw or outcome.value is UNKNOWN:
        return None
    converted = convert(outcome.value, _BOOLEAN)
    if converted.may_throw or not isinstance(converted.value, Ps1Constant):
        return None
    return converted.value.payload if isinstance(converted.value.payload, bool) else None


def unwrap_to_array_literal(node: Node) -> Ps1ArrayLiteral | None:
    """
    Unwrap parentheses and array expressions to find an inner
    `refinery.lib.scripts.ps1.model.Ps1ArrayLiteral`.
    """
    node = unwrap_parens(node)
    if isinstance(node, Ps1ArrayLiteral):
        return node
    if isinstance(node, Ps1ArrayExpression) and len(node.body) == 1:
        stmt = node.body[0]
        if isinstance(stmt, Ps1ExpressionStatement) and isinstance(stmt.expression, Ps1ArrayLiteral):
            return stmt.expression
    return None


def collect_facts(node: Node | None) -> list[Ps1Fact] | None:
    """
    The values an expression names, as facts, or `None` where it names anything else. A scalar is a
    list of one, which is what a caller reading a command's or an operator's operand wants:
    PowerShell hands one value and a collection of one to the same place.

    This is the only place the elements of a collection are taken apart, and every caller that wants
    something *of* each of them — an integer, a text, a number-or-text — asks the element that
    question itself. A collector per question would each have to state again which spellings build a
    collection, and `read` already knows: `@(1, 2)`, `(1, 2)`, `1, 2` and a cast over any of them
    are one answer here.
    """
    fact = read(node)
    if isinstance(fact, Ps1Constant) and fact.type == _OBJECT_ARRAY:
        payload = fact.payload
        return None if not isinstance(payload, tuple) else list(payload)
    return None if fact is UNKNOWN else [fact]


def collect_integers(node: Node | None) -> list[int] | None:
    """
    The integers an expression names, as a list, or `None` where it names anything else.

    What counts as an integer is `integer_of`, so a numeral whose spelling makes it something else
    is not one and neither is a `$null`: the old reader here answered the *magnitude* of a
    hexadecimal pattern, so `0xFFFFFFFF` reached its callers as 4294967295 where the value is -1.
    """
    return _each(collect_facts(node), integer_of)


def collect_texts(node: Node | None) -> list[str] | None:
    """
    The texts an expression's values contribute where PowerShell coerces each of them to a String,
    or `None` where one of them names no text. See `coerced_text` for what that coercion is and
    which operators perform it.
    """
    return _each(collect_facts(node), coerced_text)


def _each(facts: list[Ps1Fact] | None, of: Callable[[Ps1Fact], _T | None]) -> list[_T] | None:
    """
    What each of `facts` answers to `of`, or `None` where any one of them answers nothing. One
    element the caller cannot read makes the whole collection unreadable: a shorter list than the
    script builds is a different value, and there is nothing to stand in for the element dropped.
    """
    if facts is None:
        return None
    answers: list[_T] = []
    for fact in facts:
        answer = of(fact)
        if answer is None:
            return None
        answers.append(answer)
    return answers


def collect_byte_array(node: Expression) -> bytes | None:
    """
    The bytes an expression names, or `None` where it names something that is not a list of them.
    A number outside a byte is not one, which is a refusal rather than a truncation.
    """
    numbers = collect_integers(node)
    if numbers is None:
        return None
    try:
        return bytes(numbers)
    except (ValueError, OverflowError):
        return None


#: A type this module names, resolved through the one resolver rather than spelled here — a name
#: written out as text would be a second vocabulary inside the module whose purpose is to have one.
#: A name the table does not resolve raises at import, which is what `named_type` is for: every
#: answer below is keyed by the result, so a missing row would not move an answer, it would make
#: every comparison silently false.
_type = named_type


#: The type every string literal has, and the narrowest one a numeral can. A numeral's is decided by
#: its spelling and so is read rather than named here; `_INT32` is where the numeric ladder starts
#: and what a shift is masked at, not what an integer literal is.
_STRING = _type('System.String')
_INT32 = _type('System.Int32')

#: What an array literal builds. PowerShell collects the elements into an `Object[]` whatever they
#: are, which is measured rather than assumed: an array of integers and an array of strings both
#: report `System.Object[]`. The rank is what makes a member read on one resolve against
#: `System.Array`, which is where an array's members actually live.
_OBJECT_ARRAY = _type('System.Object[]')


def resolve_expression_type(
    expr: Expression,
    type_of_variable: Ps1VariableTyping | None = None,
) -> Ps1TypeName | None:
    """
    Trace the .NET type of a PowerShell expression by walking member access chains. Returns the one
    canonical `Ps1TypeName`, or `None` if the type cannot be determined.

    A numeral is asked of `read` rather than answered here, because how wide a numeral is written
    decides its type and only the spelling knows: `1L` is an Int64, `2147483648` is an Int64,
    `9223372036854775808` is a Decimal and `1e32` a Double, every one of them measured. Answering
    `System.Int32` for all of them resolved a member against a type the value did not have.
    """
    unwrapped = unwrap_parens(expr)
    if not isinstance(unwrapped, Expression):
        return None
    expr = unwrapped
    if isinstance(expr, (Ps1StringLiteral, Ps1HereString)):
        return _STRING
    if isinstance(expr, (Ps1IntegerLiteral, Ps1RealLiteral)):
        return type_of(read(expr))
    if isinstance(expr, Ps1ArrayLiteral):
        return _OBJECT_ARRAY
    if isinstance(expr, Ps1ArrayExpression):
        if (
            len(expr.body) == 1
            and isinstance(expr.body[0], Ps1ExpressionStatement)
            and isinstance(expr.body[0].expression, Ps1ArrayLiteral)
        ):
            return _OBJECT_ARRAY
    if isinstance(expr, Ps1Variable):
        if type_of_variable is not None:
            named = type_of_variable(expr)
            if named is not None:
                return named
        declared = VARIABLE_TYPES.get(expr.name.lower())
        return None if declared is None else resolve_type(declared)
    if isinstance(expr, Ps1TypeExpression):
        return resolve_type(expr.name)
    if isinstance(expr, Ps1CastExpression):
        return resolve_type(expr.type_name)
    if isinstance(expr, Ps1CommandInvocation):
        cmd_name = get_command_name(expr)
        if cmd_name is not None:
            cmd_lower = cmd_name.lower()
            if cmd_lower in OBJ_COMMANDS:
                type_str = extract_first_positional_string(expr)
                if type_str is not None:
                    return resolve_type(type_str)
            elif cmd_lower in WMI_COMMANDS:
                class_str = extract_first_positional_string(expr)
                if class_str is not None:
                    return resolve_type(class_str)
    if isinstance(expr, Ps1MemberAccess):
        if expr.object is None:
            return None
        obj_type = resolve_expression_type(expr.object, type_of_variable)
        if obj_type is None:
            return None
        member_name = get_member_name(expr.member)
        if member_name is None:
            return None
        return resolve_member_type(obj_type, member_name)
    return None


#: Commands whose declared `[OutputType]` is a trustworthy *superset* of what they emit at runtime,
#: not merely a lower bound. Most commands under-declare: one that forwards its input emits the
#: input's type, which it never lists — `Get-Random -InputObject $procs` returns a `Process`,
#: `Get-Content` on a non-filesystem provider returns whatever that provider yields — so trusting
#: the declaration lets the member gate prove `(...).Path` pure over an incomplete candidate set and
#: delete a live effect. Only commands that emit their own output and cannot pass input through
#: belong here; a read on any other command's result stays unresolved, and therefore kept.
_CLOSED_OUTPUT_CMDLETS = frozenset({
    'get-date',
    'measure-object',
})


#: The names the pipeline binds the current object to. `$PSItem` is the same variable spelled out,
#: and a script that uses one spelling to defeat a rule written over the other is the reason both
#: are listed here rather than only the short one.
_PIPELINE_VARIABLES = frozenset({'_', 'psitem'})


def _is_pipeline_variable(node) -> bool:
    """
    Whether *node* reads the current pipeline object. A splatted or scope-qualified spelling is not
    one: neither reads the automatic variable the pipeline binds.
    """
    return (
        isinstance(node, Ps1Variable)
        and not node.splatted
        and node.scope is Ps1ScopeModifier.NONE
        and node.name.lower() in _PIPELINE_VARIABLES
    )


def _pipeline_variable_candidates(
    node: Ps1Variable,
    world: Ps1WorldReach,
    type_of_variable: Ps1VariableTyping | None,
) -> frozenset[Ps1TypeName]:
    """
    The types the current pipeline object may carry where *node* reads it: the output types of the
    command feeding the element whose body *node* stands in.

    **Only a command upstream answers.** A cmdlet's declared output describes what it writes to the
    pipeline *per object*, which is already the type `$_` is bound to. Every other expression is a
    value the pipeline enumerates, and its type is the type of the whole — `@(1, 2) | ForEach-Object
    { $_ }` binds an `Int32` where the array is an `Int32[]` — so an upstream that is not a command
    contributes nothing rather than the collection's type.

    The block has to be one the command runs once per input object, which is what binds the variable
    at all; `refinery.lib.scripts.ps1.analysis.blocks.binds_the_pipeline_variable` decides it. A read
    in a `-Begin` body, or in a block written where the enclosing scope's `$_` is what is read, is
    refused there and answers nothing here.
    """
    block = node.parent
    while block is not None and not isinstance(block, Ps1ScriptBlock):
        block = block.parent
    if block is None:
        return frozenset()
    command = binds_the_pipeline_variable(block, world.shadowed_names)
    if command is None:
        return frozenset()
    element = command.parent
    if not isinstance(element, Ps1PipelineElement):
        return frozenset()
    pipeline = element.parent
    if not isinstance(pipeline, Ps1Pipeline):
        return frozenset()
    upstream = None
    for candidate in pipeline.elements:
        if candidate is element:
            break
        upstream = candidate
    if upstream is None or not isinstance(upstream.expression, Ps1CommandInvocation):
        return frozenset()
    return _command_candidates(upstream.expression, world, type_of_variable)


def candidate_types(
    expr: Expression,
    world: Ps1WorldReach,
    type_of_variable: Ps1VariableTyping | None = None,
) -> frozenset[Ps1TypeName]:
    """
    The set of canonical .NET type names the expression's value could have, or the empty set when
    the type cannot be determined. A static method call contributes the return its overloads agree
    on, a cmdlet call the output types it declares, and `$_` the output types of whatever feeds the
    pipeline element it is bound in; each can be several, so a caller reasoning about the value must
    have its conclusion hold for every candidate. The single-type forms — literals, variables,
    casts, `New-Object`, WMI, and property chains — are delegated to `resolve_expression_type`
    rather than re-derived here.

    `world` is what decides whether a command name still denotes what the metadata says, so a cmdlet
    whose name the script has taken over contributes nothing. It is asked at the position of the
    command it resolves rather than over the whole run, so a name some later statement rebinds still
    denotes the built-in everywhere no path places that statement first.
    """
    unwrapped = unwrap_parens(expr)
    if not isinstance(unwrapped, Expression):
        return frozenset()
    expr = unwrapped
    if isinstance(expr, Ps1InvokeMember):
        return _static_method_candidates(expr)
    if isinstance(expr, Ps1CommandInvocation):
        return _command_candidates(expr, world, type_of_variable)
    if _is_pipeline_variable(expr):
        return _pipeline_variable_candidates(expr, world, type_of_variable)
    single = resolve_expression_type(expr, type_of_variable)
    return frozenset() if single is None else frozenset({single})


def _static_method_candidates(node: Ps1InvokeMember) -> frozenset[Ps1TypeName]:
    """
    The return type of a `[Type]::Method(...)` call, taken only when every matching static overload
    agrees on it; disagreement or an unrecognized call is the empty set. An instance method call is
    not resolved — its receiver type would have to be traced and its overloads selected by argument
    type — so it contributes nothing rather than a guess.
    """
    if node.access is not Ps1AccessKind.STATIC:
        return frozenset()
    obj = node.object
    member = node.member
    if not isinstance(obj, Ps1TypeExpression) or not isinstance(member, str):
        return frozenset()
    returns = {
        resolve_type(overload['returns'])
        for overload in static_overloads(obj.name, member)
        if overload.get('returns')
    }
    if len(returns) != 1:
        return frozenset()
    single = next(iter(returns))
    return frozenset() if single is None else frozenset({single})


def _command_candidates(
    cmd: Ps1CommandInvocation,
    world: Ps1WorldReach,
    type_of_variable: Ps1VariableTyping | None,
) -> frozenset[Ps1TypeName]:
    """
    The types a command's result could have: the constructed or queried type for the `New-Object`
    and WMI forms the single-type ladder already knows, otherwise the output types a command
    declares through `[OutputType]` — but only for a command whose declaration is a trustworthy
    *superset* of what it emits (`_CLOSED_OUTPUT_CMDLETS`). `[OutputType]` is a lower bound in
    general: a command that forwards its input emits types it never declares, and trusting the
    declaration there would let the member gate prove an effectful read pure over an incomplete
    candidate set. Every other command contributes nothing, so a read on its result stays
    unresolved and is kept.

    The name is trusted by position (`may_trust_command_name_at`), which grants wherever the
    whole-run verdict grants *and* wherever neither flood reaches — a widening, not a narrowing.
    It is sound because the two floods bound every position at which a rebinding could have run,
    and because what is granted here is a proof about a known built-in rather than a guess about
    an artifact: relaxing such a proof by position only widens what it already covers.
    `refinery.lib.scripts.ps1.deobfuscation.deadcode._is_injected_noise_bareword` reads the same
    query for a guess rather than a proof, and states there what that costs; the soundness argument
    above is this site's own and does not carry over to it.
    """
    name = get_command_name(cmd)
    if name is None:
        return frozenset()
    lower = name.lower()
    if not world.may_trust_command_name_at(lower, cmd):
        return frozenset()
    if lower in TYPE_ARG_COMMANDS:
        single = resolve_expression_type(cmd, type_of_variable)
        return frozenset() if single is None else frozenset({single})
    if lower not in _CLOSED_OUTPUT_CMDLETS:
        return frozenset()
    declared = command_output_types(name)
    if declared is None:
        return frozenset()
    resolved = {resolve_type(one) for one in declared}
    return frozenset(one for one in resolved if one is not None)


#: The types the domain names. Resolved once through the one resolver, so that a fact carries the
#: same `Ps1TypeName` a member lookup or a grid cell is keyed by and the two can be compared.
_BYTE = _type('System.Byte')
_BOOLEAN = _type('System.Boolean')
_CHAR = _type('System.Char')
_DECIMAL = _type('System.Decimal')
_DOUBLE = _type('System.Double')
_INT16 = _type('System.Int16')
_INT64 = _type('System.Int64')
_SBYTE = _type('System.SByte')
_UINT16 = _type('System.UInt16')
_UINT32 = _type('System.UInt32')
_UINT64 = _type('System.UInt64')

#: The widths the numeric ladder is written in terms of. `System.Decimal`'s bound is its documented
#: maximum rather than a power of two, because its range is not a bit width.
#:
#: Its lower bound is written down rather than spelled `-_DECIMAL_MAX`, because that expression is
#: not the number it reads as: arithmetic on a `Decimal` is a *context* operation in Python and
#: rounds to the ambient precision, which is 28 digits where this value has 29. Every range test
#: written that way was comparing against `-79228162514264337593543950340` — a bound wider than the
#: type has, and one that moved whenever anything in the process set `decimal.getcontext().prec`.
_INT32_RANGE = (-0x80000000, 0x7FFFFFFF)
_INT64_RANGE = (-0x8000000000000000, 0x7FFFFFFFFFFFFFFF)
_DECIMAL_MAX = decimal.Decimal('79228162514264337593543950335')
_DECIMAL_MIN = decimal.Decimal('-79228162514264337593543950335')


class Ps1Fact:
    """
    What is known about one PowerShell value: nothing (`UNKNOWN`), that it is `$null` (`NULL`), that
    it has a type (`Ps1Typed`), or that it is a particular value of a particular type
    (`Ps1Constant`). These are the four elements of the lattice every question in this module is
    answered in, ordered `Ps1Constant` below `Ps1Typed` below `UNKNOWN`, with `NULL` beside the
    typed ones rather than under them: `$null.GetType()` throws, so there is no type it could carry.

    The base is a marker and carries no accessor: `type_of` is where a fact's type is read, so that
    the one place a caller asks the question is a function it can be pointed at, and an element that
    has no type does not have to pretend to answer.
    """

    __slots__ = ()


@dataclasses.dataclass(frozen=True)
class _Ps1Unknown(Ps1Fact):
    """
    Nothing is known about the value. This is the answer to every question this module declines,
    and it never means *the value is absent* — that is `NULL`.
    """

    def __repr__(self) -> str:
        return 'UNKNOWN'


@dataclasses.dataclass(frozen=True)
class _Ps1Null(Ps1Fact):
    """
    The value is `$null`. Its own element rather than a `Ps1Constant` of some type, because it has
    no type to be constant *of*: reading `GetType()` off it throws.
    """

    def __repr__(self) -> str:
        return 'NULL'


UNKNOWN: Ps1Fact = _Ps1Unknown()
NULL: Ps1Fact = _Ps1Null()


def null_expression() -> Ps1Variable:
    """
    The expression that spells `$null`. This is `render(NULL)`, given its own name and a precise
    return type so a caller building a node out of it does not carry render's `Expression | None`.
    """
    return Ps1Variable(name='Null')


@dataclasses.dataclass(frozen=True)
class Ps1Typed(Ps1Fact):
    """
    The value has this type and no more is known about it. A refinement — an interval, a known-bits
    mask — becomes a field here when one is built, so that narrowing what a typed value can be does
    not add an element to the lattice or a case to any caller.
    """

    type: Ps1TypeName

    def __repr__(self) -> str:
        return F'Typed({self.type})'


@dataclasses.dataclass(frozen=True)
class Ps1Constant(Ps1Fact):
    """
    The value is exactly `payload`, and its type is `type`. The payload's Python type is an
    implementation of the .NET one and never a substitute for it: `Ps1Constant(System.Char, 'A')`
    and `Ps1Constant(System.String, 'A')` hold equal payloads and are different values, which is the
    distinction this whole layer exists to keep. A caller deciding what a payload means reads
    `type`.

    An array's payload is a tuple of facts rather than of payloads, so that an `Object[]` whose
    elements are Chars is a different value from one whose elements are Strings — the fact a
    pipeline builds and the erasure that made `foreach` iterate once over a joined string.
    """

    type: Ps1TypeName
    payload: int | float | decimal.Decimal | str | bool | tuple[Ps1Fact, ...]

    def __repr__(self) -> str:
        return F'Constant({self.type}, {self.payload!r})'


class Ps1Outcome(typing.NamedTuple):
    """
    What an operation does: the fact it produces, and whether it may instead throw. The two are
    separate because they are not alternatives — an operation that yields an Int32 *or* throws is
    both, and a domain that had to choose could only answer `UNKNOWN` and lose the type it knows.

    Both fields are read in the same direction, which is what makes the two of them one answer:
    `may_throw` is `False` only where this module claims the operation *cannot* throw, exactly as
    `UNKNOWN` is the value of one that names none. Not knowing anything is therefore
    `Ps1Outcome(True, UNKNOWN)` and not `Ps1Outcome(False, UNKNOWN)` — the latter is a claim of
    safety made by the one answer that has no grounds for any claim. It made generalising an operand
    *remove* a throw: `1 / $x` for a divisor this module could not type answered that it cannot
    throw, where the same division over a divisor it could type answered that it can. Only
    `render` refusing to spell an `UNKNOWN` kept that out of a fold, which is a guard that holds one
    operation deep and no further.

    An operation known to throw and one this module declines to judge are the same outcome here,
    which is what *may* means. Telling them apart would want a consumer that acts on a certain
    throw, and there is none: the reader of this axis folds, and both answers stop it.
    """

    may_throw: bool
    value: Ps1Fact


#: The refusal, named once so that the several places that decline read alike. It claims nothing on
#: either axis — no value, and no freedom from a throw.
NOTHING = Ps1Outcome(True, UNKNOWN)


def type_of(fact: Ps1Fact) -> Ps1TypeName | None:
    """
    The .NET type a fact carries, or `None` for `UNKNOWN` and `NULL`. `None` is *no type is named
    here* in both cases, and a caller that needs to tell them apart compares against `NULL`.
    """
    if isinstance(fact, (Ps1Typed, Ps1Constant)):
        return fact.type
    return None


def type_test(fact: Ps1Fact, target: str | Ps1TypeName) -> bool | None:
    """
    Whether `value -is target` holds for a value this fact describes: `True`, `False`, or `None`
    where the domain cannot decide. `$null` answers `False` for every target — it has no type to be
    one of — an `UNKNOWN` fact answers `None`, and everything else is the relation
    `refinery.lib.scripts.ps1.data.is_assignable_to` reads off the collected type model from the
    fact's runtime type. The result of a type test is always a `System.Boolean`, so unlike the value
    grid there is no measured cell to stamp the answer against; the only care needed is that a `None`
    stays a fold declined rather than becoming a guessed `False`.
    """
    if fact is NULL:
        return False
    runtime = type_of(fact)
    if runtime is None:
        return None
    return is_assignable_to(runtime, target)


def integer_of(fact: Ps1Fact) -> int | None:
    """
    The integer a fact names, or `None` for a fact that names anything else. This is what a caller
    holding a fact asks instead of reaching for the payload, and what it refuses is the point: a
    `Boolean` carries a Python `int` and is not one, a `$null` is an absent value rather than a
    zero, and a `Double` or a `Decimal` that happens to be whole is still not an integer here — a
    caller that wants the number one of those *converts* to is asking `convert`, which is where the
    rounding rule lives.
    """
    if not isinstance(fact, Ps1Constant) or fact.type not in _INTEGER_RANGE:
        return None
    payload = fact.payload
    return None if isinstance(payload, bool) or not isinstance(payload, int) else payload


def integer_at(target: Ps1TypeName, value: int) -> Ps1Fact:
    """
    `value` as a value of `target`, or `UNKNOWN` where `target` names no integer width or does not
    hold it. A width that does not hold a number is a throw rather than a wrap, measured for every
    one of them, so a caller that folds refuses either way and the two are one answer here.
    """
    bounds = _INTEGER_RANGE.get(target)
    if bounds is None or not bounds[0] <= value <= bounds[1]:
        return UNKNOWN
    return Ps1Constant(target, value)


def pattern_at(target: Ps1TypeName, magnitude: int) -> Ps1Fact:
    """
    The value the bit pattern `magnitude` denotes in a register of `target`'s width, with that
    width's sign, or `UNKNOWN` where `target` names no width or the pattern is wider than it holds.

    A pattern is not a magnitude, which is the whole reason this is a separate question: measured,
    `[int]'0xFFFFFFFF'` is -1 and `[Convert]::ToInt32('FFFFFFFF', 16)` is -1, where the digits read
    as a number are four billion. Two callers with different spellings ask it, so it is stated once.
    """
    bounds = _INTEGER_RANGE.get(target)
    if bounds is None:
        return UNKNOWN
    try:
        return Ps1Constant(target, _pattern_at_width(bounds, magnitude))
    except _Throws:
        return UNKNOWN


def text_of(fact: Ps1Fact) -> str | None:
    """
    The `String` a fact names, or `None` for a fact that names anything else. This is what a caller
    holding a fact asks instead of reaching for the payload, and the reason it exists is the one
    distinction the payload cannot make: a `Char` carries a Python `str` too, and it is not a String
    — measured, the two differ in what `-is [char]` answers, in which methods they have, in what
    `[int]` makes of them and in what `+` does with them on the left. A caller that read the payload
    would get the same characters back for both.

    It is not a spelling. `refinery.lib.scripts.ps1.ast.string_value` answers what text a *node* is
    written as, which is a syntactic question the analysis layer asks about command names and paths;
    this answers what text a *value* is, which only the domain can say.
    """
    if isinstance(fact, Ps1Constant) and fact.type == _STRING and isinstance(fact.payload, str):
        return fact.payload
    return None


def coerced_text(fact: Ps1Fact) -> str | None:
    """
    The text a value contributes where PowerShell coerces it to a String, or `None` where this
    module names none. It is `convert` to a `String` and nothing else, which is what makes it a
    different question from `text_of`: that one asks what a value *is*, this asks what it *becomes*,
    and a Char answers `None` to the first and its character to the second.

    Every string operator coerces this way and uniformly, measured over `-replace`, `-split`,
    `-join`, `-f` and `-match` and over both of their operands: `'x' -replace 'x', $true` is `True`,
    `-replace 'x', 1.50d` is `1.50`, `('a','b') -join 5` is `a5b`, `-join (72, 105)` is `72105`,
    `[char]65 -replace 'A', 'B'` is `B` and `$true -replace 'T', 'X'` is `Xrue`.

    A *method* does not coerce this way and must not ask this: it converts each argument to the
    parameter's declared type, and the two disagree — `'abc'.Substring([char]1)` is `bc`, where the
    Char becomes the number one and its text would be a control character that throws.
    """
    outcome = convert(fact, _STRING)
    return None if outcome.may_throw else text_of(outcome.value)


#: The types outside the integer widths whose text carries no culture at all. The widths are not
#: listed with them because `_INTEGER_RANGE` is already the one place they are named.
_CULTURE_FREE = frozenset({_BOOLEAN, _CHAR, _STRING})


def invariant_text(fact: Ps1Fact) -> str | None:
    """
    The text a value writes where the *current culture* renders it, or `None` where that text is not
    the one this module computes.

    It is `coerced_text` narrowed to the values no culture spells differently, and the narrowing is
    the whole of it. Measured on a host whose culture writes a decimal comma: `(1.50d).ToString()`
    is `1,50` and a collection separated by `$OFS = 1.5` reads `1,5`, where `[string]1.50d` is
    `1.50`. So a caller reading a value the *host* formats — a `ToString()` call, the separator a
    collection is joined with — computes the right characters only for a Boolean, a Char, a String
    and the integer widths, each measured to agree.
    """
    found = type_of(fact)
    if found is None or (found not in _CULTURE_FREE and found not in _INTEGER_RANGE):
        return None
    return coerced_text(fact)


_DECIMAL_DIGITS = re.compile(r'[0-9]+\Z')
_REAL_DIGITS = re.compile(r'(?:[0-9]*\.[0-9]+|[0-9]+\.?)(?:e[+-]?[0-9]+)?\Z', re.IGNORECASE)
_HEX_DIGITS = re.compile(r'[0-9a-f]+\Z', re.IGNORECASE)


def read(node: Node | None) -> Ps1Fact:
    """
    What the source pins this expression to, as a fact, or `UNKNOWN` when it pins nothing. This is
    the floor the rest of the domain stands on and it **refuses rather than invents**: an expression
    it cannot decide, a literal spelled in a way no measurement covers, and a number too wide for
    any type all answer `UNKNOWN`, never a value that happens to be close.

    Only literal structure is read — literals, the array and parenthesis forms that wrap them,
    `$true`, `$false` and `$null`, and the casts that are a *spelling* rather than a conversion, for
    which see `_cast_spelling`. An operator is not read at all, so that a caller asking what the
    *source* says never receives an answer that came from evaluating something.

    A sign is not an exception to that, because the parser has already decided it: a `-` written
    directly against a numeral is part of the numeral and reaches this inside `raw`, while
    `- 2147483648` and `-(2147483648)` are unary minus over a literal and are refused here. Reaching
    past the space or the parenthesis to the numeral would report the Int32 that only the glued
    spelling has; the other two are an operator over a value and belong to `apply`.
    """
    if node is None:
        return UNKNOWN
    if isinstance(node, Ps1ParenExpression):
        return UNKNOWN if node.expression is None else read(node.expression)
    if isinstance(node, (Ps1StringLiteral, Ps1HereString)):
        return Ps1Constant(_STRING, node.value)
    if isinstance(node, (Ps1ExpandableString, Ps1ExpandableHereString)):
        return _quoted(node.parts)
    if isinstance(node, (Ps1IntegerLiteral, Ps1RealLiteral)):
        return _numeral(node.raw)
    if is_builtin_variable(node, {'true'}):
        return Ps1Constant(_BOOLEAN, True)
    if is_builtin_variable(node, {'false'}):
        return Ps1Constant(_BOOLEAN, False)
    if is_builtin_variable(node, {'null'}):
        return NULL
    if isinstance(node, (Ps1ArrayLiteral, Ps1ArrayExpression)):
        return _array(node, _pinned).value
    if isinstance(node, Ps1SubExpression):
        return _subexpression(node, _pinned).value
    if isinstance(node, Ps1CastExpression):
        return _cast_spelling(node)
    return UNKNOWN


def read_operand(node: Node | None) -> Ps1Fact:
    """
    What an operand *written in the source* contributes to an operation, which is `read` except for
    a `Decimal` numeral whose value is a whole number: that one contributes the number without the
    places it was written with.

    5.1 folds a constant expression in its parser, and a numeral reaching that fold carries no scale
    where it has nothing to hold — measured, `'x' + 1.00d` is `x1` where `'x' + 1.100d` is `x1.100`,
    and `- 1.0d` is `-1` where `- 1.10d` is `-1.10`. It is each *operand* that loses its places and
    not the result: `1.500d + 1.500d` is `3.000`, a whole number written to three places, because
    neither addend was one.

    **Only a numeral**, which is what makes this a different question from `read`. The same value
    reached any other way keeps its scale, because then no numeral stands where the parser folds:
    `$z = 1.0d; 'x' + $z` is `x1.0` and `'x' + [decimal]'1.0'` is `x1.0`, both measured, against the
    `x1` of the numeral written in place. A bare `1.0d` is `1.0` for the same reason — there is no
    operation over it to fold.

    That is also why `refinery.lib.scripts.ps1.deobfuscation.constants` will not carry such a value:
    inlining one *writes* a numeral where the source had none, which would move the operand into the
    fold and take its places away.
    """
    numeral = _folded_numeral(node)
    return read(node) if numeral is None else numeral


def survives_being_written(fact: Ps1Fact) -> bool:
    """
    Whether a value keeps its meaning when a pass writes it down as a constant where the source had
    something else. A `Decimal` whose value is a whole number written to places does not: the source
    reached it through a variable or a cast, and the numeral standing in for one of those is a
    numeral the parser folds, which takes the places away. `read_operand` is the rule, and this is
    the same rule asked of a value rather than of a node.

    Measured: `$z = 1.0d; $z + 0d` is `1.0` while the `1.0d + 0d` an inliner writes for it is `1`.
    Nothing else in the domain answers `False` here — every other value has a spelling that reads
    back as itself wherever it is put, which is what `render` and `read` being inverses means.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _DECIMAL:
        return True
    payload = fact.payload
    if not isinstance(payload, decimal.Decimal) or payload.as_tuple().exponent == 0:
        return True
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        return payload.to_integral_value() != payload


def _folded_numeral(node: Node | None) -> Ps1Fact | None:
    """
    The value a `Decimal` numeral has where the parser folds it, or `None` for a node that is not
    one of those. See `read_operand`.

    A parenthesis does not stop the fold and so does not stop this: measured, `'x' + (1.0d)` is
    `x1`, the same as without it.
    """
    if node is not None:
        node = unwrap_parens(node)
    if not isinstance(node, (Ps1IntegerLiteral, Ps1RealLiteral)):
        return None
    fact = read(node)
    if not isinstance(fact, Ps1Constant) or fact.type != _DECIMAL:
        return None
    payload = fact.payload
    if not isinstance(payload, decimal.Decimal) or payload.as_tuple().exponent == 0:
        return None
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        whole = payload.to_integral_value()
        return None if whole != payload else Ps1Constant(_DECIMAL, whole)


def fact_of(payload: object) -> Ps1Fact:
    """
    The value a Python object denotes where nothing has narrowed it, or `UNKNOWN` for one that
    denotes none.

    This is `read`'s counterpart for a caller holding a value it *computed* rather than one it
    found written down — the emulator is that caller, and `render` is where what it computed
    becomes a tree again. A number decides the way an unsuffixed numeral decides, because the
    narrowest width that holds a magnitude is the only rule the domain has for a bare one.

    **A `str` is a String and never a Char.** That is not a gap: a payload does not carry a type
    and the two are the same Python object, which is the whole reason `Ps1Constant` carries a type
    beside its payload. A caller that means a Char has to build the fact instead of asking here,
    and one whose currency cannot tell the two apart is a caller whose Chars are already gone.

    A number no literal spells names nothing here, which is `_finite`'s rule read at this boundary
    too: `render` states that a value it is handed always has a spelling, and a fact carrying an
    infinity would be the one that does not.
    """
    if payload is None:
        return NULL
    if isinstance(payload, bool):
        return Ps1Constant(_BOOLEAN, payload)
    if isinstance(payload, int):
        return _widest_needed(payload)
    if isinstance(payload, float):
        return UNKNOWN if _finite(payload) is None else _double(payload)
    if isinstance(payload, str):
        return Ps1Constant(_STRING, payload)
    if isinstance(payload, (list, tuple)):
        return _collected(Ps1Outcome(False, fact_of(one)) for one in payload).value
    return UNKNOWN


def _quoted(parts: list) -> Ps1Fact:
    """
    A double-quoted string all of whose parts are text, which is the one shape of it that pins a
    value: an expansion is a read of something this does not know, and one part it cannot name
    leaves the whole string unnamed rather than shortened.
    """
    text: list[str] = []
    for part in parts:
        if not isinstance(part, Ps1StringLiteral):
            return UNKNOWN
        text.append(part.value)
    return Ps1Constant(_STRING, ''.join(text))


def _pinned(node: Node | None) -> Ps1Outcome:
    """
    What the source pins an expression to, as an outcome. Reading a literal cannot throw, so this
    says that it cannot — but only where a value was read at all. A fact of `UNKNOWN` is the
    refusal, and a refusal claims nothing on either axis.
    """
    fact = read(node)
    return NOTHING if fact is UNKNOWN else Ps1Outcome(False, fact)


def _array(
    node: Ps1ArrayLiteral | Ps1ArrayExpression,
    of: Callable[[Expression], Ps1Outcome],
) -> Ps1Outcome:
    """
    An array whose value's payload is the facts of its elements. `of` is how one element is
    answered, which is what lets `read` build an array out of literals and `evaluate` build one out
    of anything: the two spellings below differ in the same way whatever an element is worth, so
    they are described once. One element the caller cannot answer makes the whole array unknown —
    a caller reasoning about it would otherwise be handed a shorter array than the script builds —
    and one that may throw makes the array one that may throw, because building it is what runs it.

    The two spellings do not build the same array from the same parts, which is measured rather than
    assumed. The comma operator takes each operand whole, so `(1, 2), 3` is two elements and the
    first of them is an array. `@()` collects what a pipeline hands it and a pipeline unrolls a
    collection one level on the way, so `@(@(1, 2))` and `@((1, 2))` are each *two* elements rather
    than one holding two, while `@(@(1, 2), 3)` is two — the unrolling happens once, to the value
    the statement produced, and not again to what was inside it.
    """
    if isinstance(node, Ps1ArrayLiteral):
        return _collected(of(element) for element in node.elements)
    stream = _stream(node.body, of)
    return NOTHING if stream is None else _collected(stream)


def _subexpression(
    node: Ps1SubExpression,
    of: Callable[[Expression], Ps1Outcome],
) -> Ps1Outcome:
    """
    A `$( ... )`, which collects the same stream `@( ... )` collects and then *collapses* it. That
    last step is the whole difference between the two spellings and it is measured: `$(1)` is an
    Int32 where `@(1)` is an `Object[]` of one, and `$()` is `$null` where `@()` is the empty
    array. Everything before the collapse agrees — `$(1, 2)`, `$(@(1, 2))` and `$(1; 2)` are each
    two elements, and `$((1, 2), 3)` is two of which the first is an array — so the unrolling is
    stated once, in `_stream`, rather than described twice with a chance of drifting.
    """
    stream = _stream(node.body, of)
    if stream is None:
        return NOTHING
    if not stream:
        return Ps1Outcome(False, NULL)
    if len(stream) == 1:
        return stream[0]
    return _collected(stream)


def _stream(
    body: list,
    of: Callable[[Expression], Ps1Outcome],
) -> list[Ps1Outcome] | None:
    """
    The success stream a statement list contributes, with each statement's value unrolled one level
    the way a pipeline unrolls it, or `None` where a statement is not one this can answer.
    """
    outcomes: list[Ps1Outcome] = []
    for statement in body:
        if not isinstance(statement, Ps1ExpressionStatement) or statement.expression is None:
            return None
        outcome = of(statement.expression)
        inner = outcome.value
        if isinstance(inner, Ps1Constant) and inner.type == _OBJECT_ARRAY and isinstance(
            inner.payload, tuple
        ):
            outcomes.extend(Ps1Outcome(outcome.may_throw, one) for one in inner.payload)
        else:
            outcomes.append(outcome)
    return outcomes


def _collected(outcomes: typing.Iterable[Ps1Outcome]) -> Ps1Outcome:
    """
    The collection its elements came to. Every one of them has to *be* a value, `$null` included:
    an element that carries only a type leaves the collection unknown rather than making a
    `Ps1Constant` whose payload is not one. `([int]'abc'), 1` is what that would be — an Int32 or a
    throw beside the number one, in a fact that says it is exactly this value.
    """
    gathered = tuple(outcomes)
    if not all(_is_value(outcome.value) for outcome in gathered):
        return NOTHING
    return Ps1Outcome(
        any(outcome.may_throw for outcome in gathered),
        Ps1Constant(_OBJECT_ARRAY, tuple(outcome.value for outcome in gathered)),
    )


def _is_value(fact: Ps1Fact) -> bool:
    """
    Whether a fact names a value rather than a bound on one. `$null` is one: it is what an absent
    value *is*, not the absence of knowledge about it.
    """
    return fact is NULL or isinstance(fact, Ps1Constant)


def _cast_spelling(node: Ps1CastExpression) -> Ps1Fact:
    """
    The value a cast *spells*, which is a question about the source and not an evaluation of it. The
    language has no literal for a `System.Char` or for any of the six integer widths, so `render`
    writes a value of one of those as a cast of a numeral, and this reads exactly that back:
    `read(render(fact))` is `fact` for every value the domain can spell, and until this arm existed
    it was not — a value this module wrote out could not be read in again.

    One operator deep is where that matters, because one operator deep is where folding works. A
    pass standing at `[char] 72 + [char] 105` asks `read` for each operand and `apply` for the
    operator; a Char that cannot be read back is a Char that cannot be added to anything, so the
    only way to fold it would have been to spell it as a String first, which is the erasure the
    whole phase exists to end.

    It is the *target* that is restricted and not the operand. A cast to a type that does have a
    literal is a conversion rather than a spelling — `[int] '1e3'` is a question about .NET's parser
    and `[int] (1 + 2)` an operator underneath one — and neither is what the source pins. That
    restriction is also what keeps `read` from walking into an expression: the only thing it
    recurses through is another spelling.
    """
    target = resolve_type(node.type_name)
    if target is None or target not in _SPELLED_BY_A_CAST:
        return UNKNOWN
    outcome = convert(read(node.operand), target)
    return UNKNOWN if outcome.may_throw else outcome.value


def _numeral(raw: str) -> Ps1Fact:
    """
    The fact a numeric literal's spelling denotes, measured rather than derived from the digits
    alone: the same digits are an Int32, an Int64, a Decimal or a Double depending on how wide they
    are and what is written after them.

    A spelling no measurement covers is refused. `_` is one: PowerShell 5.1 has no digit separator
    and reads `1_0` as a command name, so a lexer that accepts it must not be allowed to hand the
    domain the number ten.

    A multiplier suffix is what the model already knows it is, and what it does to the *type* is
    what is measured here: it applies to whatever the numeral is and the result is then typed by
    the rule that numeral's form uses, so `1kb` is an Int32 1024, `4gb` an Int64 4294967296,
    `1lkb` an Int64 1024 and `1.5kb` a Double 1536.
    """
    if '_' in raw:
        return UNKNOWN
    text = raw
    sign = 1
    if text[:1] in ('-', '+'):
        sign = -1 if text[0] == '-' else 1
        text = text[1:]
    multiplier = 1
    lowered = text.lower()
    for suffix, factor in MULTIPLIERS.items():
        if lowered.endswith(suffix):
            multiplier = factor
            text = text[:-len(suffix)]
            break
    if text[:2].lower() == '0x':
        return _hex_numeral(text[2:], sign, multiplier)
    return _decimal_numeral(text, sign, multiplier)


def _hex_numeral(digits: str, sign: int, multiplier: int) -> Ps1Fact:
    """
    A hexadecimal literal, which names a *bit pattern* rather than a magnitude: measured, `0xFF` is
    255, `0xFFFFFFFF` is Int32 -1 because eight digits fill an Int32, `0x100000000` is Int64
    4294967296 and seventeen digits fit nothing, which 5.1 reports as a parse error.

    A `L` suffix changes the question from *which width holds this pattern* to *read these digits as
    an Int64*, so `0xFFFFFFFFL` is 4294967295 rather than -1.

    The width the pattern fills is a *floor* on the result type and not merely a step on the way to
    it. `0xFFFFFFFFFFFFFFFF` is Int64 -1: the value -1 would fit an Int32, but the sixteen digits
    said which width was being filled, and narrowing back to what the number needs would report a
    type no value in the script has.

    A multiplier over a pattern that had to be reinterpreted as negative is refused: composing the
    two rules would answer where nothing was measured, and nothing here answers from a composition.
    """
    long_suffix = digits[-1:].lower() == 'l'
    if long_suffix:
        digits = digits[:-1]
    if not _HEX_DIGITS.match(digits):
        return UNKNOWN
    magnitude = int(digits, 16)
    if long_suffix:
        if magnitude > _INT64_RANGE[1]:
            return UNKNOWN
        return _long(sign * magnitude * multiplier)
    if magnitude <= 0xFFFFFFFF:
        width = _INT32
        value = magnitude - 0x100000000 if magnitude > _INT32_RANGE[1] else magnitude
    elif magnitude <= 0xFFFFFFFFFFFFFFFF:
        width = _INT64
        value = magnitude - 0x10000000000000000 if magnitude > _INT64_RANGE[1] else magnitude
    else:
        return UNKNOWN
    if value < 0 and multiplier != 1:
        return UNKNOWN
    return _no_narrower_than(width, sign * value * multiplier)


def _decimal_numeral(text: str, sign: int, multiplier: int) -> Ps1Fact:
    """
    A decimal literal. Without a suffix it takes the narrowest of Int32, Int64, Decimal and Double
    that holds it — measured all the way up, `2147483648` being Int64, `9223372036854775808` Decimal
    and `10^32` Double. A `L` or `D` suffix names the type instead, and over a real that is a
    conversion rather than a refusal: `1.5L` is Int64 2 and `2.5L` is Int64 2, which is the
    half-to-even rounding a cast performs.

    A real `Decimal` takes its sign by `copy_negate` rather than by a multiplication, which is a
    *context* operation in Python and rounds to the ambient 28 digits where the type holds 29: the
    literal `7922816251426433759354395033.5d` was read as `7922816251426433759354395034`, a number
    the source does not spell, and every reader of the constant inherited it.
    """
    suffix = text[-1:].lower()
    if suffix in ('l', 'd'):
        text = text[:-1]
    else:
        suffix = ''
    if _DECIMAL_DIGITS.match(text):
        magnitude = int(text) * multiplier
        if suffix == 'l':
            return _long(sign * magnitude)
        if suffix == 'd':
            return Ps1Constant(_DECIMAL, decimal.Decimal(sign * magnitude))
        return _widest_needed(sign * magnitude)
    if not _REAL_DIGITS.match(text):
        return UNKNOWN
    if suffix and multiplier != 1:
        return UNKNOWN
    if suffix == 'd':
        spelled = decimal.Decimal(text)
        return Ps1Constant(_DECIMAL, spelled.copy_negate() if sign < 0 else spelled)
    if suffix == 'l':
        return _long(sign * round(decimal.Decimal(text)))
    return _double(sign * float(text) * multiplier)


def _widest_needed(value: int) -> Ps1Fact:
    """
    The narrowest type that holds `value`, which is what an unsuffixed decimal literal takes.
    """
    if _INT32_RANGE[0] <= value <= _INT32_RANGE[1]:
        return Ps1Constant(_INT32, value)
    if _INT64_RANGE[0] <= value <= _INT64_RANGE[1]:
        return Ps1Constant(_INT64, value)
    if _DECIMAL_MIN <= value <= _DECIMAL_MAX:
        return Ps1Constant(_DECIMAL, decimal.Decimal(value))
    return _double(value)


def _no_narrower_than(floor: Ps1TypeName, value: int) -> Ps1Fact:
    """
    The narrowest type that holds `value`, but never narrower than `floor`. What a hexadecimal
    literal fills is a width, so the width is what it has however small the number it denotes is.
    """
    fact = _widest_needed(value)
    if floor == _INT64 and isinstance(fact, Ps1Constant) and fact.type == _INT32:
        return Ps1Constant(_INT64, value)
    return fact


def _long(value: int) -> Ps1Fact:
    return Ps1Constant(_INT64, value) if _INT64_RANGE[0] <= value <= _INT64_RANGE[1] else UNKNOWN


def _double(value: int | float) -> Ps1Fact:
    try:
        return Ps1Constant(_DOUBLE, float(value))
    except OverflowError:
        return UNKNOWN


#: The integer types the domain computes in, narrowest first, each with the range it holds. The
#: order is what `_stamped` walks to decide which of a cell's candidate types a computed value has,
#: so it is the widening order and not merely a listing.
_INTEGER_WIDTHS: tuple[tuple[Ps1TypeName, int, int], ...] = (
    (_BYTE, 0, 0xFF),
    (_SBYTE, -0x80, 0x7F),
    (_INT16, -0x8000, 0x7FFF),
    (_UINT16, 0, 0xFFFF),
    (_INT32, *_INT32_RANGE),
    (_UINT32, 0, 0xFFFFFFFF),
    (_INT64, *_INT64_RANGE),
    (_UINT64, 0, 0xFFFFFFFFFFFFFFFF),
)

#: The range each integer type holds, which is what a cast to it is refused outside of. Built from
#: the same table the widening order is, so the two cannot come apart.
_INTEGER_RANGE: dict[Ps1TypeName, tuple[int, int]] = {
    name: (low, high) for name, low, high in _INTEGER_WIDTHS
}

#: The widths a shift masks its count by, from the *left operand's type* rather than from how large
#: its value happens to be. Only the two the mask is documented for are here; a shift over a narrower
#: left operand keeps that operand's type in the grid and is not computed, because what the count is
#: masked by there was never measured.
_SHIFT_WIDTHS = {_INT32: 32, _INT64: 64}

#: The grid's row for `$null`, which the capture collected under the one name that is not a type a
#: value can have.
_VOID = _type('System.Void')

#: Every type the grid has a row for, which is what a whole column of it is read over. A name the
#: resolver does not know is dropped rather than raised on: it would mean the capture covers a type
#: the collected tables do not, and reading one column short is a weaker answer where refusing to
#: import is no answer at all.
_GRID_TYPES = frozenset(
    resolved for resolved in map(resolve_type, operand_witnesses()) if resolved is not None
)

#: The operand types whose witnesses reach every outcome a cell over them has.
#:
#: A capture is a *lower* bound: it records what some values did. Reading a cell as *what this
#: operation produces* is an upper-bound claim, and no witness list proves one — it can only fail to
#: disprove it. So which cells may be read that way is declared, and the declaration is a
#: measurement rather than an argument. The whole grid was captured a second time with the extremes
#: the shipped witness list is missing — `[int64]::MinValue`, `[single]::MaxValue` and `::MinValue`,
#: `[double]::MinValue` and `::Epsilon`, `[decimal]::MinValue`, `[char]65535`, six more strings and
#: three more collections — and **390 of the 4096 binary cells moved**, 93 of them by gaining a
#: throw they had not recorded. Every one of the 390 carries an operand this set leaves out, which
#: is what makes it the right set rather than a hopeful one.
#:
#: What each exclusion costs is a cell, not a worry. `Byte + String` was `{Int32}` and is really
#: `{Int32, Int64, Decimal, Double}`, which is the `1 + '2147483648'` the type corpus measures as an
#: Int64 and this module used to answer `Int32` to. `Byte - Int64` was `{Int64}` and is really
#: `{Int64, Double}`. `UInt16 * Char` was `{Int32}` and is really `{Int32, Double}`. `Byte -
#: Decimal` and `Byte -band Single` were each recorded as never throwing and each throws.
#:
#: **A cell is the full cross-product of its two operands' witnesses**, which is what makes reading
#: one as an upper bound a two-way-exhaustive claim over the chosen values rather than a pairwise
#: assumption over them. `refinery/run-pwsh-operators.ps1` applies the operator to every `(l, r)`
#: with `l` from the left type's witness list and `r` from the right type's, so a cell over two
#: types in this set was measured at every combination those lists reach and not merely at each
#: value in turn. Without that the set would be claiming something the capture never tried.
#:
#: A `Double` is here although its own extremes are absent, because there is nothing for them to
#: reach: arithmetic never leaves a Double — it saturates to an infinity rather than widening or
#: throwing — and the second capture found no cell that a Double alone moves.
#:
#: The measurement is against the shipped resource, so regenerating it re-opens the question.
#: `test.lib.scripts.ps1.corpus.GRID_WITNESSES` is the ratchet that says so out loud, and
#: `GRID_WITNESS_GAPS` beside it carries the cell that convicts each type left out here.
_SPANNED = frozenset({
    _BOOLEAN,
    _BYTE,
    _DOUBLE,
    _INT16,
    _INT32,
    _SBYTE,
    _UINT16,
    _UINT32,
    _UINT64,
    _VOID,
})


#: What a kernel computes in. A `Decimal` is here because PowerShell has one and Python's is the
#: only faithful carrier for it; a `bool` because a comparison is an operation like any other; a
#: `str` because a conversion produces one and the `Ps1TypeName` the grid names is what tells a Char
#: from a one-character String.
#:
#: A `tuple` of facts is the collection a `+` or a `*` over one produces. It carries facts rather
#: than payloads for the reason `Ps1Constant` does: the elements keep the types they had, so a
#: collection of Chars stays one. `_stamped` has the arm that licenses it.
_Number = int | float | bool | decimal.Decimal | str | tuple['Ps1Fact', ...]

#: How long a collection an operator may build. A repetition's size is an operand rather than a
#: bound, so `@(1, 2) * 0xFFFFFFFF` is an allocation and not an answer; above this the operation is
#: declined and the caller keeps the expression it had. Matched to the bound the folding pass
#: already applies to a string repetition, so the two cannot disagree about what is too large.
_MAX_COLLECTION = 0x1000


class _Throws(Exception):
    """
    Raised by a kernel for an application that PowerShell answers by throwing, so that a throw is
    reported as one rather than as a refusal. The two are different answers: a throw is knowledge.
    """


def apply(operator: str, left: Ps1Fact, right: Ps1Fact) -> Ps1Outcome:
    """
    What `left <operator> right` produces. The *type* comes from the measured grid in
    `refinery.lib.scripts.ps1.data`, never from a rule written here, and the *value* from a kernel
    that is checked against it: a computed value whose type is not one the grid recorded for that
    cell is refused rather than reported, because the grid is what a host did and the kernel is only
    what we believe.

    A cell that recorded a throw stops the kernel being consulted, unless every way that cell can
    throw is one the kernel checks for itself — see `_throws_are_modelled`. Without that exception a
    single throwing pair anywhere in a cell would cost every other pair in it its fold; with it, a
    throw the kernel cannot see is still never folded past.

    A value the kernel computed is answered without asking whether the witnesses span the operands,
    and the two halves of that survive the question the cell alone does not. The *throw*: of the
    cells the kernel computes in, the eight whose recorded silence about throwing is wrong are all a
    `Decimal` subtraction, which `_throws_are_modelled` already covers — measured against the second
    capture `_SPANNED` was found by. The *type*: an under-recorded set can only make `_stamped`
    refuse, because which promotion a pair takes is settled by their types, so the one thing their
    values decide is overflow, and an overflowed value leaves every candidate rather than landing in
    the wrong one.

    The operator is folded to lower case once, here, because PowerShell's are case-insensitive and
    a caller holding one out of a script holds whatever case was written. Doing it at the grid
    lookup alone left `-BAND` finding its cell and missing the kernel, which is a fold lost to a
    spelling.
    """
    operator = operator.lower()
    cell = binary_outcome(operator, *(_grid_type(left), _grid_type(right)))  # type: ignore[misc]
    if cell is None:
        return NOTHING
    if not cell.may_throw or _throws_are_modelled(operator, left, right):
        try:
            computed = _kernel(operator, left, right)
        except _Throws:
            return Ps1Outcome(True, UNKNOWN)
        if computed is not None:
            stamped = _typed_result(operator, computed, left, right, cell.types)
            if stamped is not UNKNOWN:
                return Ps1Outcome(False, stamped)
    return _from_binary_cell(cell, _spans(left, right))


#: The operators whose result type the promotion decides rather than the grid. These are the five
#: 5.1 runs through its numeric promotion, and the only ones a cell is a worse answer for: a cell
#: over `Int32` and `UInt64` records `Decimal`, `Double` and `UInt64` together because the three are
#: what different *values* produced, and `_promotion` is what says which of them any given pair
#: takes. Every other operator keeps reading its type from the measurement.
_PROMOTED_OPERATORS = frozenset({'+', '-', '*', '/', '%'})


def _typed_result(
    operator: str,
    computed: _Number,
    left: Ps1Fact,
    right: Ps1Fact,
    candidates: frozenset[Ps1TypeName],
) -> Ps1Fact:
    """
    The fact a computed value carries. For an arithmetic application over two numbers that is the
    type the promotion answers in; for everything else it is the one the measured cell allows.

    The two are not alternatives that happen to agree. `0 - [uint64]::MaxValue` is measured a
    `Double` on the host and the cell holds `Decimal` beside it, so reading the cell answered a
    `Decimal` — a value of a type the operation never produced. A cell cannot do better, because
    what separates the two is the sign of the signed operand.

    An arithmetic pair the promotion does not cover falls back to the cell rather than being
    refused, so a String, a collection or a `Single` operand is answered exactly as before.
    """
    if operator in _PROMOTED_OPERATORS:
        promotion = _promotion(left, right)
        if promotion is not None:
            promoted = _promoted(computed, promotion)
            if promoted is not UNKNOWN:
                return promoted
    return _stamped(computed, candidates)


#: What `-bnot` complements at, keyed by the type of the operand it is given. The complement happens
#: at a width and keeps that width's type, and an operand that *has* an integer width keeps it,
#: floored at `Int32`: measured, `-bnot [byte]5` is the Int32 -6, `-bnot [uint32]7` the UInt32
#: 4294967288 and `-bnot 1L` the Int64 -2.
#:
#: **For an operand with no integer width of its own this is a floor and not the rule.** A `Char`, a
#: `Double`, a `Single`, a `Decimal`, a `String`, a `Boolean` and `$null` are converted first, and
#: 5.1 picks the width from the *value*: `-bnot 7.0` is the Int32 -8, but `-bnot 3000000000.0` is
#: the **UInt32** 1294967295 and `-bnot 5000000000.0` the Int64 -5000000001, each the narrowest
#: width that holds the number. Naming `Int32` here is safe because it is the narrowest rung: a
#: value that does not fit one makes `convert` throw and `apply_unary` refuse, so what the floor
#: costs is a fold and never an answer.
#:
#: A type absent here is one nothing measured a width for. A collection is not missing but refused:
#: `-bnot @(1, 2)` throws, and so does `-bnot 'abc'`, which is the conversion throwing rather than
#: the operator.
_BNOT_WIDTH: dict[Ps1TypeName, Ps1TypeName] = {
    _BOOLEAN: _INT32,
    _BYTE: _INT32,
    _CHAR: _INT32,
    _DECIMAL: _INT32,
    _DOUBLE: _INT32,
    _INT16: _INT32,
    _INT32: _INT32,
    _INT64: _INT64,
    _SBYTE: _INT32,
    _STRING: _INT32,
    _UINT16: _INT32,
    _UINT32: _UINT32,
    _UINT64: _UINT64,
    _VOID: _INT32,
}


def _negated(operand: Ps1Fact) -> Ps1Outcome:
    """
    What `- operand` produces, from the measured unary grid and a kernel checked against it.

    **A String is read as a numeral first, and the row the type comes out of is the numeral's own.**
    A coerced String keeps the type its spelling has — `'5L'` is an Int64 and `'1e3'` a Double, the
    same reading every arithmetic operand gets — and negating it produces that type: `- '1e3'` is
    the Double -1000, measured. The grid's `String` row names `Int32` alone because every witness
    spelled one, so reading *that* row would stamp `- '5L'` an Int32 where a host answers an Int64.
    A value stamped with a type the operation never had is what `_typed_result` keeps `apply` from
    doing; moving the lookup onto the coerced fact is the same refusal spelled for one operand.

    **A cell this cannot compute a value in is refused outright rather than answered with its
    type.** The unary capture is a witnessed lower bound like every other, and there is no spanning
    claim for the unary grid to license reading a cell as what the operation *does*.

    A collection reaches no number and `_negatable` refuses it, which is the answer its cell carries
    anyway: `- @()` raises a `MethodNotFound`, measured, and the row names no type at all.

    **The number is subtracted from zero rather than negated**, because that is what 5.1 runs: its
    compiler emits `- x` as the binary subtraction `0 - x` and has no unary arm for it at all. The
    two differ on one value and it is a value scripts hold — `0 - 0.0` is a positive zero where
    `-(0.0)` is a negative one, and a sign this domain invented travels into every quotient taken
    from it.

    A `Decimal` is subtracted like every other number, and by the same `_computed` a binary `-`
    reaches, so that the one operation is computed one way. A sign flip through `copy_negate` stood
    here and is the reading that does *not* agree with 5.1 on a zero — Python's `Decimal` has a
    signed zero too, so `- 0d` came back as `-0` where a host writes `0`.

    The result reaches `_decimal_result` for the same reason every binary kernel result does: a
    number that has left the range a `Decimal` holds is the throw a host raises, one that has
    overflowed to an infinity is a value this domain does not carry, and one the type holds only a
    rounding of is not a value this can report.
    """
    coerced = _coerced_numeral(operand)
    if coerced is not None:
        if not isinstance(coerced, Ps1Constant):
            return NOTHING
        operand = coerced
    source = _grid_type(operand)
    cell = None if source is None else unary_outcome('-', source)
    if cell is None:
        return NOTHING
    number = _negatable(operand)
    if number is None:
        return NOTHING
    try:
        computed = _computed(operator_module.sub, 0, number)
    except _Throws:
        return Ps1Outcome(True, UNKNOWN)
    if computed is None:
        return NOTHING
    stamped = _stamped(computed, cell.types)
    return NOTHING if stamped is UNKNOWN else Ps1Outcome(False, stamped)


def _negatable(operand: Ps1Fact) -> int | float | decimal.Decimal | None:
    """
    The number `- operand` negates, or `None` where this module computes nothing for it. `$null`
    negates as the integer zero, measured: `- $null` is the Int32 0.

    Everything else is the number a cast reads the value as, which is one reader rather than a
    second copy of it: a Boolean is its truth and a Char its code point there already, and the
    payload is checked against the type the fact carries rather than trusted.
    """
    if operand is NULL:
        return 0
    return _numeric_source(operand) if isinstance(operand, Ps1Constant) else None


def apply_unary(operator: str, operand: Ps1Fact) -> Ps1Outcome:
    """
    What `<operator> operand` produces.

    **Unary minus reads the measured grid**, exactly as `apply` reads the binary one: the *type* is
    what a host was observed to produce and the *value* comes from a kernel checked against it. The
    capture covers `-` over all sixteen operand rows, and the cells where it names two types are the
    ones the value decides between — `Int32` negates to an `Int32` or, where the result leaves that
    width, to a `Double`. Measured: `- 5` is the Int32 -5 and `- (-2147483648)` the Double
    2147483648, `- [uint32]1` the Double -1 and `- [char]65` the Int32 -65. `_stamped` is what picks
    among a cell's types by which of them holds the number, so nothing here states that rule twice.

    A String is negated by being read as a numeral first, which is where the `String` row's recorded
    throw comes from: `- '5'` is -5, `- ' 5 '` is -5, `- ''` is 0 and `- 'abc'` throws. That is the
    same coercion arithmetic performs, so the same reader sees it and the same throws are modelled.

    `-not` is absent because it is not this question: it negates a truth value, which `convert` to
    a `Boolean` already answers, and `is_truthy` is what asks.

    `-bnot` keeps a table of its own, `_BNOT_WIDTH`, because what it needs is not the result type
    but the *width the complement happens at*, and that is a different measurement. Reading the grid
    for its result type as well is recorded work, not done here.
    """
    operator = operator.lower()
    if operator == '-':
        return _negated(operand)
    if operator != '-bnot':
        return NOTHING
    source = _grid_type(operand)
    width = None if source is None else _BNOT_WIDTH.get(source)
    if width is None:
        return NOTHING
    converted = convert(operand, width)
    number = integer_of(converted.value)
    if converted.may_throw or number is None:
        return NOTHING
    low, high = _INTEGER_RANGE[width]
    complement = ~number
    return Ps1Outcome(False, Ps1Constant(width, complement % (high + 1) if low == 0 else complement))


def convert(fact: Ps1Fact, target: Ps1TypeName) -> Ps1Outcome:
    """
    What `[target] fact` produces, read from the measured conversion grid exactly as `apply` reads
    the binary one: the *type* is what a host was observed to produce and the *value* comes from a
    kernel checked against it.

    A cast throws where the value does not fit its target — measured, `[byte]300`, `[byte]-1`,
    `[int]2147483648`, `[char]65536` and `[char]-1` all throw rather than wrapping — and `_cast`
    raises for exactly that, so a cell that recorded a throw may still be computed where the target
    is one whose range the kernel checks. For any other target a recorded throw is one nothing here
    sees, and the cell answers alone.

    A `String` operand is read by rules of its own, in `_from_string`, and only the spellings those
    rules were measured over are computed: .NET parses a String by rules Python does not share, and
    5.1 has two of them that disagree with each other. Every other spelling reaches the grid for its
    type and stops there, which is `[int]'abc'` still being *an Int32 or a throw* — see
    `_from_conversion_cell` for why a cast may say that where an operator may not.
    """
    source = _grid_type(fact)
    cell = None if source is None else conversion_outcome(target, source)
    if cell is None:
        return NOTHING
    if not cell.may_throw or _cast_throws_are_modelled(target):
        try:
            computed = _cast(target, fact)
        except _Throws:
            return Ps1Outcome(True, UNKNOWN)
        if computed is not None:
            stamped = _stamped(computed, cell.types)
            if stamped is not UNKNOWN:
                return Ps1Outcome(False, stamped)
    return _from_conversion_cell(cell, _spans(fact))


def evaluate(
    node: Node | None,
    type_of_variable: Ps1VariableTyping | None = None,
) -> Ps1Outcome:
    """
    What this expression produces. It is the module's one recursion and the only entry a caller
    holding a *tree* needs: `read`, `convert` and `apply` each answer about one step, and a consumer
    that walked the tree itself would be deciding at every node what is decided here once. A literal
    is `read`, a parenthesis is its inner, an array is its elements, a cast is `convert` over its
    operand, an operator is `apply` over both of its, and everything else carries whatever type the
    static surface names for it.

    It refuses far more than it answers, and that is the contract rather than a shortfall: for an
    expression another reader in this module can answer, this agrees with that reader or names
    nothing — never a third thing.

    A throw travels up. An operand that may throw makes the expression consuming it one that may
    throw, whatever the operation does with the value, and so does not knowing what the operand
    does, because `Ps1Outcome` reads its two fields in one direction.

    A type literal is refused rather than answered, which is the one place this deliberately says
    less than `resolve_expression_type`. That function answers `[int]` with `System.Int32` because
    what asks it is a member lookup, and the type a literal *names* is what a lookup needs; the
    value one *is* is a `System.RuntimeType`, and no measurement here covers it.

    A unary operator is `apply_unary`, which reads a grid of its own. A sign written against a
    numeral does not reach it — the parser puts that sign inside the numeral's spelling, so `-1` is
    a literal and `read` answers it; what reaches here is `- 1` with a space, and `- $x`.

    `type_of_variable` is what the caller can say about a variable occurrence, exactly as
    `resolve_expression_type` takes it. Nothing here invents a variable's value, so a variable the
    caller cannot type names nothing.

    **The answer for a node is remembered until some tree changes**, because being the one entry a
    caller with a tree needs is only affordable if it is. What asks this is a visitor descending the
    tree, and a visitor asks about a node's operand at the operand, then again at its parent, then
    again at *its* parent: the recursion below is proportional to the subtree, so the walk as a
    whole is quadratic in the depth of an expression, and a chain of two hundred operands the pass
    refuses to fold is where that stops being theoretical. What is remembered is keyed on the node's
    identity, and the whole table is dropped the moment `refinery.lib.scripts.mutation_epoch` moves
    — so an entry can only ever be read back over the same tree that produced it, and this stays a
    function of its arguments.

    **Only the query that types no variable is remembered**, which is the one that descent makes. A
    caller-supplied typing is state this module does not own: two callers that type an occurrence
    differently may not share an answer, so an entry would have to be keyed on the callable, which
    is comparable only by identity — and a bound method fails that against itself, so the table
    would be written and never read. Keeping one alive to compare against is worse than useless: a
    typing reaches the pass that wrote it and so the tree, and a table whose keys are weak so that a
    tree it answered for can be collected would then hold that tree through its own values.
    """
    if node is None:
        return NOTHING
    if type_of_variable is not None:
        return _evaluated(node, type_of_variable)
    global _EVALUATED_AT
    epoch = mutation_epoch()
    if epoch != _EVALUATED_AT:
        _EVALUATIONS.clear()
        _EVALUATED_AT = epoch
    remembered = _EVALUATIONS.get(node)
    if remembered is not None:
        return remembered
    outcome = _evaluated(node, None)
    if mutation_epoch() == epoch:
        _EVALUATIONS[node] = outcome
    return outcome


#: What `evaluate` has already answered for a node, and the mutation counter those answers stand on.
#: The counter is one number for the whole table rather than one per entry because it invalidates
#: every entry at once, and the table is emptied rather than left to be stepped over. Keys are weak
#: so that a tree nothing else holds is still collected; nothing here refers back to one.
_EVALUATIONS: WeakKeyDictionary[Node, Ps1Outcome] = WeakKeyDictionary()
_EVALUATED_AT = -1


def _evaluated(
    node: Node,
    type_of_variable: Ps1VariableTyping | None,
) -> Ps1Outcome:
    """
    `evaluate` itself, with the remembering stripped off. Every recursive step goes back through
    `evaluate` rather than calling this directly, because an operand asked about here is the same
    operand a caller asks about on its own, and the two must share what they found.
    """
    literal = read(node)
    if literal is not UNKNOWN:
        return Ps1Outcome(False, literal)
    if isinstance(node, Ps1ParenExpression):
        return evaluate(node.expression, type_of_variable)
    if isinstance(node, (Ps1ArrayLiteral, Ps1ArrayExpression)):
        return _array(node, lambda element: evaluate(element, type_of_variable))
    if isinstance(node, Ps1SubExpression):
        return _subexpression(node, lambda element: evaluate(element, type_of_variable))
    if isinstance(node, Ps1CastExpression):
        return _evaluated_cast(node, type_of_variable)
    if isinstance(node, Ps1BinaryExpression):
        return _evaluated_binary(node, type_of_variable)
    if isinstance(node, Ps1UnaryExpression):
        return _evaluated_unary(node, type_of_variable)
    if isinstance(node, Ps1TypeExpression) or not isinstance(node, Expression):
        return NOTHING
    named = resolve_expression_type(node, type_of_variable)
    return NOTHING if named is None else Ps1Outcome(True, Ps1Typed(named))


def _evaluated_cast(
    node: Ps1CastExpression,
    type_of_variable: Ps1VariableTyping | None,
) -> Ps1Outcome:
    """
    A cast, which is `convert` over whatever its operand evaluates to.

    An operand that names no type at all is the one case `convert` cannot be asked about, because
    the grid it reads is indexed by the source's type and there is no row to look in. Only there
    does the cast answer on its own, and what it answers is its target — naming one is what a cast
    does, so `[int] $s` is an Int32 or it throws whatever `$s` holds. Which targets that is true of
    is read off the grid rather than assumed; see `_cast_names`.

    Anywhere else `convert` has the last word, including where it names no value: `[byte] 300`
    throws for the value in hand, which is a stronger thing to know than *a Byte or a throw*, and
    reaching for the target there would trade it away.
    """
    target = resolve_type(node.type_name)
    if target is None:
        return NOTHING
    operand = evaluate(node.operand, type_of_variable)
    if operand.value is not UNKNOWN:
        converted = convert(operand.value, target)
        return Ps1Outcome(operand.may_throw or converted.may_throw, converted.value)
    named = _cast_names(target)
    return NOTHING if named is None else Ps1Outcome(True, named)


def _evaluated_unary(
    node: Ps1UnaryExpression,
    type_of_variable: Ps1VariableTyping | None,
) -> Ps1Outcome:
    """
    A unary operator, which is `apply_unary` over whatever its operand evaluates to.
    """
    operand = _operand(node.operand, type_of_variable)
    applied = apply_unary(node.operator, operand.value)
    return Ps1Outcome(operand.may_throw or applied.may_throw, applied.value)


def _evaluated_binary(
    node: Ps1BinaryExpression,
    type_of_variable: Ps1VariableTyping | None,
) -> Ps1Outcome:
    """
    An operator, which is `apply` over both of its operands.

    A short-circuiting operator is answered here rather than excepted, and the answer is weaker than
    the host's on purpose. `-and` and `-or` are measured, so `apply` has a cell for them; what no
    cell can carry is that the right operand may never run at all. So `$false -and (1 / 0)` is
    reported as a value that may throw where 5.1 answers `$false` and cannot throw. The error is a
    fold refused and never a wrong answer: a throw is over-claimed rather than dropped, and the
    right operand it was claimed for is the one whose value is unknown, which refuses the cell
    anyway. Reading the left operand alone where it settles the result is a fold this does not take.
    """
    left = _operand(node.left, type_of_variable)
    right = _operand(node.right, type_of_variable)
    applied = apply(node.operator, left.value, right.value)
    return Ps1Outcome(
        left.may_throw or right.may_throw or applied.may_throw, applied.value)


def _operand(
    node: Node | None,
    type_of_variable: Ps1VariableTyping | None,
) -> Ps1Outcome:
    """
    What an operand of an operator produces, which is `evaluate` over it except where `read_operand`
    says a numeral standing there loses the places it was written with.
    """
    numeral = _folded_numeral(node)
    if numeral is not None:
        return Ps1Outcome(False, numeral)
    return evaluate(node, type_of_variable)


@functools.cache
def _cast_names(target: Ps1TypeName) -> Ps1Fact | None:
    """
    The type a cast to `target` produces whatever it is given, or `None` where it does not always
    produce one. A cast is the one operation whose result type is settled by what was written rather
    than by what the operand held, which makes this the only thing the domain can say about a value
    it knows nothing about — and it is read off the grid's whole column rather than assumed, because
    `[array] $null` is `$null`, measured, so `[array]` is the one target a value passes through
    untouched and therefore the one that names no type.

    A source row that only ever threw contributes no type and contradicts none: `[char] @(1, 2)`
    throws, and `[char] $x` is a Char or a throw all the same.
    """
    named: set[Ps1TypeName] = set()
    for source in _GRID_TYPES:
        cell = conversion_outcome(target, source)
        if cell is None or cell.may_be_null:
            return None
        named |= cell.types
    return Ps1Typed(next(iter(named))) if len(named) == 1 else None


def _cast_throws_are_modelled(target: Ps1TypeName) -> bool:
    """
    Whether every way the grid recorded a cast to `target` throwing is one `_cast` checks for
    itself. A cast to an integer type or to a `Char` throws when the value does not fit, which is
    the range `_cast` refuses; a cell for any other target that recorded a throw threw for a reason
    nothing here models.
    """
    return target in _INTEGER_RANGE or target == _CHAR


def _collection_is_true(elements: tuple[Ps1Fact, ...]) -> bool | None:
    """
    Whether a collection counts as true, or `None` where this cannot say.

    How many elements it holds decides it, and only a collection of exactly one asks what is inside.
    Measured: `@()` is `$False`, `@(0, 0)` is `$True` although both of its elements are zero, and
    `@(0)` is `$False` because a collection of one is as true as the thing it holds.
    """
    if len(elements) != 1:
        return len(elements) > 0
    return _element_is_true(elements[0])


def _element_is_true(fact: Ps1Fact) -> bool | None:
    """
    Whether the single element of a one-element collection counts as true, or `None` where this
    cannot say.

    **This is not the same question as `[bool]` of that element**, and two measured pairs say so:
    `(,[char]0)` is `$True` where `[char]0` is `$False`, and `(,(,0))` is `$True` where `(,0)` is
    `$False`. 5.1 reaches a different function here, one with no `Char` arm at all — so every Char
    is true to it — and one that answers a collection by whether it holds anything rather than by
    asking this question again. The 5.1 source says in a comment that it declines to recurse on
    purpose, to bound the work.

    Everything else is the ordinary conversion, which is why this asks `_cast` for it rather than
    spelling a second copy of it out.

    An element that is not a value is refused rather than guessed at. A collection's payload does
    not guarantee its elements are values — `@() + (0 -shl $true)` holds a type and no value — and
    the function 5.1 uses here ends in `return true`, which would report such an element as true.
    """
    if fact is NULL:
        return False
    if not isinstance(fact, Ps1Constant):
        return None
    nested = _elements(fact)
    if nested is not None:
        return len(nested) >= 1
    if fact.type == _CHAR:
        return True
    truth = _cast(_BOOLEAN, fact)
    return truth if isinstance(truth, bool) else None


def _cast(target: Ps1TypeName, fact: Ps1Fact) -> _Number | None:
    """
    The value a cast produces, or `None` where this module computes nothing for it.

    A `Char` is a number to everything that reads one — `[int][char]65` is 65, measured — and it is
    a value to everything that renders one, `[string][char]65` being `A`. It is fed to `Boolean` as
    that number and to no other remaining target: `[bool][char]0` is `$False` and `[bool][char]65`
    and `[bool][char]'0'` are `$True`, all measured, which is the code point against zero and so is
    what the number already answers. The rest still have no row and are still refused.

    A real is rounded half to even on its way to an integer, which is what a host does rather than
    what a truncation would: `[int]1.5` and `[int]2.5` are both 2, `[int]1.4` is 1 and `[int]-1.5`
    is -2, all measured.

    A `Double` reaches `String` through `_double_text` but not `Decimal`: widening one to a Decimal
    through Python would carry the binary expansion of a value the host converts by its decimal digits.

    A `String` is read by `_from_string`, which is a different oracle from every other source and
    reaches only the targets whose throws this module already sees.

    `$null` converts as the zero of whatever it is cast to, which is the same thing `_kernel` makes
    of it in an arithmetic context and is measured here for every target the domain names:
    `[int]$null` and `[decimal]$null` are zero, `[char]$null` is the NUL character and `[bool]$null`
    is `$False`, each of them what the same cast of `0` produces. The one exception is `String`,
    where `[string]$null` is empty and `[string]0` is `0` — an absent value writes nothing, and the
    zero it computes as is not a zero it renders as.
    """
    if fact is NULL:
        return '' if target == _STRING else _cast(target, Ps1Constant(_INT32, 0))
    if not isinstance(fact, Ps1Constant):
        return None
    if target == _STRING:
        return _rendered(fact)
    if fact.type == _STRING and isinstance(fact.payload, str):
        return _from_string(target, fact.payload)
    if target == _BOOLEAN:
        elements = _elements(fact)
        if elements is not None:
            return _collection_is_true(elements)
    number = _numeric_source(fact)
    if number is None:
        return None
    if target == _BOOLEAN:
        return number != 0
    if target == _CHAR:
        rounded = _rounded(number)
        return None if rounded is None else _character(rounded)
    if target in _INTEGER_RANGE:
        rounded = _rounded(number)
        return None if rounded is None else _within(_INTEGER_RANGE[target], rounded)
    if fact.type == _CHAR:
        return None
    if target == _DOUBLE:
        return float(number)
    if target == _DECIMAL and not isinstance(number, float):
        return decimal.Decimal(number)
    return None


#: The whitespace a cast strips off a String before reading a number out of it. Measured: `' 5 '`,
#: a leading or trailing tab, a carriage return and a newline each convert to 5. A string of nothing
#: but whitespace is not the empty one — `[int]''` is 0 and `[int]'   '` throws — so this trims a
#: text that has something in it and never decides what an empty one is.
_CAST_TRIM = ' \t\r\n'

#: The two spellings a cast reads a number out of a String by, which are not the spellings a numeral
#: has in source. Measured: a plain decimal with an optional sign and an optional fraction converts
#: (`[int]'007'` is 7, `[int]'7.5'` is 8, `[int]'.5'` is 0, `[int]'5.'` is 5, `[int]'+7'` is 7), and
#: a hexadecimal one without a sign converts (`[byte]'0x80'` is 128).
#:
#: Everything else this module refuses rather than reads, because the two oracles 5.1 has for a
#: String disagree and neither is Python's. `[int]'1e3'` is 1000 and `[byte]'1e3'` throws, while
#: `1 + '1e3'` is the Double 1001; `[int]'1kb'` throws although `'1kb' * 1` is 1024; and
#: `[int]'1_0'`, `[int]'0b1010'` and `[int]'0o17'` throw where Python's own `int` reads all three.
_CAST_DECIMAL = re.compile(r'[+-]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)\Z')
_CAST_HEX = re.compile(r'0[xX][0-9a-fA-F]+\Z')


def _from_string(target: Ps1TypeName, text: str) -> _Number | None:
    """
    The value a cast of the String `text` to `target` produces, or `None` where this module computes
    nothing for it.

    The targets are the ones whose every throw `convert` already sees, so that reading a String is
    not also a claim about what else the cast might do: an integer width and a `Char` throw when the
    value does not fit, which is checked here, and a `Boolean` does not throw at all. A `Double`,
    a `Single` or a `Decimal` from a String is left to the grid, which answers *that type or a
    throw* — the parse that would be needed there accepts an exponent and a thousands separator,
    and what the host does with one that overflows was not measured.

    A `Boolean` is `False` for the empty String and `True` for every other, `'0'`, `'False'` and
    `' '` included: it is the length that decides and never the text. A `Char` is the one character
    a one-character String holds and a throw for every other length, `''` included.

    A number is read at the *target's* width rather than at its own, which is what makes a
    hexadecimal String a bit pattern: measured, `[byte]'0x80'` is 128 and `[sbyte]'0x80'` is -128,
    `[uint16]'0xFFFF'` is 65535 and `[int]'0xFFFFFFFF'` is -1, and one digit more than the width
    holds throws — `[byte]'0x100'` does. A decimal String is not a pattern and keeps its sign, so
    `[byte]'-1'` throws where `[byte]'0x80'` does not.
    """
    if target == _BOOLEAN:
        return text != ''
    if target == _CHAR:
        if len(text) != 1:
            raise _Throws
        return text
    bounds = _INTEGER_RANGE.get(target)
    if bounds is None:
        return None
    if not text:
        return 0
    digits = text.strip(_CAST_TRIM)
    if _CAST_HEX.match(digits):
        return _pattern_at_width(bounds, int(digits[2:], 16))
    if not _CAST_DECIMAL.match(digits):
        return None
    rounded = _rounded(decimal.Decimal(digits))
    return None if rounded is None else _within(bounds, rounded)


def _pattern_at_width(bounds: tuple[int, int], magnitude: int) -> int:
    """
    The value a bit pattern of `magnitude` denotes in a register of the width `bounds` describes,
    with the sign that width has. A pattern too wide for the register is a throw rather than a
    truncation.
    """
    low, high = bounds
    span = high + 1 if low == 0 else (high + 1) * 2
    if magnitude >= span:
        raise _Throws
    return magnitude if magnitude <= high else magnitude - span


def _numeric_source(fact: Ps1Constant) -> int | float | decimal.Decimal | None:
    """
    The number a cast reads this value as, or `None` for a value no cast here computes from. The
    payload is checked against the type the fact carries rather than trusted, because the two are
    only ever built together here and a mismatch is a defect rather than a case.
    """
    payload = fact.payload
    if fact.type == _BOOLEAN and isinstance(payload, bool):
        return int(payload)
    if fact.type in _INTEGER_RANGE and isinstance(payload, int):
        return payload
    if fact.type == _CHAR and isinstance(payload, str) and len(payload) == 1:
        return ord(payload)
    if fact.type == _DECIMAL and isinstance(payload, decimal.Decimal):
        return payload
    if fact.type == _DOUBLE and isinstance(payload, float):
        return payload
    return None


def _double_text(payload: float) -> str | None:
    """
    The text 5.1 writes a `Double` as, which is the default `Double.ToString()` of the .NET Framework
    PowerShell 5.1 runs on: fifteen significant digits rounded half to even, in fixed-point notation
    while the exponent stays inside them and scientific notation outside. That is C's `%.15g` once its
    `e` is raised to an `E`, the two agreeing on the rounding, the fixed/scientific threshold and the
    two-digit-minimum exponent. Measured: `[string]0.5` is `0.5`, `[string]1E20` is `1E+20`,
    `[string]0.0000001` is `1E-07`, and `[string]` of the Double `9223372036854775808` is
    `9.22337203685478E+18` — each the value `%.15g` gives.

    A negative zero is refused rather than guessed, its `0` and `-0` spellings not being measured, so a
    withheld fold stands in. A non-finite value never reaches a Double fact — `_finite` keeps it out —
    so it is refused here only defensively.
    """
    if payload != payload or payload in (INFINITY, -INFINITY):
        return None
    if payload == 0.0 and math.copysign(1.0, payload) < 0:
        return None
    return F'{payload:.15g}'.replace('e', 'E')


def _rendered(fact: Ps1Constant) -> str | None:
    """
    The text a cast to `String` produces. Measured: `[string]5` is `5`, `[string]$true` is `True`,
    `[string]10d` is `10`, `[string]1.50d` keeps its trailing zero and `[string][char]65` is `A`.
    A String is its own text, which is the identity `[string]'foo'` is.

    It is also the text a *concatenation* contributes the value, which is one question and not two:
    measured, `'a' + $true` is `aTrue` and `'a' + 1.50d` is `a1.50`, each the same as writing
    `[string]` against the operand. See `_concatenated`.

    A `Decimal` is written in plain notation rather than by `str`, which switches to an exponent
    wherever the number is spelled with a positive one: `[string]1e3d` is `1000` on the host and was
    `1E+3` here, which is a text no `Decimal` .NET writes ever takes.

    A `Double` is written by `_double_text`. That the cast is culture-invariant is what lets this
    module compute it — `[string]0.5` is `0.5` on every host — where the `ToString()` a Double answers
    is not, which is why `invariant_text` refuses one. An `Object[]` is absent for the reason that
    survives: a collection is separated by `$OFS`, which lives in the session.
    """
    if fact.type == _STRING:
        return fact.payload if isinstance(fact.payload, str) else None
    if fact.type == _DECIMAL:
        return format(fact.payload, 'f') if isinstance(fact.payload, decimal.Decimal) else None
    if fact.type == _DOUBLE:
        return _double_text(fact.payload) if isinstance(fact.payload, float) else None
    if fact.type in _INTEGER_RANGE or fact.type in (_CHAR, _BOOLEAN):
        return str(fact.payload)
    return None


def _rounded(number: int | float | decimal.Decimal) -> int | None:
    """
    The integer a number converts to, half to even, or `None` for one that has no integer at all.
    """
    if isinstance(number, int):
        return number
    try:
        return round(number)
    except (OverflowError, ValueError):
        return None


def _character(code: int) -> str:
    if not 0 <= code <= 0xFFFF:
        raise _Throws
    return chr(code)


def _within(bounds: tuple[int, int], value: int) -> int:
    low, high = bounds
    if not low <= value <= high:
        raise _Throws
    return value


def _grid_type(fact: Ps1Fact) -> Ps1TypeName | None:
    """
    The type a fact is looked up under in the grid. `$null` has none, and the capture recorded its
    row under `System.Void` — the one name in the grid that is not a type any value carries.
    """
    if fact is NULL:
        return _VOID
    return type_of(fact)


def _spans(*facts: Ps1Fact) -> bool:
    """
    Whether every operand is of a type the grid's witnesses reach every outcome of, so that the cell
    they index may be read as what the operation *does* rather than as what a capture *saw*. See
    `_SPANNED` for which types those are and what it took to find out.
    """
    return all(_grid_type(fact) in _SPANNED for fact in facts)


def _cell_value(cell) -> Ps1Fact:
    """
    The fact a cell's recorded outcomes name. One type and no `$null` beside it is a typed value;
    `$null` and no type at all is `$null`, which is a value and not an absence — `$null * 5` really
    is `$null`, and reading that cell as *unknown* would leave a caller to guess where a measurement
    had already answered. Anything wider names nothing, because a caller cannot act on a value that
    might be either of two types.
    """
    if not cell.types and cell.may_be_null:
        return NULL
    if len(cell.types) == 1 and not cell.may_be_null:
        return Ps1Typed(next(iter(cell.types)))
    return UNKNOWN


def _from_binary_cell(cell, spanned: bool) -> Ps1Outcome:
    """
    What a binary cell says on its own, with nothing computed from the values.

    An operator's result type is decided by the operands' values as much as by their types — that is
    the whole reason a cell is a set — so a cell whose operands the witnesses do not span is read as
    nothing at all. Not its type, which was measured to be a lower bound and not a bound; not its
    silence about throwing, which is a lower bound in the same way and is wrong in 93 cells; and not
    its `$null`, which is a claim about a value like any other.
    """
    if not spanned:
        return NOTHING
    return Ps1Outcome(cell.may_throw, _cell_value(cell))


def _from_conversion_cell(cell, spanned: bool) -> Ps1Outcome:
    """
    What a conversion cell says on its own, which is more than a binary cell says, because a cast's
    result type is settled by what was *written*: a cast produces a value assignable to its target
    or it throws, whatever the operand held. Measured, and not assumed from the shape of a cast:
    every target's cells carry exactly that target, and the one exception is `[array]`, whose
    accelerator names an abstract type and whose cells carry the one concrete array type it builds.

    So a source the witnesses do not span keeps the type and loses what the witnesses were the only
    evidence for — that the cast cannot throw, and that it answers `$null`.
    """
    named = _cell_value(cell)
    if spanned:
        return Ps1Outcome(cell.may_throw, named)
    return Ps1Outcome(True, named if isinstance(named, Ps1Typed) else UNKNOWN)


#: The .NET `TypeCode` each type the domain computes in carries. The numbers are not an ordering
#: this module chose: 5.1 promotes an arithmetic pair by taking the *larger of the two type codes*
#: and running the application in the arithmetic that code selects, so the ordinals are the rule.
#: `System.Void` has no entry because `$null` never reaches the promotion: `+` answers a null left
#: operand with the right one *as it stands*, and `*` answers it with null, both decided before any
#: promotion runs. `_promotion` refuses such a pair and the measured cell answers it.
#:
#: `System.Single` is absent although it has a code of 13, because no fact carries one: `render`
#: cannot spell a Single, so nothing reaches here with that type and an entry would be a rule about
#: a value this module never holds. A Single operand is refused by the lookup below, which is the
#: same answer it gets today.
_TYPE_CODE: dict[Ps1TypeName, int] = {
    _BOOLEAN: 3,
    _CHAR: 4,
    _SBYTE: 5,
    _BYTE: 6,
    _INT16: 7,
    _UINT16: 8,
    _INT32: 9,
    _UINT32: 10,
    _INT64: 11,
    _UINT64: 12,
    _DOUBLE: 14,
    _DECIMAL: 15,
}

#: The integer types whose values can be negative, which is the question the promotion asks when a
#: signed operand meets an unsigned one wide enough to need the answer.
_SIGNED_INTEGERS = frozenset({_SBYTE, _INT16, _INT32, _INT64})

#: What each arithmetic produces, and what it produces instead when the result leaves that type.
#: An integer arithmetic widens to a `Double` — never to a `Decimal`, whatever the grid cell holds,
#: because the widening is `(double)result` in every one of the four integer kernels. A `Decimal`
#: arithmetic does not widen at all: it raises, and the domain reports that as a throw.
_PROMOTED_RESULT: dict[str, tuple[Ps1TypeName, Ps1TypeName | None]] = {
    'int': (_INT32, _DOUBLE),
    'uint': (_UINT32, _DOUBLE),
    'long': (_INT64, _DOUBLE),
    'ulong': (_UINT64, _DOUBLE),
    'decimal': (_DECIMAL, None),
    'double': (_DOUBLE, None),
}


def _promotion(left: Ps1Fact, right: Ps1Fact) -> str | None:
    """
    Which arithmetic 5.1 runs `left <op> right` in, or `None` where the pair is not one this
    promotion covers.

    The larger of the two type codes selects it, with one question left to the values: where the
    wider operand is an unsigned integer and the other is a signed one, a *negative* signed value
    cannot be represented there, so the application widens instead — to `Int64` beside a `UInt32`
    and to `Decimal` beside a `UInt64`. That is the one place the answer depends on a value rather
    than a type, and it is why the measured grid cannot carry it: the cell over `Int32` and
    `UInt64` holds `Decimal`, `Double` and `UInt64` at once, and which of them a pair takes is
    settled here.
    """
    if left is NULL or right is NULL:
        # `$null` does not reach the promotion at all: `+` answers a null left with the *right
        # operand itself*, so `$null + $true` is the Boolean `$true` and not the integer 1, and `*`
        # answers a null left with null. Those are decided before any promotion runs, and the
        # measured cell already carries them.
        return None
    codes = []
    for fact in (left, right):
        # The promotion is over what a string *coerces to*, not over `String`: measured,
        # `1 + '2147483648'` is an Int64 and `1 + '1.5L'` is an Int64 3, because the numeral the
        # string spells is the operand the type codes are compared over.
        coerced = _coerced_numeral(fact)
        name = type_of(fact if coerced is None else coerced)
        code = None if name is None else _TYPE_CODE.get(name)
        if code is None:
            return None
        codes.append(code)
    top = max(codes)
    if top <= _TYPE_CODE[_INT32]:
        return 'int'
    if top == _TYPE_CODE[_UINT32]:
        return 'long' if _is_negative(left) or _is_negative(right) else 'uint'
    if top == _TYPE_CODE[_INT64]:
        return 'long'
    if top == _TYPE_CODE[_UINT64]:
        return 'decimal' if _is_negative(left) or _is_negative(right) else 'ulong'
    if top == _TYPE_CODE[_DECIMAL]:
        return 'decimal'
    return 'double'


def _is_negative(fact: Ps1Fact) -> bool:
    """
    Whether a signed integer operand carries a negative value. Only a signed integer is asked,
    because the promotion asks this of nothing else: an unsigned operand cannot be negative, and a
    floating or `Decimal` one is already wider than the question.
    """
    if not isinstance(fact, Ps1Constant) or fact.type not in _SIGNED_INTEGERS:
        return False
    return isinstance(fact.payload, int) and fact.payload < 0


def _promoted(value: _Number, promotion: str) -> Ps1Fact:
    """
    The fact an arithmetic produced, stamped with the type that arithmetic answers in rather than
    with one read out of a grid cell. An integer that has left its own range is the widening the
    kernel performs, and a `Decimal` that has left its range is not a fact at all — the host raises
    there, and `_decimal_result` has already reported it.
    """
    natural, widened = _PROMOTED_RESULT[promotion]
    if isinstance(value, bool):
        return Ps1Constant(_BOOLEAN, value)
    if isinstance(value, int) and natural in _INTEGER_RANGE:
        low, high = _INTEGER_RANGE[natural]
        if low <= value <= high:
            return Ps1Constant(natural, value)
        return UNKNOWN if widened is None else _double(value)
    if isinstance(value, decimal.Decimal):
        return Ps1Constant(_DECIMAL, value) if natural is _DECIMAL else UNKNOWN
    if isinstance(value, float):
        if natural is not _DOUBLE and widened is not _DOUBLE:
            return UNKNOWN
        return UNKNOWN if _finite(value) is None else Ps1Constant(_DOUBLE, value)
    return UNKNOWN


def _stamped(value: _Number, candidates: frozenset[Ps1TypeName]) -> Ps1Fact:
    """
    The fact a computed value has, given the types the grid recorded for the cell it came out of.

    A `Decimal` and a `Double` are stamped with themselves, and refused where the cell did not
    record that type: a computed `Decimal` reported as a `Double` would be a value the operation
    never had.

    An integer is stamped with the one integer candidate that holds it. *One* is the whole rule:
    where two of them do — a cell such as `SByte + UInt32`, whose outcome set holds both `Int64` and
    `UInt32` because which one a pair takes depends on the signs — nothing here can say which, and
    the value is refused rather than guessed. An integer no candidate holds takes `Decimal` or
    `Double` when the cell recorded one, which is the widening a host performs on overflow and the
    reason `2147483647 + 1` is a Double.

    **The dispatch is exhaustive on purpose, and a payload of a kind not named here is refused.**
    What stood at the end was `Double` for anything that reached it, which read as the arm for a
    `float` and was in fact the arm for *everything else too* — so a kernel returning a collection
    would have had it stamped `Ps1Constant(System.Double, (1, 2))`, a value of a type it is not, by
    the one function whose whole job is to refuse exactly that. Nothing returns such a payload
    today, which is what made it a trap rather than a defect: this is the guard every new kernel arm
    is licensed by, so it has to fail closed for the kinds those arms will introduce.

    The final refusal is unreachable while `_Number` names no collection, and a type checker says
    so. That is the invariant rather than dead code: widening `_Number` is what a kernel arm over
    collections has to do first, and the refusal is what that widening then lands on until an arm
    here is written for the kind it added.
    """
    if isinstance(value, bool):
        return Ps1Constant(_BOOLEAN, value) if _BOOLEAN in candidates else UNKNOWN
    if isinstance(value, int):
        holders = [
            name for name, low, high in _INTEGER_WIDTHS
            if name in candidates and low <= value <= high
        ]
        if len(holders) == 1:
            return Ps1Constant(holders[0], value)
        if holders:
            return UNKNOWN
        if _DECIMAL in candidates and _DECIMAL_MIN <= value <= _DECIMAL_MAX:
            return Ps1Constant(_DECIMAL, decimal.Decimal(value))
        return _double(value) if _DOUBLE in candidates else UNKNOWN
    if isinstance(value, decimal.Decimal):
        return Ps1Constant(_DECIMAL, value) if _DECIMAL in candidates else UNKNOWN
    if isinstance(value, str):
        holders = [name for name in (_CHAR, _STRING) if name in candidates]
        return Ps1Constant(holders[0], value) if len(holders) == 1 else UNKNOWN
    if isinstance(value, float):
        return Ps1Constant(_DOUBLE, value) if _DOUBLE in candidates else UNKNOWN
    if isinstance(value, tuple):
        return Ps1Constant(_OBJECT_ARRAY, value) if _OBJECT_ARRAY in candidates else UNKNOWN
    return UNKNOWN


def _kernel(operator: str, left: Ps1Fact, right: Ps1Fact) -> _Number | None:
    """
    The value an application produces, or `None` where this module computes nothing for it. The
    arithmetic is computed only over operands that are integers of the domain's own widths, or a
    Decimal or a Double beside one: a String or a collection reaches the grid for its type and stops
    there, so that no arithmetic here is performed in a Python type that is not what PowerShell was
    using. The one thing a String does compute is `+`, which over a String or a Char left operand is
    a concatenation and not arithmetic at all — see `_concatenated`.

    `$null` computes as the integer zero, which is what a host converts it to in an arithmetic
    context: `10 - $null` is 10, `$null - 5` is -5 and `$null -band 1` is 0, all measured. It is the
    grid that decides whether the context is arithmetic at all, so a `$null` reaching an operator
    that does something else with it never gets here.

    A shift is computed only over an `Int32` or `Int64` left operand, because the count is masked by
    the *left operand's width* and only those two widths are documented. That the width comes from
    the type and not from how large the value happens to be is the point: a small value in a wide
    variable is still shifted at the wide mask.

    A bitwise operator is computed only over integers. PowerShell will bitwise a Double by rounding
    it first, which is a conversion, and a kernel that reached for Python's operators there would be
    performing a different one.
    """
    if operator in ('-shl', '-shr'):
        if not _is_domain_integer(left) or not _is_domain_integer(right):
            return None
        left_type = type_of(left)
        width = None if left_type is None else _SHIFT_WIDTHS.get(left_type)
        if width is None:
            return None
        count = _integer_payload(right) & (width - 1)
        return _shifted(_integer_payload(left), count, width, operator == '-shl')
    if operator in _BITWISE:
        operands = [_bitwise_operand(fact) for fact in (left, right)]
        if operands[0] is None or operands[1] is None:
            return None
        return _BITWISE[operator](operands[0], operands[1])
    if operator == '+':
        if _concatenates(left):
            return _concatenated(left, right)
        if left is NULL and isinstance(right, Ps1Constant):
            # `$null` on the left of `+` is answered with the right operand *as it stands*, so
            # `$null + @(1, 2)` is that collection and not a longer one — measured two elements,
            # where `@(1, 2) + $null` is three. Nothing is added and nothing is converted.
            return right.payload
    if _elements(left) is not None and operator in ('+', '*'):
        return _collected_operand(operator, left, right)
    if operator == '*' and _replicated(left):
        # A String on the left of `*` is repeated, not multiplied: `'5' * 2` is the String `55` and
        # not the number 10. Nothing here computes that repetition, so the pair is declined before
        # it can be read as arithmetic.
        return None
    if operator in _COMPARISON_SPELLINGS:
        elements = _elements(left)
        if elements is not None:
            return _filtered(operator, elements, right)
        return _compared(operator, left, right)
    operands = _numeric_pair(left, right)
    if operands is None:
        return None
    a, b = operands
    if operator == '/':
        if b == 0:
            return _divided_by_zero(b)
        if isinstance(a, int) and isinstance(b, int) and a % b == 0:
            return a // b
        if isinstance(a, decimal.Decimal) and isinstance(b, decimal.Decimal):
            quotient = _decimal_quotient(a, b)
            return None if quotient is None else _decimal_result(quotient)
        return _decimal_result(operator_module.truediv(a, b))
    if operator == '%':
        if b == 0:
            return _divided_by_zero(b)
        if isinstance(a, int) and isinstance(b, int):
            remainder = abs(a) % abs(b)
            return -remainder if a < 0 else remainder
        if isinstance(a, decimal.Decimal) and isinstance(b, decimal.Decimal):
            return _decimal_result(_decimal_remainder(a, b))
        return _finite(math.fmod(a, b))
    arithmetic = _ARITHMETIC.get(operator)
    return None if arithmetic is None else _computed(arithmetic, a, b)


def _divided_by_zero(divisor: _Number) -> _Number | None:
    """
    What dividing by a zero produces, which is not one answer: an integer or a `Decimal` divisor of
    zero throws, and a floating one does not. Measured on both counts — the `/` and `%` cells over
    `Int32` and over `Decimal` each recorded a throw, and the ones over `Double` and `Single`
    recorded none although `0.0` is among the witnesses the capture divided by.

    So a float names no value here rather than a throw: what a host produces is an infinity or a
    `NaN`, and `_finite` is where the domain says it does not carry one. Raising instead would have
    reported `1.5 / 0.0` as an operation that may throw, which is a claim about the one axis a
    caller acts on and it is false.
    """
    if isinstance(divisor, float):
        return None
    raise _Throws


#: The types a value is a text of. A `Char` is one of them wherever an operator's *left* operand
#: decides what the operation is: `'a' + 1` is the String `a1` and `[char]65 + 1` is `A1`, measured,
#: where `1 + 'a'` throws and `1 + [char]65` is the number 66. That a Char is a text on one side of
#: an operator and a number on the other is the whole of what makes the Char erasure a wrong *value*
#: rather than only a wrong type.
_TEXTUAL = (_STRING, _CHAR)


def _is_text(fact: Ps1Fact) -> typing.TypeGuard[Ps1Constant]:
    """
    Whether a fact is a text this module holds.
    """
    return isinstance(fact, Ps1Constant) and fact.type in _TEXTUAL


def _concatenates(fact: Ps1Fact) -> typing.TypeGuard[Ps1Constant]:
    """
    Whether a left operand of `+` joins text rather than adding, which is the counterpart of
    `_replicated` for the other operator its left operand decides.

    It is a question of its own rather than a line inside `_concatenated`, because whether a `+` joins
    text or adds is decided by the *left* operand alone, not by whether the tail can be spelled. A
    tail `_concatenated` declines — an `Object[]` the session's `$OFS` separates, a right operand that
    is not a constant — is still a concatenation, and reading its refusal as *this is not one* would
    let `'a' + @(1, 2)` fall through to the arithmetic, a wrong reading where a host joins the text.
    """
    return _is_text(fact)


def _concatenated(left: Ps1Constant, right: Ps1Fact) -> str | None:
    """
    The text `+` joins when its *left* operand is a String or a Char, or `None` where this module
    computes nothing for it. Which operands those are is `_concatenates`, which the caller has
    already asked.

    The right operand contributes what a cast of it to `String` would, `$null` contributing nothing:
    `'a' + $null` is `a`, measured. A value `_rendered` refuses is refused here for its own reason —
    an `Object[]` because `$OFS` separates it and lives in the session, a right operand that is not a
    constant because there is no value to spell.
    """
    head = _rendered(left)
    if head is None:
        return None
    if right is NULL:
        return head
    if not isinstance(right, Ps1Constant):
        return None
    tail = _rendered(right)
    return None if tail is None else head + tail


def _numeric_pair(left: Ps1Fact, right: Ps1Fact):
    """
    The two payloads as Python numbers that compute in the same way PowerShell's promoted pair does,
    or `None` where they do not. An integer beside a Decimal computes as a Decimal and an integer
    beside a Double as a Double, which is what the promotion does; a Decimal beside a Double is
    refused, because Python will not mix them and choosing one to convert would be performing the
    promotion rather than reading it.
    """
    kinds = []
    values: list[int | float | decimal.Decimal] = []
    for operand in (left, right):
        coerced = _coerced_numeral(operand)
        if coerced is not None:
            if not isinstance(coerced, Ps1Constant):
                raise _Throws
            operand = coerced
        fact = operand
        if _is_domain_integer(fact):
            kinds.append('i')
            values.append(_integer_payload(fact))
            continue
        number = _char_code(fact)
        if number is None:
            number = _truth_value(fact)
        if number is not None:
            kinds.append('i')
            values.append(number)
            continue
        if not isinstance(fact, Ps1Constant):
            return None
        if fact.type == _DECIMAL and isinstance(fact.payload, decimal.Decimal):
            kinds.append('m')
        elif fact.type == _DOUBLE and isinstance(fact.payload, float):
            kinds.append('f')
        else:
            return None
        values.append(fact.payload)
    if 'm' in kinds and 'f' in kinds:
        return None
    if 'm' in kinds:
        return decimal.Decimal(values[0]), decimal.Decimal(values[1])
    if 'f' in kinds:
        return float(values[0]), float(values[1])
    return values[0], values[1]


def _decimal_remainder(a: decimal.Decimal, b: decimal.Decimal) -> decimal.Decimal:
    """
    `a % b` over two `Decimal`s, at the precision the intermediate quotient needs rather than the one
    the result does. Python reaches a remainder through that quotient and raises `InvalidOperation`
    where it does not fit the context, so the ambient 28 digits refuse `[decimal]::MaxValue % 1.5d` —
    measured a `Decimal` 0 on the host, and raised out of `apply` and into the caller here until this
    was given room. The remainder itself is smaller than the divisor and needs no room at all.
    """
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        return a % b


#: What an operation over two `Decimal`s has to be computed at. Python's operators are *context*
#: operations and the ambient precision is 28 digits, where a `System.Decimal` is a 96-bit
#: coefficient and holds 29: at the default context `79228162514264337593543950335d + 0d` came back
#: as `79228162514264337593543950340`, a wrong value reported as a definite one, and anything else
#: in the process moving `decimal.getcontext().prec` moved it again. The room here is for the
#: *exact* result, so that what the type holds is decided by `_decimal_result` and
#: `_decimal_quotient` rather than by a rounding whose rule nothing here measured. Both operands are
#: inside the `Decimal` range, so a product is at most 58 digits, a sum at most 58 places, and the
#: intermediate quotient a remainder is reached through at most the largest over the smallest.
_DECIMAL_ARITHMETIC = decimal.Context(prec=120)


def _computed(
    arithmetic: Callable[[typing.Any, typing.Any], _Number],
    a: _Number,
    b: _Number,
) -> _Number | None:
    """
    An arithmetic result, with a `Decimal` operand computed at the precision the type has rather
    than the one the process happens to be set to. See `_DECIMAL_ARITHMETIC`.
    """
    if not isinstance(a, decimal.Decimal) and not isinstance(b, decimal.Decimal):
        return _decimal_result(arithmetic(a, b))
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        return _decimal_result(arithmetic(a, b))


def _decimal_result(value: _Number) -> _Number | None:
    """
    A computed number, with a `Decimal` that has left the range of a `Decimal` reported as the throw
    it is. Python carries such a value without complaint; .NET does not have it, so neither does the
    domain, and calling it a throw is what the host does rather than a refusal.

    A `Decimal` the type cannot hold *exactly* is refused instead. .NET rounds such a result to the
    96 bits and 28 places it has, by a rule no measurement here covers, so computing it at a
    precision that carries the exact answer and then reporting whatever Python's own rounding made
    of it would be a value of our invention. See `_holds_exactly`.
    """
    if isinstance(value, decimal.Decimal):
        if not value.is_finite():
            return None
        if not _DECIMAL_MIN <= value <= _DECIMAL_MAX:
            raise _Throws
        if not _holds_exactly(value):
            return None
    return _finite(value)


def _holds_exactly(value: decimal.Decimal) -> bool:
    """
    Whether a `System.Decimal` is the number this `Decimal` is, rather than a rounding of it. The
    type is a coefficient of at most 96 bits scaled by a power of ten between zero and twenty-eight,
    and both halves of that are asked here: a result computed at `_DECIMAL_ARITHMETIC`'s precision
    can carry more places than the type has, and one inside the range the caller already tested can
    still spell more digits than the coefficient holds — `9.9999999999999999999999999999` is smaller
    than a `Decimal`'s largest value and is not a `Decimal`.
    """
    spelling = value.as_tuple()
    if not isinstance(spelling.exponent, int) or spelling.exponent < -28:
        return False
    coefficient = 0
    for digit in spelling.digits:
        coefficient = coefficient * 10 + digit
    return coefficient <= _DECIMAL_MAX


#: The smallest step a `System.Decimal` takes, which is what a quotient the type cannot hold exactly
#: has to be rounded onto.
_DECIMAL_STEP = decimal.Decimal(1).scaleb(-28)


def _decimal_quotient(a: decimal.Decimal, b: decimal.Decimal) -> decimal.Decimal | None:
    """
    `a / b` over two `Decimal`s, or `None` where this module computes nothing for it.

    A quotient the type holds exactly is that quotient — measured, `79228162514264337593543950335d /
    1d` prints in full. One it does not is rounded onto the twenty-eight places the type has, which
    is what a host does with `1d / 3d` and its `0.3333333333333333333333333333`. Both are computed
    at `_DECIMAL_ARITHMETIC` rather than at the ambient precision: dividing under
    `decimal.getcontext()` made `1d / 3d` the number `0.3` in a process that had set `prec` to one,
    a wrong constant folded into a script by a setting that has nothing to do with PowerShell.

    **A quotient that lands exactly between two of those places is refused**, because which way .NET
    breaks that tie is not something anything here measured. Asking is cheap and exact: round it both
    ways, and answer only where the two agree, which is every quotient whose discarded remainder is
    not a half.
    """
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        exact = a / b
        if not _DECIMAL_MIN <= exact <= _DECIMAL_MAX:
            raise _Throws
        if _holds_exactly(exact):
            return exact
        down = exact.quantize(_DECIMAL_STEP, rounding=decimal.ROUND_HALF_DOWN)
        up = exact.quantize(_DECIMAL_STEP, rounding=decimal.ROUND_HALF_UP)
    return down if down == up else None


def _finite(value: _Number) -> _Number | None:
    """
    A computed number, unless it is one no literal spells. An overflow to infinity is a value
    PowerShell has and this domain deliberately does not carry, because every use of it downstream
    would have to refuse anyway and a fact that cannot be spelled is worse than no fact.
    """
    if isinstance(value, float) and (value != value or value in (INFINITY, -INFINITY)):
        return None
    return value


def _is_domain_integer(fact: Ps1Fact) -> bool:
    if fact is NULL:
        return True
    return isinstance(fact, Ps1Constant) and isinstance(fact.payload, int) and not isinstance(
        fact.payload, bool) and any(name == fact.type for name, _, _ in _INTEGER_WIDTHS)


def _integer_payload(fact: Ps1Fact) -> int:
    return 0 if fact is NULL else typing.cast(int, typing.cast(Ps1Constant, fact).payload)


#: What a string is trimmed of before it is read as a number, which is what 5.1 trims: `1 + ' 7 '`
#: is the Int32 8, measured.
_COERCE_TRIM = ' \t\r\n\v\f'


def _coerced_numeral(fact: Ps1Fact) -> Ps1Fact | None:
    """
    The number a `String` operand computes as in an arithmetic context, or `None` for a fact that
    is not a String, or `UNKNOWN` for one that spells no number.

    This is not `convert` to a numeric type and the two genuinely disagree: a coerced string is
    *re-lexed as a numeric literal*, so it keeps that literal's own type and honours the suffixes
    and multipliers a literal has. Measured: `1 + '1kb'` is the Int32 1025 and `1 + '1.5L'` is the
    Int64 3, neither of which a conversion to a named type produces. It is the same reading
    `_numeral` gives a literal in the source, which is what keeps one numeral rule in the module
    rather than two.

    Two things the literal reading does not do on its own. A string is trimmed first, and one that
    is empty or all whitespace is the integer zero rather than nothing — `1 + '  '` is 1. And an
    infinite Double is refused, because a literal may spell one and the coercion may not: measured,
    `1 + '1e400'` throws where `'1e400' + 1` joins text.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _STRING:
        return None
    if not isinstance(fact.payload, str):
        return UNKNOWN
    trimmed = fact.payload.strip(_COERCE_TRIM)
    if not trimmed:
        return Ps1Constant(_INT32, 0)
    read_as = _numeral(trimmed)
    if isinstance(read_as, Ps1Constant) and isinstance(read_as.payload, float):
        if _finite(read_as.payload) is None:
            return UNKNOWN
    return read_as


def _replicated(fact: Ps1Fact) -> bool:
    """
    Whether a left operand of `*` is repeated rather than multiplied.
    """
    return isinstance(fact, Ps1Constant) and fact.type in (_STRING, _OBJECT_ARRAY)


def _elements(fact: Ps1Fact) -> tuple[Ps1Fact, ...] | None:
    """
    The facts a collection holds, or `None` for one that is not a collection or does not name its
    elements.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _OBJECT_ARRAY:
        return None
    payload = fact.payload
    if not isinstance(payload, tuple) or not all(isinstance(one, Ps1Fact) for one in payload):
        return None
    return payload


def _collected_operand(operator: str, left: Ps1Fact, right: Ps1Fact) -> _Number | None:
    """
    What a collection on the left of `+` or `*` produces, or `None` where this declines to say.

    Measured: `@(1, 2) + @(3, 4)` is the four-element collection, `@(1, 2) + 5` the three-element
    one, and `@(1, 2) + $null` is **three** elements rather than two, because appending `$null`
    appends an element. `@(1, 2) * 2` repeats and `@(1, 2) * 0` is empty. The collection has to be
    on the left: `5 + @(1, 2)` and `2 * @(1, 2)` both throw, and their cells record it.

    **The repeat count is taken as an `Int32` and throws when it does not fit one.** Measured:
    `@() * [uint64]18446744073709551615` throws an `InvalidCastIConvertible`, where `@() * 5000` is
    the empty collection. The throw is the count's conversion and not the size of anything, which is
    why an empty left operand does not escape it: nothing repeated is nothing, and 5.1 still refuses
    the count before it can say so. `_MAX_COLLECTION` bounds what this builds and is a separate
    question — it bounds the *product*, which is zero for every count when there is nothing to
    repeat, so it is no bound at all here and never was the thing standing in the way.
    """
    elements = _elements(left)
    if elements is None:
        return None
    if operator == '+':
        tail = _elements(right)
        joined = elements + (tail if tail is not None else (right,))
        return None if len(joined) > _MAX_COLLECTION else joined
    if not _is_domain_integer(right):
        return None
    count = _within(_INTEGER_RANGE[_INT32], _integer_payload(right))
    if count < 0 or len(elements) * count > _MAX_COLLECTION:
        return None
    return elements * count


def _bitwise_operand(fact: Ps1Fact) -> int | None:
    """
    The integer a bitwise operator computes over, or `None` for an operand it does not reach one
    from. A Char is its code point and a String is the numeral it spells, both measured:
    `[char]48 -band [byte]255` is 48 and `'10' -band 6` is 2.
    """
    if _is_domain_integer(fact):
        return _integer_payload(fact)
    code = _char_code(fact)
    if code is not None:
        return code
    truth = _truth_value(fact)
    if truth is not None:
        return truth
    coerced = _coerced_numeral(fact)
    if coerced is None:
        return None
    if not isinstance(coerced, Ps1Constant):
        raise _Throws
    return _integer_payload(coerced) if _is_domain_integer(coerced) else None


def _truth_value(fact: Ps1Fact) -> int | None:
    """
    The number a `Boolean` computes as, or `None` for a fact that is not one.

    Measured, a Boolean is an ordinary number to arithmetic: `$true + 1` is the Int32 2, `$false +
    1` is 1, `$true + $true` is 2 and `$true + 1.5` is the Double 2.5. The one operator it is not a
    number to is `*` with the Boolean on its *left* — `$true * 2` throws where `2 * $true` is 2 —
    and that is the cell's throw rather than a rule here, exactly as it is for a Char.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _BOOLEAN:
        return None
    return 1 if fact.payload else 0


def _char_code(fact: Ps1Fact) -> int | None:
    """
    The number a `Char` computes as, or `None` for a fact that is not one.

    A Char is a number to every operator but two, and the two are the ones that claim it first:
    `+` reads a Char *left* operand as text — `[char]65 + 1` is the String `A1` and `1 + [char]65`
    is the Int32 66, both measured — and `_concatenated` answers that before anything here is
    asked; `*` has no operator for a Char at all and its cell records the throw. Everywhere else
    the code point is the operand, which is what makes `[char]65 -bxor 32` the Int32 97 rather than
    a fold nobody takes.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _CHAR:
        return None
    return ord(fact.payload) if isinstance(fact.payload, str) and len(fact.payload) == 1 else None


def _shifted(value: int, count: int, width: int, left: bool) -> int:
    """
    A shift performed in a `width`-bit two's complement register, which is where PowerShell performs
    it: shifting left out of the register discards the bits rather than growing the number, and
    shifting right preserves the sign.
    """
    if not left:
        return value >> count
    span = 1 << width
    result = (value << count) & (span - 1)
    return result - span if result >= span >> 1 else result


#: The value a computed Double reaches on overflow, which the domain does not carry.
INFINITY = float('inf')


def _throws_are_modelled(operator: str, left: Ps1Fact, right: Ps1Fact) -> bool:
    """
    Whether the kernel can see, for these operands, every way the operator throws — so that a cell
    which recorded a throw somewhere may still be computed here.

    Division and remainder throw for a zero divisor and nothing else, and the divisor is in hand.
    Addition, subtraction and multiplication throw only where a `Decimal` result leaves the range of
    a `Decimal`, which `_decimal_result` raises for; over the other numeric types they do not throw
    at all, and a cell of theirs that recorded one is recording something this does not model.

    A `String` operand is the other case, and it is why these cells throw at all: a string reaching
    arithmetic is read as a numeral and one that spells no number raises — `16 + 'file'` throws and
    `1 + '5'` does not, out of the same cell. `_coerced_numeral` is what sees it, so the throw is
    modelled wherever that runs, which is every operator the kernel reads a number for. Without
    this the cell's recorded throw stops the kernel being consulted and every string in arithmetic
    is refused, including the ones the host answers.

    Neither licence is given where the operator throws for what its operands *are*, which is a throw
    no reading of their numbers can see; see `_throws_for_what_the_operands_are`.
    """
    if _throws_for_what_the_operands_are(operator, left, right):
        return False
    if operator in ('/', '%'):
        return True
    if operator == '*' and _elements(left) is not None:
        # The measured throw is the count's conversion rather than any size: a repeat count is taken
        # as an `Int32`, and `@() * [uint64]18446744073709551615` throws an
        # `InvalidCastIConvertible` for one that does not fit, an empty left operand included.
        # `_collected_operand` raises for exactly that count and declines every size it will not
        # build, so the kernel never answers where the host raises, which is what this gate asks; it
        # merely answers less.
        return True
    if _STRING in (type_of(left), type_of(right)):
        return operator in _ARITHMETIC or operator in _BITWISE
    if operator in _ARITHMETIC:
        return _DECIMAL in (type_of(left), type_of(right))
    return False


def _throws_for_what_the_operands_are(operator: str, left: Ps1Fact, right: Ps1Fact) -> bool:
    """
    Whether an operation throws for what its operands *are* rather than for the values they carry.
    Such a throw is invisible to a kernel that reads numbers out of them, so a cell that recorded
    one may not be computed in however well the conversions are modelled.

    Two are measured. A Boolean or a Char on the left of `*` has no multiplication at all — `$true *
    2` and `[char]48 * 2` both raise, where `2 * $true` is 2 and `2 * [char]48` is 96. And a Boolean
    on the left of a `Decimal` raises for `+`, `-`, `/` and `%` — `$true - 1.0d` throws where
    `1.0d - $true` is 0 and `$true - 1.5` is -0.5.

    Both reached the kernel on the licence the *other* operand gave, and both were answered: the
    String licence made `[char]48 * '1'` the number 48 and the `Decimal` licence made `$true - 1.0d`
    a `Decimal` zero, each a value standing where the host aborts the script.
    """
    if operator == '*':
        return type_of(left) in (_BOOLEAN, _CHAR)
    return type_of(left) == _BOOLEAN and type_of(right) == _DECIMAL


_ARITHMETIC = {
    '+': operator_module.add,
    '-': operator_module.sub,
    '*': operator_module.mul,
}

_BITWISE: dict[str, Callable[[int, int], int]] = {
    '-band': int.__and__,
    '-bor': int.__or__,
    '-bxor': int.__xor__,
}


class _Comparison(typing.NamedTuple):
    """
    How one spelling of a comparison operator compares: what it makes of an ordering, whether it is
    an equality — the two an absent or a textual operand are answered for by a rule of their own —
    whether a matching pair is the answer or its negation, and whether the case a text was written
    in counts.
    """

    decides: Callable[[_Number, _Number], bool]
    equality: bool
    negated: bool
    cased: bool


#: Every spelling of a comparison operator, which is a closed set: 5.1 writes each of the six with a
#: `-c` prefix for the comparison the case counts in and an `-i` prefix for the one it does not, and
#: the bare spelling is the case-insensitive one.
_COMPARISON_SPELLINGS: dict[str, _Comparison] = {
    F'-{prefix}{base}': _Comparison(decides, base in ('eq', 'ne'), base == 'ne', prefix == 'c')
    for base, decides in (
        ('eq', operator_module.eq),
        ('ne', operator_module.ne),
        ('lt', operator_module.lt),
        ('le', operator_module.le),
        ('gt', operator_module.gt),
        ('ge', operator_module.ge),
    )
    for prefix in ('', 'c', 'i')
}


def _compared(operator: str, left: Ps1Fact, right: Ps1Fact) -> bool | None:
    """
    What a comparison produces, or `None` where this module computes nothing for it. Every spelling
    is decided here and none of them reaches the arithmetic on its own, because what a comparison
    compares is settled by its operands before any number is read out of them.
    """
    comparison = _COMPARISON_SPELLINGS[operator]
    if left is NULL or right is NULL:
        return _compared_to_absent(comparison, left, right)
    if _compares_as_text(comparison, left, right):
        return _compared_as_text(comparison, left, right)
    if type_of(left) == _BOOLEAN:
        return _compared_as_truth(comparison, left, right)
    operands = _numeric_pair(left, right)
    return None if operands is None else comparison.decides(*operands)


def _filtered(
    operator: str,
    elements: tuple[Ps1Fact, ...],
    right: Ps1Fact,
) -> tuple[Ps1Fact, ...] | None:
    """
    The elements a comparison keeps when its left operand is a collection, or `None` where any one
    of them cannot be decided. 5.1 reads `10, 20, 30 -eq 20` as the elements the scalar comparison
    holds for — `@(20)` — and `10, 20, 30, 20, 10 -ne 20` as `10, 30, 10`, so the same predicate
    that answers the scalar case answers each element here.

    An element the scalar comparison declines withholds the whole result rather than being dropped
    from it: a collection missing the members it could not read is a different collection from the
    one 5.1 builds. Which operators reach here is `apply`'s throw gate to decide and not this — an
    ordering whose cell records a throw never does, so an element that would raise a comparison is
    refused before this filters anything.
    """
    kept: list[Ps1Fact] = []
    for element in elements:
        decided = _compared(operator, element, right)
        if decided is None:
            return None
        if decided:
            kept.append(element)
    return tuple(kept)


def _compared_to_absent(comparison: _Comparison, left: Ps1Fact, right: Ps1Fact) -> bool | None:
    """
    What a comparison with `$null` on one side produces, or `None` where this module computes
    nothing for it.

    **An absent value compares by presence and never by conversion.** `$null -eq $null` is `$True`
    while `$null -eq 0`, `$null -eq ''`, `0 -eq $null` and `'' -eq $null` are all `$False`, measured
    — 5.1 answers a null on either side before it converts anything, so the empty String is not the
    absent value and neither is the zero.

    An *ordering* is the same answer read as an order rather than as a match, and what it orders is
    presence and not the zero a conversion would put there: measured, `$null -lt 0` is `$True` where
    `0 -lt 0` is `$False`, and `$null -ge 100` is `$False`. Where the other operand sits is
    `_sorts_below_absent`.
    """
    if not _is_scalar_value(left) or not _is_scalar_value(right):
        return None
    if left is NULL and right is NULL:
        order = 0
    elif left is NULL:
        order = 1 if _sorts_below_absent(right) else -1
    else:
        order = -1 if _sorts_below_absent(left) else 1
    return comparison.decides(order, 0)


def _sorts_below_absent(fact: Ps1Fact) -> bool:
    """
    Whether a value sorts below `$null` rather than above it, which is what 5.1 asks of the operand
    an absent one is compared against. A *negative* number is below it and everything else is above
    — a zero, a positive number, a text, a truth, an unsigned integer. Measured: `$null -lt 0` is
    `$True` while `$null -lt -5` is `$False` and `$null -gt -5` is `$True`, and `$null -lt ''` is
    `$True`.
    """
    payload = fact.payload if isinstance(fact, Ps1Constant) else None
    return isinstance(payload, (int, float, decimal.Decimal)) and payload < 0


def _is_scalar_value(fact: Ps1Fact) -> bool:
    """
    Whether a fact names one value this module holds. A `Ps1Typed` names a type and no value, so
    whether it is the absent one is exactly what is not known about it; a collection is a value and
    is not one value, and an equality against it filters rather than compares — `@(1, 2) -eq $null`
    is the empty collection, measured, and not `$False`.
    """
    return fact is NULL or (isinstance(fact, Ps1Constant) and fact.type != _OBJECT_ARRAY)


def _compared_as_truth(comparison: _Comparison, left: Ps1Fact, right: Ps1Fact) -> bool | None:
    """
    What a comparison with a Boolean on the left produces, or `None` where this module computes
    nothing for it.

    A Boolean on the left converts the right operand to a Boolean and compares the two truths, which
    is neither the number a Boolean is to arithmetic nor the text it writes: measured, `$true -eq 2`
    and `$true -eq '0'` are both `$True` — every non-zero number and every non-empty text is the
    truth the left operand already is — while `$true -lt 2` is `$False`, because two operands that
    convert to the same truth are neither below nor above one another. Reading the Boolean as its
    number answered all three the other way round.
    """
    here = _truth_value(left)
    if here is None or not isinstance(right, Ps1Constant):
        return None
    try:
        there = _cast(_BOOLEAN, right)
    except _Throws:
        return None
    return None if not isinstance(there, bool) else comparison.decides(here, int(there))


def _compares_as_text(comparison: _Comparison, left: Ps1Fact, right: Ps1Fact) -> bool:
    """
    Whether a comparison joins two texts rather than two numbers, which its *left* operand decides.

    A String on the left converts the right operand to a String, so it is a text comparison whatever
    that operand is: measured, `'1.0' -eq 1` is `$False` — `1` is written `1` and does not match
    `1.0` — against `1 -eq '1.0'`, which is `$True` because there the number decides and the text is
    read as one.

    A Char on the left is a text only where the question is whether the two are equal. 5.1 answers
    that by their characters and ignores the case unless the spelling says otherwise —
    `[char]65 -eq [char]97` is `$True` and `[char]65 -ceq [char]97` is `$False`, measured — where it
    *orders* two Chars by their code points, so `[char]97 -lt [char]66` is `$False` and a collation
    of `a` against `B` would answer `$True`. What a Char converts the right operand to is a Char,
    which is the character a number spells and not the text it writes: `[char]48 -eq 48` is `$True`,
    measured, so only a Char beside another text is compared as one.
    """
    if not _is_text(left):
        return False
    if left.type == _STRING:
        return True
    return comparison.equality and _is_text(right)


def _compared_as_text(comparison: _Comparison, left: Ps1Fact, right: Ps1Fact) -> bool | None:
    """
    What a comparison of two texts produces, or `None` where this module computes nothing for it.

    An *ordering* is refused. 5.1 orders two texts by `CompareInfo.Compare`, which is a collation and
    not the arithmetic below: measured, `'10' -lt '9'` is `$True` and `'2' -lt '10'` is `$False`,
    both of which reading the numerals answers the other way.

    An equality is answered from the text each operand writes, with the case counting only where the
    spelling says it does: `[char]48 -eq '0'` and `[char]65 -eq [char]97` are both `$True` while
    `[char]65 -ceq [char]97` is `$False`, measured.

    **A pair this decides is not equal is refused where either text leaves ASCII**, because the
    comparison 5.1 makes is a collation there too and it calls texts equal that no reading of their
    code points does: `'ss' -eq [char]0x00DF` is `$True`, measured, and so is a text against the
    same text with a soft hyphen in it. Equality the other way round survives the boundary — two
    texts this reads as the same are the same text or a case variant of it, which a collation under
    `IgnoreCase` agrees with — so what the boundary costs is a refusal and never an answer.
    """
    if not comparison.equality:
        return None
    if not isinstance(left, Ps1Constant) or not isinstance(right, Ps1Constant):
        return None
    head, tail = _rendered(left), _rendered(right)
    if head is None or tail is None:
        return None
    same = head == tail if comparison.cased else head.lower() == tail.lower()
    if not same and not (head.isascii() and tail.isascii()):
        return None
    return same != comparison.negated


#: The literal suffix that pins a spelled number to its type, for the types that have one. The set
#: is the whole of what 5.1 has: `l` names an Int64 and `d` a Decimal, and the rest of the suffixes
#: a reader may expect — `y`, `uy`, `s`, `us`, `u`, `ul`, `n` — arrived in 6.2 and 7.0.
_LITERAL_SUFFIX = {_INT32: '', _INT64: 'L', _DECIMAL: 'd'}

#: The cast a value is written under where the language spells no literal of its type. Each is
#: measured: `[byte] 5` is a Byte, `[sbyte] -5` an SByte, `[uint64] 18446744073709551615` a UInt64
#: and `[char] 65` the Char `A`. A decimal numeral is the operand every one of them converts from
#: without loss, including the values above `Int64`, which reach the cast as a Decimal literal.
#:
#: `System.Single` is absent because the domain names no constant of it: no literal spells one,
#: no width row holds one and nothing stamps one, so a value that would need this entry cannot
#: be built.
_CAST_SPELLING = {
    _BYTE: 'byte',
    _SBYTE: 'sbyte',
    _INT16: 'int16',
    _UINT16: 'uint16',
    _UINT32: 'uint32',
    _UINT64: 'uint64',
}

#: The types a cast *spells* rather than converts to, which is the six widths above and the `Char`
#: `_rendered_character` writes the same way. One set keys both directions — `render` writes a cast
#: for exactly these and `read` reads one back for exactly these — so neither can grow without the
#: other and `read(render(fact)) == fact` cannot quietly stop holding.
_SPELLED_BY_A_CAST = frozenset(_CAST_SPELLING) | {_CHAR}


def render(fact: Ps1Fact) -> Expression | None:
    """
    The expression that spells this value. **A value always has one**: a literal where the language
    has a literal of its type, and the cast of one where it does not, so that a caller holding a
    `Ps1Constant` never has to choose between leaving the source alone and spelling something else.

    `None` is therefore not a refusal to spell a value: it is the answer for a fact that *names*
    no value. `UNKNOWN` and `Ps1Typed` are the two, and beside them stand a payload that does not
    carry its own type, which is a malformed fact rather than a value, and a `Double` that is not
    finite. Infinity and NaN have no literal and no cast that reaches them, and the domain does
    not carry one either — `_finite` refuses a computed one — so that last refusal is unreachable
    rather than a gap.

    A number is spelled with its sign attached to the digits, which is the spelling that keeps its
    type: `-2147483648` is one literal that fits Int32, and a caller putting the result somewhere a
    parenthesis would separate the two has changed an Int32 into an Int64. Where a *slot* reads that
    spelling as something else — a command argument reads a leading dash as part of a word, and a
    cast written bare there is one word too — it is the slot that brackets it, in
    `refinery.lib.scripts.ps1.synth`, because only the slot knows what stands beside it.
    """
    if fact is NULL:
        return null_expression()
    if not isinstance(fact, Ps1Constant):
        return None
    payload = fact.payload
    if fact.type == _BOOLEAN:
        return Ps1Variable(name='True' if payload else 'False')
    if fact.type == _STRING:
        return make_string_literal(payload) if isinstance(payload, str) else None
    if fact.type == _CHAR:
        return _rendered_character(payload)
    if fact.type == _OBJECT_ARRAY:
        return _rendered_array(payload) if isinstance(payload, tuple) else None
    if fact.type == _DOUBLE:
        return _rendered_double(payload)
    if isinstance(payload, bool) or not isinstance(payload, (int, decimal.Decimal)):
        return None
    suffix = _LITERAL_SUFFIX.get(fact.type)
    if suffix is not None:
        if fact.type == _DECIMAL:
            return Ps1RealLiteral(raw=F'{payload}{suffix}')
        return Ps1IntegerLiteral(raw=F'{payload}{suffix}')
    target = _CAST_SPELLING.get(fact.type)
    if target is None:
        return None
    return Ps1CastExpression(type_name=target, operand=Ps1IntegerLiteral(raw=str(payload)))


def folded_binary(left: Expression, operator: str, right: Expression) -> Expression | None:
    """
    The expression `left operator right` folds to, or `None` where the pair is not constant or the
    operation may throw.

    The operands are read exactly as `evaluate` reads the two sides of any binary expression, so a
    fold here agrees with folding the same operator written out longhand: the value a compound
    assignment `$x op= e` leaves is `$x op e`, and this is what lets the short spelling reach the
    same constant the long one does. Each operand is copied before it is read, because the throwaway
    node built to hold them adopts the children it is handed, and a fold must leave the tree it read
    from untouched whether or not a caller installs the result.
    """
    combined = Ps1BinaryExpression(
        left=_clone_node(left),
        operator=operator,
        right=_clone_node(right),
    )
    outcome = evaluate(combined)
    return None if outcome.may_throw else render(outcome.value)


def folded_increment(previous: Expression, delta: int) -> Expression | None:
    """
    The value `$x++` or `$x--` leaves in `$x`, given its previous value `previous` and a `delta` of
    `+1` or `-1`, or `None` where that value is not constant or the increment throws.

    `++` and `--` are not the binary `$x + 1` and `$x - 1`: they require a number and add the delta
    to it, where `+` and `-` would concatenate a String, coerce one, or read a Boolean as an integer
    — none of which the increment does. 5.1 answers the delta itself for `$null` and throws
    `OperatorRequiresNumber` for a String, a Char, a Boolean or a collection, so this folds only over
    `$null` and the numeric types and refuses the rest, standing no value where 5.1 raised. Over a
    number the increment *is* the binary sum, which is why `folded_binary` computes it once the
    operand is one — and `$null`, which `_is_domain_integer` reads as the zero the sum needs.
    """
    fact = read(previous)
    numeric = _is_domain_integer(fact) or (
        isinstance(fact, Ps1Constant) and fact.type in (_DECIMAL, _DOUBLE)
    )
    if not numeric:
        return None
    return folded_binary(previous, '+' if delta > 0 else '-', Ps1IntegerLiteral(raw='1'))


def _rendered_character(payload) -> Expression | None:
    """
    A `Char`, written as the cast of its code point. The one-character String that carries the same
    payload is a different value and not a shorter spelling of this one: measured, the two differ in
    the type they report, in what `-is [char]` answers, in which String methods they have and in
    what `[int]` makes of them.
    """
    if not isinstance(payload, str) or len(payload) != 1:
        return None
    return Ps1CastExpression(type_name='char', operand=Ps1IntegerLiteral(raw=str(ord(payload))))


def _rendered_array(elements: tuple[Ps1Fact, ...]) -> Expression | None:
    """
    A collection, spelled by the comma operator that builds exactly it. `@()` is the empty form
    and nothing else, because it collects what a pipeline unrolls rather than what was written:
    measured, `@(@(1, 2))` is a two-element array where `,(1, 2)` is a one-element array holding
    one, and `(1, 2), 3` is the two-element array with an array in it.

    One element that names no value refuses the whole collection: a shorter array than the script
    builds is a different value, and there is no element to stand in for the one that was dropped.
    """
    if not elements:
        return Ps1ArrayExpression(body=[])
    spelled: list[Expression] = []
    for element in elements:
        one = render(element)
        if one is None:
            return None
        spelled.append(one)
    return Ps1ArrayLiteral(elements=spelled)


def _rendered_double(payload) -> Expression | None:
    if not isinstance(payload, float) or payload != payload or payload in (INFINITY, -INFINITY):
        return None
    return Ps1RealLiteral(raw=repr(payload))


def make_string_literal(value: str) -> Ps1StringLiteral | Ps1HereString:
    """
    The literal that spells `value` as a `String`, for a caller that holds a bare Python `str` and
    no fact. It is `render`'s String arm, and it is the last place in the unit where a value is
    spelled without its type having been named — a `str` reaching here becomes a String whatever it
    was, which is what the ledger's Char rows were. Each caller loses this as it starts carrying a
    `Ps1Fact` instead; what is left is the emulation of a .NET method that really does produce a
    String, and a text this module computed itself.

    A here-string is chosen for multi-line text because it needs no escaping, and only where the
    text cannot close it early: a line beginning `'@` inside the value would end the string there
    and let the rest of it be read as script.
    """
    has_newline = '\n' in value
    has_nonprint = any(c in value for c in _NONPRINT_CONTROL)
    herestring_safe = not value.startswith("'@") and "\n'@" not in value
    if has_newline and not has_nonprint and herestring_safe:
        return Ps1HereString(value=value, raw=F"@'\n{value}\n'@")
    if has_nonprint or has_newline:
        escaped = value.replace('`', '``').replace('"', '`"').replace('$', '`$')
        for ch, esc in BACKTICK_ENCODE.items():
            escaped = escaped.replace(ch, esc)
        return Ps1StringLiteral(value=value, raw=F'"{escaped}"')
    if "'" not in value:
        raw = F"'{value}'"
    elif '"' not in value and '$' not in value and '`' not in value:
        raw = F'"{value}"'
    else:
        raw = "'" + value.replace("'", "''") + "'"
    return Ps1StringLiteral(value=value, raw=raw)

Global variables

var Ps1VariableTyping

What a caller knows about the type of a variable occurrence. A function and not a table keyed by name, because a name is not a variable: two bodies may write the same name, and which write a read observes is a question only the caller's flow model can answer. type_at() is the one implementation of it, and it is what keeps this module free of flow while still letting a pass answer per occurrence.

var NOTHING

The refusal, named once so that the several places that decline read alike. It claims nothing on either axis — no value, and no freedom from a throw.

var INFINITY

The value a computed Double reaches on overflow, which the domain does not carry.

Functions

def is_truthy(node)

Whether an expression counts as true to PowerShell, or None where this cannot say.

This is 5.1's conversion to a Boolean and nothing else, which is the one predicate if, while, for, do, -and, -or, -xor, -not and a [bool] cast all reach. It is answered by reading the value the expression names and converting that, rather than by a rule spelled out here, so that a spelling this module has never seen is refused instead of being given a truth of its own invention.

A rule spelled out here is what this used to be, and - '0' is the case that ended it: a minus sign in front of a String converts the String to a number, so the text '0' is true while the number it negates to is false. The rule read the minus as leaving truth alone — which holds for a number and for nothing else — and answered True where a host answers False.

Both throws are refused, and they are two. evaluate() says whether reaching the value may throw and convert() says whether making a Boolean of it may, and the second does not carry the first: $null * [int]'abc' evaluates to a definite $null that may throw, and converting that $null alone is a $False that cannot. Reading only the conversion would report a truth for an expression a host never finishes, and a caller that prunes on it would delete the throw.

Expand source code Browse git
def is_truthy(node: Node | None) -> bool | None:
    """
    Whether an expression counts as true to PowerShell, or `None` where this cannot say.

    This is 5.1's conversion to a `Boolean` and nothing else, which is the one predicate `if`,
    `while`, `for`, `do`, `-and`, `-or`, `-xor`, `-not` and a `[bool]` cast all reach. It is
    answered by reading the value the expression names and converting *that*, rather than by a rule
    spelled out here, so that a spelling this module has never seen is refused instead of being
    given a truth of its own invention.

    A rule spelled out here is what this used to be, and `- '0'` is the case that ended it: a minus
    sign in front of a String converts the String to a number, so the text `'0'` is true while the
    number it negates to is false. The rule read the minus as leaving truth alone — which holds for
    a number and for nothing else — and answered `True` where a host answers `False`.

    **Both throws are refused, and they are two.** `evaluate` says whether reaching the value may
    throw and `convert` says whether making a Boolean of it may, and the second does not carry the
    first: `$null * [int]'abc'` evaluates to a definite `$null` that may throw, and converting that
    `$null` alone is a `$False` that cannot. Reading only the conversion would report a truth for an
    expression a host never finishes, and a caller that prunes on it would delete the throw.
    """
    if node is None:
        return None
    outcome = evaluate(node, None)
    if outcome.may_throw or outcome.value is UNKNOWN:
        return None
    converted = convert(outcome.value, _BOOLEAN)
    if converted.may_throw or not isinstance(converted.value, Ps1Constant):
        return None
    return converted.value.payload if isinstance(converted.value.payload, bool) else None
def unwrap_to_array_literal(node)

Unwrap parentheses and array expressions to find an inner Ps1ArrayLiteral.

Expand source code Browse git
def unwrap_to_array_literal(node: Node) -> Ps1ArrayLiteral | None:
    """
    Unwrap parentheses and array expressions to find an inner
    `refinery.lib.scripts.ps1.model.Ps1ArrayLiteral`.
    """
    node = unwrap_parens(node)
    if isinstance(node, Ps1ArrayLiteral):
        return node
    if isinstance(node, Ps1ArrayExpression) and len(node.body) == 1:
        stmt = node.body[0]
        if isinstance(stmt, Ps1ExpressionStatement) and isinstance(stmt.expression, Ps1ArrayLiteral):
            return stmt.expression
    return None
def collect_facts(node)

The values an expression names, as facts, or None where it names anything else. A scalar is a list of one, which is what a caller reading a command's or an operator's operand wants: PowerShell hands one value and a collection of one to the same place.

This is the only place the elements of a collection are taken apart, and every caller that wants something of each of them — an integer, a text, a number-or-text — asks the element that question itself. A collector per question would each have to state again which spellings build a collection, and read() already knows: @(1, 2), (1, 2), 1, 2 and a cast over any of them are one answer here.

Expand source code Browse git
def collect_facts(node: Node | None) -> list[Ps1Fact] | None:
    """
    The values an expression names, as facts, or `None` where it names anything else. A scalar is a
    list of one, which is what a caller reading a command's or an operator's operand wants:
    PowerShell hands one value and a collection of one to the same place.

    This is the only place the elements of a collection are taken apart, and every caller that wants
    something *of* each of them — an integer, a text, a number-or-text — asks the element that
    question itself. A collector per question would each have to state again which spellings build a
    collection, and `read` already knows: `@(1, 2)`, `(1, 2)`, `1, 2` and a cast over any of them
    are one answer here.
    """
    fact = read(node)
    if isinstance(fact, Ps1Constant) and fact.type == _OBJECT_ARRAY:
        payload = fact.payload
        return None if not isinstance(payload, tuple) else list(payload)
    return None if fact is UNKNOWN else [fact]
def collect_integers(node)

The integers an expression names, as a list, or None where it names anything else.

What counts as an integer is integer_of(), so a numeral whose spelling makes it something else is not one and neither is a $null: the old reader here answered the magnitude of a hexadecimal pattern, so 0xFFFFFFFF reached its callers as 4294967295 where the value is -1.

Expand source code Browse git
def collect_integers(node: Node | None) -> list[int] | None:
    """
    The integers an expression names, as a list, or `None` where it names anything else.

    What counts as an integer is `integer_of`, so a numeral whose spelling makes it something else
    is not one and neither is a `$null`: the old reader here answered the *magnitude* of a
    hexadecimal pattern, so `0xFFFFFFFF` reached its callers as 4294967295 where the value is -1.
    """
    return _each(collect_facts(node), integer_of)
def collect_texts(node)

The texts an expression's values contribute where PowerShell coerces each of them to a String, or None where one of them names no text. See coerced_text() for what that coercion is and which operators perform it.

Expand source code Browse git
def collect_texts(node: Node | None) -> list[str] | None:
    """
    The texts an expression's values contribute where PowerShell coerces each of them to a String,
    or `None` where one of them names no text. See `coerced_text` for what that coercion is and
    which operators perform it.
    """
    return _each(collect_facts(node), coerced_text)
def collect_byte_array(node)

The bytes an expression names, or None where it names something that is not a list of them. A number outside a byte is not one, which is a refusal rather than a truncation.

Expand source code Browse git
def collect_byte_array(node: Expression) -> bytes | None:
    """
    The bytes an expression names, or `None` where it names something that is not a list of them.
    A number outside a byte is not one, which is a refusal rather than a truncation.
    """
    numbers = collect_integers(node)
    if numbers is None:
        return None
    try:
        return bytes(numbers)
    except (ValueError, OverflowError):
        return None
def resolve_expression_type(expr, type_of_variable=None)

Trace the .NET type of a PowerShell expression by walking member access chains. Returns the one canonical Ps1TypeName, or None if the type cannot be determined.

A numeral is asked of read() rather than answered here, because how wide a numeral is written decides its type and only the spelling knows: 1L is an Int64, 2147483648 is an Int64, 9223372036854775808 is a Decimal and 1e32 a Double, every one of them measured. Answering System.Int32 for all of them resolved a member against a type the value did not have.

Expand source code Browse git
def resolve_expression_type(
    expr: Expression,
    type_of_variable: Ps1VariableTyping | None = None,
) -> Ps1TypeName | None:
    """
    Trace the .NET type of a PowerShell expression by walking member access chains. Returns the one
    canonical `Ps1TypeName`, or `None` if the type cannot be determined.

    A numeral is asked of `read` rather than answered here, because how wide a numeral is written
    decides its type and only the spelling knows: `1L` is an Int64, `2147483648` is an Int64,
    `9223372036854775808` is a Decimal and `1e32` a Double, every one of them measured. Answering
    `System.Int32` for all of them resolved a member against a type the value did not have.
    """
    unwrapped = unwrap_parens(expr)
    if not isinstance(unwrapped, Expression):
        return None
    expr = unwrapped
    if isinstance(expr, (Ps1StringLiteral, Ps1HereString)):
        return _STRING
    if isinstance(expr, (Ps1IntegerLiteral, Ps1RealLiteral)):
        return type_of(read(expr))
    if isinstance(expr, Ps1ArrayLiteral):
        return _OBJECT_ARRAY
    if isinstance(expr, Ps1ArrayExpression):
        if (
            len(expr.body) == 1
            and isinstance(expr.body[0], Ps1ExpressionStatement)
            and isinstance(expr.body[0].expression, Ps1ArrayLiteral)
        ):
            return _OBJECT_ARRAY
    if isinstance(expr, Ps1Variable):
        if type_of_variable is not None:
            named = type_of_variable(expr)
            if named is not None:
                return named
        declared = VARIABLE_TYPES.get(expr.name.lower())
        return None if declared is None else resolve_type(declared)
    if isinstance(expr, Ps1TypeExpression):
        return resolve_type(expr.name)
    if isinstance(expr, Ps1CastExpression):
        return resolve_type(expr.type_name)
    if isinstance(expr, Ps1CommandInvocation):
        cmd_name = get_command_name(expr)
        if cmd_name is not None:
            cmd_lower = cmd_name.lower()
            if cmd_lower in OBJ_COMMANDS:
                type_str = extract_first_positional_string(expr)
                if type_str is not None:
                    return resolve_type(type_str)
            elif cmd_lower in WMI_COMMANDS:
                class_str = extract_first_positional_string(expr)
                if class_str is not None:
                    return resolve_type(class_str)
    if isinstance(expr, Ps1MemberAccess):
        if expr.object is None:
            return None
        obj_type = resolve_expression_type(expr.object, type_of_variable)
        if obj_type is None:
            return None
        member_name = get_member_name(expr.member)
        if member_name is None:
            return None
        return resolve_member_type(obj_type, member_name)
    return None
def candidate_types(expr, world, type_of_variable=None)

The set of canonical .NET type names the expression's value could have, or the empty set when the type cannot be determined. A static method call contributes the return its overloads agree on, a cmdlet call the output types it declares, and $_ the output types of whatever feeds the pipeline element it is bound in; each can be several, so a caller reasoning about the value must have its conclusion hold for every candidate. The single-type forms — literals, variables, casts, New-Object, WMI, and property chains — are delegated to resolve_expression_type() rather than re-derived here.

world is what decides whether a command name still denotes what the metadata says, so a cmdlet whose name the script has taken over contributes nothing. It is asked at the position of the command it resolves rather than over the whole run, so a name some later statement rebinds still denotes the built-in everywhere no path places that statement first.

Expand source code Browse git
def candidate_types(
    expr: Expression,
    world: Ps1WorldReach,
    type_of_variable: Ps1VariableTyping | None = None,
) -> frozenset[Ps1TypeName]:
    """
    The set of canonical .NET type names the expression's value could have, or the empty set when
    the type cannot be determined. A static method call contributes the return its overloads agree
    on, a cmdlet call the output types it declares, and `$_` the output types of whatever feeds the
    pipeline element it is bound in; each can be several, so a caller reasoning about the value must
    have its conclusion hold for every candidate. The single-type forms — literals, variables,
    casts, `New-Object`, WMI, and property chains — are delegated to `resolve_expression_type`
    rather than re-derived here.

    `world` is what decides whether a command name still denotes what the metadata says, so a cmdlet
    whose name the script has taken over contributes nothing. It is asked at the position of the
    command it resolves rather than over the whole run, so a name some later statement rebinds still
    denotes the built-in everywhere no path places that statement first.
    """
    unwrapped = unwrap_parens(expr)
    if not isinstance(unwrapped, Expression):
        return frozenset()
    expr = unwrapped
    if isinstance(expr, Ps1InvokeMember):
        return _static_method_candidates(expr)
    if isinstance(expr, Ps1CommandInvocation):
        return _command_candidates(expr, world, type_of_variable)
    if _is_pipeline_variable(expr):
        return _pipeline_variable_candidates(expr, world, type_of_variable)
    single = resolve_expression_type(expr, type_of_variable)
    return frozenset() if single is None else frozenset({single})
def null_expression()

The expression that spells $null. This is render()(NULL), given its own name and a precise return type so a caller building a node out of it does not carry render's Expression | None.

Expand source code Browse git
def null_expression() -> Ps1Variable:
    """
    The expression that spells `$null`. This is `render(NULL)`, given its own name and a precise
    return type so a caller building a node out of it does not carry render's `Expression | None`.
    """
    return Ps1Variable(name='Null')
def type_of(fact)

The .NET type a fact carries, or None for UNKNOWN and NULL. None is no type is named here in both cases, and a caller that needs to tell them apart compares against NULL.

Expand source code Browse git
def type_of(fact: Ps1Fact) -> Ps1TypeName | None:
    """
    The .NET type a fact carries, or `None` for `UNKNOWN` and `NULL`. `None` is *no type is named
    here* in both cases, and a caller that needs to tell them apart compares against `NULL`.
    """
    if isinstance(fact, (Ps1Typed, Ps1Constant)):
        return fact.type
    return None
def type_test(fact, target)

Whether value -is target holds for a value this fact describes: True, False, or None where the domain cannot decide. $null answers False for every target — it has no type to be one of — an UNKNOWN fact answers None, and everything else is the relation is_assignable_to() reads off the collected type model from the fact's runtime type. The result of a type test is always a System.Boolean, so unlike the value grid there is no measured cell to stamp the answer against; the only care needed is that a None stays a fold declined rather than becoming a guessed False.

Expand source code Browse git
def type_test(fact: Ps1Fact, target: str | Ps1TypeName) -> bool | None:
    """
    Whether `value -is target` holds for a value this fact describes: `True`, `False`, or `None`
    where the domain cannot decide. `$null` answers `False` for every target — it has no type to be
    one of — an `UNKNOWN` fact answers `None`, and everything else is the relation
    `refinery.lib.scripts.ps1.data.is_assignable_to` reads off the collected type model from the
    fact's runtime type. The result of a type test is always a `System.Boolean`, so unlike the value
    grid there is no measured cell to stamp the answer against; the only care needed is that a `None`
    stays a fold declined rather than becoming a guessed `False`.
    """
    if fact is NULL:
        return False
    runtime = type_of(fact)
    if runtime is None:
        return None
    return is_assignable_to(runtime, target)
def integer_of(fact)

The integer a fact names, or None for a fact that names anything else. This is what a caller holding a fact asks instead of reaching for the payload, and what it refuses is the point: a Boolean carries a Python int and is not one, a $null is an absent value rather than a zero, and a Double or a Decimal that happens to be whole is still not an integer here — a caller that wants the number one of those converts to is asking convert(), which is where the rounding rule lives.

Expand source code Browse git
def integer_of(fact: Ps1Fact) -> int | None:
    """
    The integer a fact names, or `None` for a fact that names anything else. This is what a caller
    holding a fact asks instead of reaching for the payload, and what it refuses is the point: a
    `Boolean` carries a Python `int` and is not one, a `$null` is an absent value rather than a
    zero, and a `Double` or a `Decimal` that happens to be whole is still not an integer here — a
    caller that wants the number one of those *converts* to is asking `convert`, which is where the
    rounding rule lives.
    """
    if not isinstance(fact, Ps1Constant) or fact.type not in _INTEGER_RANGE:
        return None
    payload = fact.payload
    return None if isinstance(payload, bool) or not isinstance(payload, int) else payload
def integer_at(target, value)

value as a value of target, or UNKNOWN where target names no integer width or does not hold it. A width that does not hold a number is a throw rather than a wrap, measured for every one of them, so a caller that folds refuses either way and the two are one answer here.

Expand source code Browse git
def integer_at(target: Ps1TypeName, value: int) -> Ps1Fact:
    """
    `value` as a value of `target`, or `UNKNOWN` where `target` names no integer width or does not
    hold it. A width that does not hold a number is a throw rather than a wrap, measured for every
    one of them, so a caller that folds refuses either way and the two are one answer here.
    """
    bounds = _INTEGER_RANGE.get(target)
    if bounds is None or not bounds[0] <= value <= bounds[1]:
        return UNKNOWN
    return Ps1Constant(target, value)
def pattern_at(target, magnitude)

The value the bit pattern magnitude denotes in a register of target's width, with that width's sign, or UNKNOWN where target names no width or the pattern is wider than it holds.

A pattern is not a magnitude, which is the whole reason this is a separate question: measured, [int]'0xFFFFFFFF' is -1 and [Convert]::ToInt32('FFFFFFFF', 16) is -1, where the digits read as a number are four billion. Two callers with different spellings ask it, so it is stated once.

Expand source code Browse git
def pattern_at(target: Ps1TypeName, magnitude: int) -> Ps1Fact:
    """
    The value the bit pattern `magnitude` denotes in a register of `target`'s width, with that
    width's sign, or `UNKNOWN` where `target` names no width or the pattern is wider than it holds.

    A pattern is not a magnitude, which is the whole reason this is a separate question: measured,
    `[int]'0xFFFFFFFF'` is -1 and `[Convert]::ToInt32('FFFFFFFF', 16)` is -1, where the digits read
    as a number are four billion. Two callers with different spellings ask it, so it is stated once.
    """
    bounds = _INTEGER_RANGE.get(target)
    if bounds is None:
        return UNKNOWN
    try:
        return Ps1Constant(target, _pattern_at_width(bounds, magnitude))
    except _Throws:
        return UNKNOWN
def text_of(fact)

The String a fact names, or None for a fact that names anything else. This is what a caller holding a fact asks instead of reaching for the payload, and the reason it exists is the one distinction the payload cannot make: a Char carries a Python str too, and it is not a String — measured, the two differ in what -is [char] answers, in which methods they have, in what [int] makes of them and in what + does with them on the left. A caller that read the payload would get the same characters back for both.

It is not a spelling. string_value() answers what text a node is written as, which is a syntactic question the analysis layer asks about command names and paths; this answers what text a value is, which only the domain can say.

Expand source code Browse git
def text_of(fact: Ps1Fact) -> str | None:
    """
    The `String` a fact names, or `None` for a fact that names anything else. This is what a caller
    holding a fact asks instead of reaching for the payload, and the reason it exists is the one
    distinction the payload cannot make: a `Char` carries a Python `str` too, and it is not a String
    — measured, the two differ in what `-is [char]` answers, in which methods they have, in what
    `[int]` makes of them and in what `+` does with them on the left. A caller that read the payload
    would get the same characters back for both.

    It is not a spelling. `refinery.lib.scripts.ps1.ast.string_value` answers what text a *node* is
    written as, which is a syntactic question the analysis layer asks about command names and paths;
    this answers what text a *value* is, which only the domain can say.
    """
    if isinstance(fact, Ps1Constant) and fact.type == _STRING and isinstance(fact.payload, str):
        return fact.payload
    return None
def coerced_text(fact)

The text a value contributes where PowerShell coerces it to a String, or None where this module names none. It is convert() to a String and nothing else, which is what makes it a different question from text_of(): that one asks what a value is, this asks what it becomes, and a Char answers None to the first and its character to the second.

Every string operator coerces this way and uniformly, measured over -replace, -split, -join, -f and -match and over both of their operands: 'x' -replace 'x', $true is True, -replace 'x', 1.50d is 1.50, ('a','b') -join 5 is a5b, -join (72, 105) is 72105, [char]65 -replace 'A', 'B' is B and $true -replace 'T', 'X' is Xrue.

A method does not coerce this way and must not ask this: it converts each argument to the parameter's declared type, and the two disagree — 'abc'.Substring([char]1) is bc, where the Char becomes the number one and its text would be a control character that throws.

Expand source code Browse git
def coerced_text(fact: Ps1Fact) -> str | None:
    """
    The text a value contributes where PowerShell coerces it to a String, or `None` where this
    module names none. It is `convert` to a `String` and nothing else, which is what makes it a
    different question from `text_of`: that one asks what a value *is*, this asks what it *becomes*,
    and a Char answers `None` to the first and its character to the second.

    Every string operator coerces this way and uniformly, measured over `-replace`, `-split`,
    `-join`, `-f` and `-match` and over both of their operands: `'x' -replace 'x', $true` is `True`,
    `-replace 'x', 1.50d` is `1.50`, `('a','b') -join 5` is `a5b`, `-join (72, 105)` is `72105`,
    `[char]65 -replace 'A', 'B'` is `B` and `$true -replace 'T', 'X'` is `Xrue`.

    A *method* does not coerce this way and must not ask this: it converts each argument to the
    parameter's declared type, and the two disagree — `'abc'.Substring([char]1)` is `bc`, where the
    Char becomes the number one and its text would be a control character that throws.
    """
    outcome = convert(fact, _STRING)
    return None if outcome.may_throw else text_of(outcome.value)
def invariant_text(fact)

The text a value writes where the current culture renders it, or None where that text is not the one this module computes.

It is coerced_text() narrowed to the values no culture spells differently, and the narrowing is the whole of it. Measured on a host whose culture writes a decimal comma: (1.50d).ToString() is 1,50 and a collection separated by $OFS = 1.5 reads 1,5, where [string]1.50d is 1.50. So a caller reading a value the host formats — a ToString() call, the separator a collection is joined with — computes the right characters only for a Boolean, a Char, a String and the integer widths, each measured to agree.

Expand source code Browse git
def invariant_text(fact: Ps1Fact) -> str | None:
    """
    The text a value writes where the *current culture* renders it, or `None` where that text is not
    the one this module computes.

    It is `coerced_text` narrowed to the values no culture spells differently, and the narrowing is
    the whole of it. Measured on a host whose culture writes a decimal comma: `(1.50d).ToString()`
    is `1,50` and a collection separated by `$OFS = 1.5` reads `1,5`, where `[string]1.50d` is
    `1.50`. So a caller reading a value the *host* formats — a `ToString()` call, the separator a
    collection is joined with — computes the right characters only for a Boolean, a Char, a String
    and the integer widths, each measured to agree.
    """
    found = type_of(fact)
    if found is None or (found not in _CULTURE_FREE and found not in _INTEGER_RANGE):
        return None
    return coerced_text(fact)
def read(node)

What the source pins this expression to, as a fact, or UNKNOWN when it pins nothing. This is the floor the rest of the domain stands on and it refuses rather than invents: an expression it cannot decide, a literal spelled in a way no measurement covers, and a number too wide for any type all answer UNKNOWN, never a value that happens to be close.

Only literal structure is read — literals, the array and parenthesis forms that wrap them, $true, $false and $null, and the casts that are a spelling rather than a conversion, for which see _cast_spelling. An operator is not read at all, so that a caller asking what the source says never receives an answer that came from evaluating something.

A sign is not an exception to that, because the parser has already decided it: a - written directly against a numeral is part of the numeral and reaches this inside raw, while - 2147483648 and -(2147483648) are unary minus over a literal and are refused here. Reaching past the space or the parenthesis to the numeral would report the Int32 that only the glued spelling has; the other two are an operator over a value and belong to apply().

Expand source code Browse git
def read(node: Node | None) -> Ps1Fact:
    """
    What the source pins this expression to, as a fact, or `UNKNOWN` when it pins nothing. This is
    the floor the rest of the domain stands on and it **refuses rather than invents**: an expression
    it cannot decide, a literal spelled in a way no measurement covers, and a number too wide for
    any type all answer `UNKNOWN`, never a value that happens to be close.

    Only literal structure is read — literals, the array and parenthesis forms that wrap them,
    `$true`, `$false` and `$null`, and the casts that are a *spelling* rather than a conversion, for
    which see `_cast_spelling`. An operator is not read at all, so that a caller asking what the
    *source* says never receives an answer that came from evaluating something.

    A sign is not an exception to that, because the parser has already decided it: a `-` written
    directly against a numeral is part of the numeral and reaches this inside `raw`, while
    `- 2147483648` and `-(2147483648)` are unary minus over a literal and are refused here. Reaching
    past the space or the parenthesis to the numeral would report the Int32 that only the glued
    spelling has; the other two are an operator over a value and belong to `apply`.
    """
    if node is None:
        return UNKNOWN
    if isinstance(node, Ps1ParenExpression):
        return UNKNOWN if node.expression is None else read(node.expression)
    if isinstance(node, (Ps1StringLiteral, Ps1HereString)):
        return Ps1Constant(_STRING, node.value)
    if isinstance(node, (Ps1ExpandableString, Ps1ExpandableHereString)):
        return _quoted(node.parts)
    if isinstance(node, (Ps1IntegerLiteral, Ps1RealLiteral)):
        return _numeral(node.raw)
    if is_builtin_variable(node, {'true'}):
        return Ps1Constant(_BOOLEAN, True)
    if is_builtin_variable(node, {'false'}):
        return Ps1Constant(_BOOLEAN, False)
    if is_builtin_variable(node, {'null'}):
        return NULL
    if isinstance(node, (Ps1ArrayLiteral, Ps1ArrayExpression)):
        return _array(node, _pinned).value
    if isinstance(node, Ps1SubExpression):
        return _subexpression(node, _pinned).value
    if isinstance(node, Ps1CastExpression):
        return _cast_spelling(node)
    return UNKNOWN
def read_operand(node)

What an operand written in the source contributes to an operation, which is read() except for a Decimal numeral whose value is a whole number: that one contributes the number without the places it was written with.

5.1 folds a constant expression in its parser, and a numeral reaching that fold carries no scale where it has nothing to hold — measured, 'x' + 1.00d is x1 where 'x' + 1.100d is x1.100, and - 1.0d is -1 where - 1.10d is -1.10. It is each operand that loses its places and not the result: 1.500d + 1.500d is 3.000, a whole number written to three places, because neither addend was one.

Only a numeral, which is what makes this a different question from read(). The same value reached any other way keeps its scale, because then no numeral stands where the parser folds: $z = 1.0d; 'x' + $z is x1.0 and 'x' + [decimal]'1.0' is x1.0, both measured, against the x1 of the numeral written in place. A bare 1.0d is 1.0 for the same reason — there is no operation over it to fold.

That is also why refinery.lib.scripts.ps1.deobfuscation.constants will not carry such a value: inlining one writes a numeral where the source had none, which would move the operand into the fold and take its places away.

Expand source code Browse git
def read_operand(node: Node | None) -> Ps1Fact:
    """
    What an operand *written in the source* contributes to an operation, which is `read` except for
    a `Decimal` numeral whose value is a whole number: that one contributes the number without the
    places it was written with.

    5.1 folds a constant expression in its parser, and a numeral reaching that fold carries no scale
    where it has nothing to hold — measured, `'x' + 1.00d` is `x1` where `'x' + 1.100d` is `x1.100`,
    and `- 1.0d` is `-1` where `- 1.10d` is `-1.10`. It is each *operand* that loses its places and
    not the result: `1.500d + 1.500d` is `3.000`, a whole number written to three places, because
    neither addend was one.

    **Only a numeral**, which is what makes this a different question from `read`. The same value
    reached any other way keeps its scale, because then no numeral stands where the parser folds:
    `$z = 1.0d; 'x' + $z` is `x1.0` and `'x' + [decimal]'1.0'` is `x1.0`, both measured, against the
    `x1` of the numeral written in place. A bare `1.0d` is `1.0` for the same reason — there is no
    operation over it to fold.

    That is also why `refinery.lib.scripts.ps1.deobfuscation.constants` will not carry such a value:
    inlining one *writes* a numeral where the source had none, which would move the operand into the
    fold and take its places away.
    """
    numeral = _folded_numeral(node)
    return read(node) if numeral is None else numeral
def survives_being_written(fact)

Whether a value keeps its meaning when a pass writes it down as a constant where the source had something else. A Decimal whose value is a whole number written to places does not: the source reached it through a variable or a cast, and the numeral standing in for one of those is a numeral the parser folds, which takes the places away. read_operand() is the rule, and this is the same rule asked of a value rather than of a node.

Measured: $z = 1.0d; $z + 0d is 1.0 while the 1.0d + 0d an inliner writes for it is 1. Nothing else in the domain answers False here — every other value has a spelling that reads back as itself wherever it is put, which is what render() and read() being inverses means.

Expand source code Browse git
def survives_being_written(fact: Ps1Fact) -> bool:
    """
    Whether a value keeps its meaning when a pass writes it down as a constant where the source had
    something else. A `Decimal` whose value is a whole number written to places does not: the source
    reached it through a variable or a cast, and the numeral standing in for one of those is a
    numeral the parser folds, which takes the places away. `read_operand` is the rule, and this is
    the same rule asked of a value rather than of a node.

    Measured: `$z = 1.0d; $z + 0d` is `1.0` while the `1.0d + 0d` an inliner writes for it is `1`.
    Nothing else in the domain answers `False` here — every other value has a spelling that reads
    back as itself wherever it is put, which is what `render` and `read` being inverses means.
    """
    if not isinstance(fact, Ps1Constant) or fact.type != _DECIMAL:
        return True
    payload = fact.payload
    if not isinstance(payload, decimal.Decimal) or payload.as_tuple().exponent == 0:
        return True
    with decimal.localcontext(_DECIMAL_ARITHMETIC):
        return payload.to_integral_value() != payload
def fact_of(payload)

The value a Python object denotes where nothing has narrowed it, or UNKNOWN for one that denotes none.

This is read()'s counterpart for a caller holding a value it computed rather than one it found written down — the emulator is that caller, and render() is where what it computed becomes a tree again. A number decides the way an unsuffixed numeral decides, because the narrowest width that holds a magnitude is the only rule the domain has for a bare one.

A str is a String and never a Char. That is not a gap: a payload does not carry a type and the two are the same Python object, which is the whole reason Ps1Constant carries a type beside its payload. A caller that means a Char has to build the fact instead of asking here, and one whose currency cannot tell the two apart is a caller whose Chars are already gone.

A number no literal spells names nothing here, which is _finite's rule read at this boundary too: render() states that a value it is handed always has a spelling, and a fact carrying an infinity would be the one that does not.

Expand source code Browse git
def fact_of(payload: object) -> Ps1Fact:
    """
    The value a Python object denotes where nothing has narrowed it, or `UNKNOWN` for one that
    denotes none.

    This is `read`'s counterpart for a caller holding a value it *computed* rather than one it
    found written down — the emulator is that caller, and `render` is where what it computed
    becomes a tree again. A number decides the way an unsuffixed numeral decides, because the
    narrowest width that holds a magnitude is the only rule the domain has for a bare one.

    **A `str` is a String and never a Char.** That is not a gap: a payload does not carry a type
    and the two are the same Python object, which is the whole reason `Ps1Constant` carries a type
    beside its payload. A caller that means a Char has to build the fact instead of asking here,
    and one whose currency cannot tell the two apart is a caller whose Chars are already gone.

    A number no literal spells names nothing here, which is `_finite`'s rule read at this boundary
    too: `render` states that a value it is handed always has a spelling, and a fact carrying an
    infinity would be the one that does not.
    """
    if payload is None:
        return NULL
    if isinstance(payload, bool):
        return Ps1Constant(_BOOLEAN, payload)
    if isinstance(payload, int):
        return _widest_needed(payload)
    if isinstance(payload, float):
        return UNKNOWN if _finite(payload) is None else _double(payload)
    if isinstance(payload, str):
        return Ps1Constant(_STRING, payload)
    if isinstance(payload, (list, tuple)):
        return _collected(Ps1Outcome(False, fact_of(one)) for one in payload).value
    return UNKNOWN
def apply(operator, left, right)

What left <operator> right produces. The type comes from the measured grid in refinery.lib.scripts.ps1.data, never from a rule written here, and the value from a kernel that is checked against it: a computed value whose type is not one the grid recorded for that cell is refused rather than reported, because the grid is what a host did and the kernel is only what we believe.

A cell that recorded a throw stops the kernel being consulted, unless every way that cell can throw is one the kernel checks for itself — see _throws_are_modelled. Without that exception a single throwing pair anywhere in a cell would cost every other pair in it its fold; with it, a throw the kernel cannot see is still never folded past.

A value the kernel computed is answered without asking whether the witnesses span the operands, and the two halves of that survive the question the cell alone does not. The throw: of the cells the kernel computes in, the eight whose recorded silence about throwing is wrong are all a Decimal subtraction, which _throws_are_modelled already covers — measured against the second capture _SPANNED was found by. The type: an under-recorded set can only make _stamped refuse, because which promotion a pair takes is settled by their types, so the one thing their values decide is overflow, and an overflowed value leaves every candidate rather than landing in the wrong one.

The operator is folded to lower case once, here, because PowerShell's are case-insensitive and a caller holding one out of a script holds whatever case was written. Doing it at the grid lookup alone left -BAND finding its cell and missing the kernel, which is a fold lost to a spelling.

Expand source code Browse git
def apply(operator: str, left: Ps1Fact, right: Ps1Fact) -> Ps1Outcome:
    """
    What `left <operator> right` produces. The *type* comes from the measured grid in
    `refinery.lib.scripts.ps1.data`, never from a rule written here, and the *value* from a kernel
    that is checked against it: a computed value whose type is not one the grid recorded for that
    cell is refused rather than reported, because the grid is what a host did and the kernel is only
    what we believe.

    A cell that recorded a throw stops the kernel being consulted, unless every way that cell can
    throw is one the kernel checks for itself — see `_throws_are_modelled`. Without that exception a
    single throwing pair anywhere in a cell would cost every other pair in it its fold; with it, a
    throw the kernel cannot see is still never folded past.

    A value the kernel computed is answered without asking whether the witnesses span the operands,
    and the two halves of that survive the question the cell alone does not. The *throw*: of the
    cells the kernel computes in, the eight whose recorded silence about throwing is wrong are all a
    `Decimal` subtraction, which `_throws_are_modelled` already covers — measured against the second
    capture `_SPANNED` was found by. The *type*: an under-recorded set can only make `_stamped`
    refuse, because which promotion a pair takes is settled by their types, so the one thing their
    values decide is overflow, and an overflowed value leaves every candidate rather than landing in
    the wrong one.

    The operator is folded to lower case once, here, because PowerShell's are case-insensitive and
    a caller holding one out of a script holds whatever case was written. Doing it at the grid
    lookup alone left `-BAND` finding its cell and missing the kernel, which is a fold lost to a
    spelling.
    """
    operator = operator.lower()
    cell = binary_outcome(operator, *(_grid_type(left), _grid_type(right)))  # type: ignore[misc]
    if cell is None:
        return NOTHING
    if not cell.may_throw or _throws_are_modelled(operator, left, right):
        try:
            computed = _kernel(operator, left, right)
        except _Throws:
            return Ps1Outcome(True, UNKNOWN)
        if computed is not None:
            stamped = _typed_result(operator, computed, left, right, cell.types)
            if stamped is not UNKNOWN:
                return Ps1Outcome(False, stamped)
    return _from_binary_cell(cell, _spans(left, right))
def apply_unary(operator, operand)

What <operator> operand produces.

Unary minus reads the measured grid, exactly as apply() reads the binary one: the type is what a host was observed to produce and the value comes from a kernel checked against it. The capture covers - over all sixteen operand rows, and the cells where it names two types are the ones the value decides between — Int32 negates to an Int32 or, where the result leaves that width, to a Double. Measured: - 5 is the Int32 -5 and - (-2147483648) the Double 2147483648, - [uint32]1 the Double -1 and - [char]65 the Int32 -65. _stamped is what picks among a cell's types by which of them holds the number, so nothing here states that rule twice.

A String is negated by being read as a numeral first, which is where the String row's recorded throw comes from: - '5' is -5, - ' 5 ' is -5, - '' is 0 and - 'abc' throws. That is the same coercion arithmetic performs, so the same reader sees it and the same throws are modelled.

-not is absent because it is not this question: it negates a truth value, which convert() to a Boolean already answers, and is_truthy() is what asks.

-bnot keeps a table of its own, _BNOT_WIDTH, because what it needs is not the result type but the width the complement happens at, and that is a different measurement. Reading the grid for its result type as well is recorded work, not done here.

Expand source code Browse git
def apply_unary(operator: str, operand: Ps1Fact) -> Ps1Outcome:
    """
    What `<operator> operand` produces.

    **Unary minus reads the measured grid**, exactly as `apply` reads the binary one: the *type* is
    what a host was observed to produce and the *value* comes from a kernel checked against it. The
    capture covers `-` over all sixteen operand rows, and the cells where it names two types are the
    ones the value decides between — `Int32` negates to an `Int32` or, where the result leaves that
    width, to a `Double`. Measured: `- 5` is the Int32 -5 and `- (-2147483648)` the Double
    2147483648, `- [uint32]1` the Double -1 and `- [char]65` the Int32 -65. `_stamped` is what picks
    among a cell's types by which of them holds the number, so nothing here states that rule twice.

    A String is negated by being read as a numeral first, which is where the `String` row's recorded
    throw comes from: `- '5'` is -5, `- ' 5 '` is -5, `- ''` is 0 and `- 'abc'` throws. That is the
    same coercion arithmetic performs, so the same reader sees it and the same throws are modelled.

    `-not` is absent because it is not this question: it negates a truth value, which `convert` to
    a `Boolean` already answers, and `is_truthy` is what asks.

    `-bnot` keeps a table of its own, `_BNOT_WIDTH`, because what it needs is not the result type
    but the *width the complement happens at*, and that is a different measurement. Reading the grid
    for its result type as well is recorded work, not done here.
    """
    operator = operator.lower()
    if operator == '-':
        return _negated(operand)
    if operator != '-bnot':
        return NOTHING
    source = _grid_type(operand)
    width = None if source is None else _BNOT_WIDTH.get(source)
    if width is None:
        return NOTHING
    converted = convert(operand, width)
    number = integer_of(converted.value)
    if converted.may_throw or number is None:
        return NOTHING
    low, high = _INTEGER_RANGE[width]
    complement = ~number
    return Ps1Outcome(False, Ps1Constant(width, complement % (high + 1) if low == 0 else complement))
def convert(fact, target)

What [target] fact produces, read from the measured conversion grid exactly as apply() reads the binary one: the type is what a host was observed to produce and the value comes from a kernel checked against it.

A cast throws where the value does not fit its target — measured, [byte]300, [byte]-1, [int]2147483648, [char]65536 and [char]-1 all throw rather than wrapping — and _cast raises for exactly that, so a cell that recorded a throw may still be computed where the target is one whose range the kernel checks. For any other target a recorded throw is one nothing here sees, and the cell answers alone.

A String operand is read by rules of its own, in _from_string, and only the spellings those rules were measured over are computed: .NET parses a String by rules Python does not share, and 5.1 has two of them that disagree with each other. Every other spelling reaches the grid for its type and stops there, which is [int]'abc' still being an Int32 or a throw — see _from_conversion_cell for why a cast may say that where an operator may not.

Expand source code Browse git
def convert(fact: Ps1Fact, target: Ps1TypeName) -> Ps1Outcome:
    """
    What `[target] fact` produces, read from the measured conversion grid exactly as `apply` reads
    the binary one: the *type* is what a host was observed to produce and the *value* comes from a
    kernel checked against it.

    A cast throws where the value does not fit its target — measured, `[byte]300`, `[byte]-1`,
    `[int]2147483648`, `[char]65536` and `[char]-1` all throw rather than wrapping — and `_cast`
    raises for exactly that, so a cell that recorded a throw may still be computed where the target
    is one whose range the kernel checks. For any other target a recorded throw is one nothing here
    sees, and the cell answers alone.

    A `String` operand is read by rules of its own, in `_from_string`, and only the spellings those
    rules were measured over are computed: .NET parses a String by rules Python does not share, and
    5.1 has two of them that disagree with each other. Every other spelling reaches the grid for its
    type and stops there, which is `[int]'abc'` still being *an Int32 or a throw* — see
    `_from_conversion_cell` for why a cast may say that where an operator may not.
    """
    source = _grid_type(fact)
    cell = None if source is None else conversion_outcome(target, source)
    if cell is None:
        return NOTHING
    if not cell.may_throw or _cast_throws_are_modelled(target):
        try:
            computed = _cast(target, fact)
        except _Throws:
            return Ps1Outcome(True, UNKNOWN)
        if computed is not None:
            stamped = _stamped(computed, cell.types)
            if stamped is not UNKNOWN:
                return Ps1Outcome(False, stamped)
    return _from_conversion_cell(cell, _spans(fact))
def evaluate(node, type_of_variable=None)

What this expression produces. It is the module's one recursion and the only entry a caller holding a tree needs: read(), convert() and apply() each answer about one step, and a consumer that walked the tree itself would be deciding at every node what is decided here once. A literal is read(), a parenthesis is its inner, an array is its elements, a cast is convert() over its operand, an operator is apply() over both of its, and everything else carries whatever type the static surface names for it.

It refuses far more than it answers, and that is the contract rather than a shortfall: for an expression another reader in this module can answer, this agrees with that reader or names nothing — never a third thing.

A throw travels up. An operand that may throw makes the expression consuming it one that may throw, whatever the operation does with the value, and so does not knowing what the operand does, because Ps1Outcome reads its two fields in one direction.

A type literal is refused rather than answered, which is the one place this deliberately says less than resolve_expression_type(). That function answers [int] with System.Int32 because what asks it is a member lookup, and the type a literal names is what a lookup needs; the value one is is a System.RuntimeType, and no measurement here covers it.

A unary operator is apply_unary(), which reads a grid of its own. A sign written against a numeral does not reach it — the parser puts that sign inside the numeral's spelling, so -1 is a literal and read() answers it; what reaches here is - 1 with a space, and - $x.

type_of_variable is what the caller can say about a variable occurrence, exactly as resolve_expression_type() takes it. Nothing here invents a variable's value, so a variable the caller cannot type names nothing.

The answer for a node is remembered until some tree changes, because being the one entry a caller with a tree needs is only affordable if it is. What asks this is a visitor descending the tree, and a visitor asks about a node's operand at the operand, then again at its parent, then again at its parent: the recursion below is proportional to the subtree, so the walk as a whole is quadratic in the depth of an expression, and a chain of two hundred operands the pass refuses to fold is where that stops being theoretical. What is remembered is keyed on the node's identity, and the whole table is dropped the moment mutation_epoch() moves — so an entry can only ever be read back over the same tree that produced it, and this stays a function of its arguments.

Only the query that types no variable is remembered, which is the one that descent makes. A caller-supplied typing is state this module does not own: two callers that type an occurrence differently may not share an answer, so an entry would have to be keyed on the callable, which is comparable only by identity — and a bound method fails that against itself, so the table would be written and never read. Keeping one alive to compare against is worse than useless: a typing reaches the pass that wrote it and so the tree, and a table whose keys are weak so that a tree it answered for can be collected would then hold that tree through its own values.

Expand source code Browse git
def evaluate(
    node: Node | None,
    type_of_variable: Ps1VariableTyping | None = None,
) -> Ps1Outcome:
    """
    What this expression produces. It is the module's one recursion and the only entry a caller
    holding a *tree* needs: `read`, `convert` and `apply` each answer about one step, and a consumer
    that walked the tree itself would be deciding at every node what is decided here once. A literal
    is `read`, a parenthesis is its inner, an array is its elements, a cast is `convert` over its
    operand, an operator is `apply` over both of its, and everything else carries whatever type the
    static surface names for it.

    It refuses far more than it answers, and that is the contract rather than a shortfall: for an
    expression another reader in this module can answer, this agrees with that reader or names
    nothing — never a third thing.

    A throw travels up. An operand that may throw makes the expression consuming it one that may
    throw, whatever the operation does with the value, and so does not knowing what the operand
    does, because `Ps1Outcome` reads its two fields in one direction.

    A type literal is refused rather than answered, which is the one place this deliberately says
    less than `resolve_expression_type`. That function answers `[int]` with `System.Int32` because
    what asks it is a member lookup, and the type a literal *names* is what a lookup needs; the
    value one *is* is a `System.RuntimeType`, and no measurement here covers it.

    A unary operator is `apply_unary`, which reads a grid of its own. A sign written against a
    numeral does not reach it — the parser puts that sign inside the numeral's spelling, so `-1` is
    a literal and `read` answers it; what reaches here is `- 1` with a space, and `- $x`.

    `type_of_variable` is what the caller can say about a variable occurrence, exactly as
    `resolve_expression_type` takes it. Nothing here invents a variable's value, so a variable the
    caller cannot type names nothing.

    **The answer for a node is remembered until some tree changes**, because being the one entry a
    caller with a tree needs is only affordable if it is. What asks this is a visitor descending the
    tree, and a visitor asks about a node's operand at the operand, then again at its parent, then
    again at *its* parent: the recursion below is proportional to the subtree, so the walk as a
    whole is quadratic in the depth of an expression, and a chain of two hundred operands the pass
    refuses to fold is where that stops being theoretical. What is remembered is keyed on the node's
    identity, and the whole table is dropped the moment `refinery.lib.scripts.mutation_epoch` moves
    — so an entry can only ever be read back over the same tree that produced it, and this stays a
    function of its arguments.

    **Only the query that types no variable is remembered**, which is the one that descent makes. A
    caller-supplied typing is state this module does not own: two callers that type an occurrence
    differently may not share an answer, so an entry would have to be keyed on the callable, which
    is comparable only by identity — and a bound method fails that against itself, so the table
    would be written and never read. Keeping one alive to compare against is worse than useless: a
    typing reaches the pass that wrote it and so the tree, and a table whose keys are weak so that a
    tree it answered for can be collected would then hold that tree through its own values.
    """
    if node is None:
        return NOTHING
    if type_of_variable is not None:
        return _evaluated(node, type_of_variable)
    global _EVALUATED_AT
    epoch = mutation_epoch()
    if epoch != _EVALUATED_AT:
        _EVALUATIONS.clear()
        _EVALUATED_AT = epoch
    remembered = _EVALUATIONS.get(node)
    if remembered is not None:
        return remembered
    outcome = _evaluated(node, None)
    if mutation_epoch() == epoch:
        _EVALUATIONS[node] = outcome
    return outcome
def render(fact)

The expression that spells this value. A value always has one: a literal where the language has a literal of its type, and the cast of one where it does not, so that a caller holding a Ps1Constant never has to choose between leaving the source alone and spelling something else.

None is therefore not a refusal to spell a value: it is the answer for a fact that names no value. UNKNOWN and Ps1Typed are the two, and beside them stand a payload that does not carry its own type, which is a malformed fact rather than a value, and a Double that is not finite. Infinity and NaN have no literal and no cast that reaches them, and the domain does not carry one either — _finite refuses a computed one — so that last refusal is unreachable rather than a gap.

A number is spelled with its sign attached to the digits, which is the spelling that keeps its type: -2147483648 is one literal that fits Int32, and a caller putting the result somewhere a parenthesis would separate the two has changed an Int32 into an Int64. Where a slot reads that spelling as something else — a command argument reads a leading dash as part of a word, and a cast written bare there is one word too — it is the slot that brackets it, in refinery.lib.scripts.ps1.synth, because only the slot knows what stands beside it.

Expand source code Browse git
def render(fact: Ps1Fact) -> Expression | None:
    """
    The expression that spells this value. **A value always has one**: a literal where the language
    has a literal of its type, and the cast of one where it does not, so that a caller holding a
    `Ps1Constant` never has to choose between leaving the source alone and spelling something else.

    `None` is therefore not a refusal to spell a value: it is the answer for a fact that *names*
    no value. `UNKNOWN` and `Ps1Typed` are the two, and beside them stand a payload that does not
    carry its own type, which is a malformed fact rather than a value, and a `Double` that is not
    finite. Infinity and NaN have no literal and no cast that reaches them, and the domain does
    not carry one either — `_finite` refuses a computed one — so that last refusal is unreachable
    rather than a gap.

    A number is spelled with its sign attached to the digits, which is the spelling that keeps its
    type: `-2147483648` is one literal that fits Int32, and a caller putting the result somewhere a
    parenthesis would separate the two has changed an Int32 into an Int64. Where a *slot* reads that
    spelling as something else — a command argument reads a leading dash as part of a word, and a
    cast written bare there is one word too — it is the slot that brackets it, in
    `refinery.lib.scripts.ps1.synth`, because only the slot knows what stands beside it.
    """
    if fact is NULL:
        return null_expression()
    if not isinstance(fact, Ps1Constant):
        return None
    payload = fact.payload
    if fact.type == _BOOLEAN:
        return Ps1Variable(name='True' if payload else 'False')
    if fact.type == _STRING:
        return make_string_literal(payload) if isinstance(payload, str) else None
    if fact.type == _CHAR:
        return _rendered_character(payload)
    if fact.type == _OBJECT_ARRAY:
        return _rendered_array(payload) if isinstance(payload, tuple) else None
    if fact.type == _DOUBLE:
        return _rendered_double(payload)
    if isinstance(payload, bool) or not isinstance(payload, (int, decimal.Decimal)):
        return None
    suffix = _LITERAL_SUFFIX.get(fact.type)
    if suffix is not None:
        if fact.type == _DECIMAL:
            return Ps1RealLiteral(raw=F'{payload}{suffix}')
        return Ps1IntegerLiteral(raw=F'{payload}{suffix}')
    target = _CAST_SPELLING.get(fact.type)
    if target is None:
        return None
    return Ps1CastExpression(type_name=target, operand=Ps1IntegerLiteral(raw=str(payload)))
def folded_binary(left, operator, right)

The expression left operator right folds to, or None where the pair is not constant or the operation may throw.

The operands are read exactly as evaluate() reads the two sides of any binary expression, so a fold here agrees with folding the same operator written out longhand: the value a compound assignment $x op= e leaves is $x op e, and this is what lets the short spelling reach the same constant the long one does. Each operand is copied before it is read, because the throwaway node built to hold them adopts the children it is handed, and a fold must leave the tree it read from untouched whether or not a caller installs the result.

Expand source code Browse git
def folded_binary(left: Expression, operator: str, right: Expression) -> Expression | None:
    """
    The expression `left operator right` folds to, or `None` where the pair is not constant or the
    operation may throw.

    The operands are read exactly as `evaluate` reads the two sides of any binary expression, so a
    fold here agrees with folding the same operator written out longhand: the value a compound
    assignment `$x op= e` leaves is `$x op e`, and this is what lets the short spelling reach the
    same constant the long one does. Each operand is copied before it is read, because the throwaway
    node built to hold them adopts the children it is handed, and a fold must leave the tree it read
    from untouched whether or not a caller installs the result.
    """
    combined = Ps1BinaryExpression(
        left=_clone_node(left),
        operator=operator,
        right=_clone_node(right),
    )
    outcome = evaluate(combined)
    return None if outcome.may_throw else render(outcome.value)
def folded_increment(previous, delta)

The value $x++ or $x-- leaves in $x, given its previous value previous and a delta of +1 or -1, or None where that value is not constant or the increment throws.

++ and -- are not the binary $x + 1 and $x - 1: they require a number and add the delta to it, where + and - would concatenate a String, coerce one, or read a Boolean as an integer — none of which the increment does. 5.1 answers the delta itself for $null and throws OperatorRequiresNumber for a String, a Char, a Boolean or a collection, so this folds only over $null and the numeric types and refuses the rest, standing no value where 5.1 raised. Over a number the increment is the binary sum, which is why folded_binary() computes it once the operand is one — and $null, which _is_domain_integer reads as the zero the sum needs.

Expand source code Browse git
def folded_increment(previous: Expression, delta: int) -> Expression | None:
    """
    The value `$x++` or `$x--` leaves in `$x`, given its previous value `previous` and a `delta` of
    `+1` or `-1`, or `None` where that value is not constant or the increment throws.

    `++` and `--` are not the binary `$x + 1` and `$x - 1`: they require a number and add the delta
    to it, where `+` and `-` would concatenate a String, coerce one, or read a Boolean as an integer
    — none of which the increment does. 5.1 answers the delta itself for `$null` and throws
    `OperatorRequiresNumber` for a String, a Char, a Boolean or a collection, so this folds only over
    `$null` and the numeric types and refuses the rest, standing no value where 5.1 raised. Over a
    number the increment *is* the binary sum, which is why `folded_binary` computes it once the
    operand is one — and `$null`, which `_is_domain_integer` reads as the zero the sum needs.
    """
    fact = read(previous)
    numeric = _is_domain_integer(fact) or (
        isinstance(fact, Ps1Constant) and fact.type in (_DECIMAL, _DOUBLE)
    )
    if not numeric:
        return None
    return folded_binary(previous, '+' if delta > 0 else '-', Ps1IntegerLiteral(raw='1'))
def make_string_literal(value)

The literal that spells value as a String, for a caller that holds a bare Python str and no fact. It is render()'s String arm, and it is the last place in the unit where a value is spelled without its type having been named — a str reaching here becomes a String whatever it was, which is what the ledger's Char rows were. Each caller loses this as it starts carrying a Ps1Fact instead; what is left is the emulation of a .NET method that really does produce a String, and a text this module computed itself.

A here-string is chosen for multi-line text because it needs no escaping, and only where the text cannot close it early: a line beginning '@ inside the value would end the string there and let the rest of it be read as script.

Expand source code Browse git
def make_string_literal(value: str) -> Ps1StringLiteral | Ps1HereString:
    """
    The literal that spells `value` as a `String`, for a caller that holds a bare Python `str` and
    no fact. It is `render`'s String arm, and it is the last place in the unit where a value is
    spelled without its type having been named — a `str` reaching here becomes a String whatever it
    was, which is what the ledger's Char rows were. Each caller loses this as it starts carrying a
    `Ps1Fact` instead; what is left is the emulation of a .NET method that really does produce a
    String, and a text this module computed itself.

    A here-string is chosen for multi-line text because it needs no escaping, and only where the
    text cannot close it early: a line beginning `'@` inside the value would end the string there
    and let the rest of it be read as script.
    """
    has_newline = '\n' in value
    has_nonprint = any(c in value for c in _NONPRINT_CONTROL)
    herestring_safe = not value.startswith("'@") and "\n'@" not in value
    if has_newline and not has_nonprint and herestring_safe:
        return Ps1HereString(value=value, raw=F"@'\n{value}\n'@")
    if has_nonprint or has_newline:
        escaped = value.replace('`', '``').replace('"', '`"').replace('$', '`$')
        for ch, esc in BACKTICK_ENCODE.items():
            escaped = escaped.replace(ch, esc)
        return Ps1StringLiteral(value=value, raw=F'"{escaped}"')
    if "'" not in value:
        raw = F"'{value}'"
    elif '"' not in value and '$' not in value and '`' not in value:
        raw = F'"{value}"'
    else:
        raw = "'" + value.replace("'", "''") + "'"
    return Ps1StringLiteral(value=value, raw=raw)

Classes

class Ps1Fact

What is known about one PowerShell value: nothing (UNKNOWN), that it is $null (NULL), that it has a type (Ps1Typed), or that it is a particular value of a particular type (Ps1Constant). These are the four elements of the lattice every question in this module is answered in, ordered Ps1Constant below Ps1Typed below UNKNOWN, with NULL beside the typed ones rather than under them: $null.GetType() throws, so there is no type it could carry.

The base is a marker and carries no accessor: type_of() is where a fact's type is read, so that the one place a caller asks the question is a function it can be pointed at, and an element that has no type does not have to pretend to answer.

Expand source code Browse git
class Ps1Fact:
    """
    What is known about one PowerShell value: nothing (`UNKNOWN`), that it is `$null` (`NULL`), that
    it has a type (`Ps1Typed`), or that it is a particular value of a particular type
    (`Ps1Constant`). These are the four elements of the lattice every question in this module is
    answered in, ordered `Ps1Constant` below `Ps1Typed` below `UNKNOWN`, with `NULL` beside the
    typed ones rather than under them: `$null.GetType()` throws, so there is no type it could carry.

    The base is a marker and carries no accessor: `type_of` is where a fact's type is read, so that
    the one place a caller asks the question is a function it can be pointed at, and an element that
    has no type does not have to pretend to answer.
    """

    __slots__ = ()

Subclasses

  • Ps1Constant
  • Ps1Typed
  • refinery.lib.scripts.ps1.analysis.values._Ps1Null
  • refinery.lib.scripts.ps1.analysis.values._Ps1Unknown
class Ps1Typed (type)

The value has this type and no more is known about it. A refinement — an interval, a known-bits mask — becomes a field here when one is built, so that narrowing what a typed value can be does not add an element to the lattice or a case to any caller.

Expand source code Browse git
@dataclasses.dataclass(frozen=True)
class Ps1Typed(Ps1Fact):
    """
    The value has this type and no more is known about it. A refinement — an interval, a known-bits
    mask — becomes a field here when one is built, so that narrowing what a typed value can be does
    not add an element to the lattice or a case to any caller.
    """

    type: Ps1TypeName

    def __repr__(self) -> str:
        return F'Typed({self.type})'

Ancestors

Instance variables

var type

The type of the None singleton.

class Ps1Constant (type, payload)

The value is exactly payload, and its type is type. The payload's Python type is an implementation of the .NET one and never a substitute for it: Ps1Constant(System.Char, 'A') and Ps1Constant(System.String, 'A') hold equal payloads and are different values, which is the distinction this whole layer exists to keep. A caller deciding what a payload means reads type.

An array's payload is a tuple of facts rather than of payloads, so that an Object[] whose elements are Chars is a different value from one whose elements are Strings — the fact a pipeline builds and the erasure that made foreach iterate once over a joined string.

Expand source code Browse git
@dataclasses.dataclass(frozen=True)
class Ps1Constant(Ps1Fact):
    """
    The value is exactly `payload`, and its type is `type`. The payload's Python type is an
    implementation of the .NET one and never a substitute for it: `Ps1Constant(System.Char, 'A')`
    and `Ps1Constant(System.String, 'A')` hold equal payloads and are different values, which is the
    distinction this whole layer exists to keep. A caller deciding what a payload means reads
    `type`.

    An array's payload is a tuple of facts rather than of payloads, so that an `Object[]` whose
    elements are Chars is a different value from one whose elements are Strings — the fact a
    pipeline builds and the erasure that made `foreach` iterate once over a joined string.
    """

    type: Ps1TypeName
    payload: int | float | decimal.Decimal | str | bool | tuple[Ps1Fact, ...]

    def __repr__(self) -> str:
        return F'Constant({self.type}, {self.payload!r})'

Ancestors

Instance variables

var type

The type of the None singleton.

var payload

The type of the None singleton.

class Ps1Outcome (may_throw, value)

What an operation does: the fact it produces, and whether it may instead throw. The two are separate because they are not alternatives — an operation that yields an Int32 or throws is both, and a domain that had to choose could only answer UNKNOWN and lose the type it knows.

Both fields are read in the same direction, which is what makes the two of them one answer: may_throw is False only where this module claims the operation cannot throw, exactly as UNKNOWN is the value of one that names none. Not knowing anything is therefore Ps1Outcome(True, UNKNOWN) and not Ps1Outcome(False, UNKNOWN) — the latter is a claim of safety made by the one answer that has no grounds for any claim. It made generalising an operand remove a throw: 1 / $x for a divisor this module could not type answered that it cannot throw, where the same division over a divisor it could type answered that it can. Only render() refusing to spell an UNKNOWN kept that out of a fold, which is a guard that holds one operation deep and no further.

An operation known to throw and one this module declines to judge are the same outcome here, which is what may means. Telling them apart would want a consumer that acts on a certain throw, and there is none: the reader of this axis folds, and both answers stop it.

Expand source code Browse git
class Ps1Outcome(typing.NamedTuple):
    """
    What an operation does: the fact it produces, and whether it may instead throw. The two are
    separate because they are not alternatives — an operation that yields an Int32 *or* throws is
    both, and a domain that had to choose could only answer `UNKNOWN` and lose the type it knows.

    Both fields are read in the same direction, which is what makes the two of them one answer:
    `may_throw` is `False` only where this module claims the operation *cannot* throw, exactly as
    `UNKNOWN` is the value of one that names none. Not knowing anything is therefore
    `Ps1Outcome(True, UNKNOWN)` and not `Ps1Outcome(False, UNKNOWN)` — the latter is a claim of
    safety made by the one answer that has no grounds for any claim. It made generalising an operand
    *remove* a throw: `1 / $x` for a divisor this module could not type answered that it cannot
    throw, where the same division over a divisor it could type answered that it can. Only
    `render` refusing to spell an `UNKNOWN` kept that out of a fold, which is a guard that holds one
    operation deep and no further.

    An operation known to throw and one this module declines to judge are the same outcome here,
    which is what *may* means. Telling them apart would want a consumer that acts on a certain
    throw, and there is none: the reader of this axis folds, and both answers stop it.
    """

    may_throw: bool
    value: Ps1Fact

Ancestors

  • builtins.tuple

Instance variables

var may_throw

Alias for field number 0

Expand source code Browse git
class Ps1Outcome(typing.NamedTuple):
    """
    What an operation does: the fact it produces, and whether it may instead throw. The two are
    separate because they are not alternatives — an operation that yields an Int32 *or* throws is
    both, and a domain that had to choose could only answer `UNKNOWN` and lose the type it knows.

    Both fields are read in the same direction, which is what makes the two of them one answer:
    `may_throw` is `False` only where this module claims the operation *cannot* throw, exactly as
    `UNKNOWN` is the value of one that names none. Not knowing anything is therefore
    `Ps1Outcome(True, UNKNOWN)` and not `Ps1Outcome(False, UNKNOWN)` — the latter is a claim of
    safety made by the one answer that has no grounds for any claim. It made generalising an operand
    *remove* a throw: `1 / $x` for a divisor this module could not type answered that it cannot
    throw, where the same division over a divisor it could type answered that it can. Only
    `render` refusing to spell an `UNKNOWN` kept that out of a fold, which is a guard that holds one
    operation deep and no further.

    An operation known to throw and one this module declines to judge are the same outcome here,
    which is what *may* means. Telling them apart would want a consumer that acts on a certain
    throw, and there is none: the reader of this axis folds, and both answers stop it.
    """

    may_throw: bool
    value: Ps1Fact
var value

Alias for field number 1

Expand source code Browse git
class Ps1Outcome(typing.NamedTuple):
    """
    What an operation does: the fact it produces, and whether it may instead throw. The two are
    separate because they are not alternatives — an operation that yields an Int32 *or* throws is
    both, and a domain that had to choose could only answer `UNKNOWN` and lose the type it knows.

    Both fields are read in the same direction, which is what makes the two of them one answer:
    `may_throw` is `False` only where this module claims the operation *cannot* throw, exactly as
    `UNKNOWN` is the value of one that names none. Not knowing anything is therefore
    `Ps1Outcome(True, UNKNOWN)` and not `Ps1Outcome(False, UNKNOWN)` — the latter is a claim of
    safety made by the one answer that has no grounds for any claim. It made generalising an operand
    *remove* a throw: `1 / $x` for a divisor this module could not type answered that it cannot
    throw, where the same division over a divisor it could type answered that it can. Only
    `render` refusing to spell an `UNKNOWN` kept that out of a fold, which is a guard that holds one
    operation deep and no further.

    An operation known to throw and one this module declines to judge are the same outcome here,
    which is what *may* means. Telling them apart would want a consumer that acts on a certain
    throw, and there is none: the reader of this axis folds, and both answers stop it.
    """

    may_throw: bool
    value: Ps1Fact