Module refinery.lib.inno.emulator

An emulator for Inno Setup executables. The implementation is unlikely to be 100% correct as it was engineered by making various malicious scripts execute reasonably well, not by implementing an exact copy of the (only) reference implementation. This grew and grew as I wrote it, and seems mildly insane in hindsight.

Expand source code Browse git
"""
An emulator for Inno Setup executables. The implementation is unlikely to be 100% correct as it
was engineered by making various malicious scripts execute reasonably well, not by implementing
an exact copy of [the (only) reference implementation][PS]. This grew and grew as I wrote it,
and seems mildly insane in hindsight.

[PS]: https://github.com/remobjects/pascalscript
"""
from __future__ import annotations

import bisect
import fnmatch
import hashlib
import inspect
import ntpath
import random
import re
import shlex
import struct

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum, auto
from functools import wraps
from time import process_time
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    ClassVar,
    Generator,
    Generic,
    Iterable,
    Iterator,
    Mapping,
    NamedTuple,
    Sequence,
    TypeVar,
    get_origin,
)

from refinery.lib import json
from refinery.lib.inno import CaseInsensitiveDict, constants, expressions, pascal
from refinery.lib.inno.archive import InnoArchive, TextDirective
from refinery.lib.inno.delphi import formatting, numbers, strings, variants, x87
from refinery.lib.inno.delphi.errors import DelphiException, EIntOverflow, SysConst, Undefined
from refinery.lib.inno.delphi.formatting import VarRec, VType
from refinery.lib.inno.delphi.runtime import Platform
from refinery.lib.inno.delphi.variants import Variant, VarType
from refinery.lib.inno.emulation import (
    AbortEmulation,
    EmulatorException,
    EmulatorExecutionLimit,
    EmulatorMaxArray,
    EmulatorMaxCalls,
    EmulatorMaxStack,
    EmulatorMaxString,
    EmulatorMemoryError,
    EmulatorTimeout,
    EmulatorUndefined,
    EntryField,
    InternalError,
    NeedSymbol,
    NewEmulationError,
    NewFunctionCall,
    NewInstruction,
    NewMutex,
    NewPassword,
    OpCodeNotImplemented,
)
from refinery.lib.inno.host import (
    HostSetup,
    SetupConstants,
    architecture,
    machine_folders,
    processor,
)
from refinery.lib.inno.ifps import (
    TC,
    AOp,
    COp,
    Dialect,
    EHType,
    Function,
    IFPSFile,
    IFPSType,
    Instruction,
    Op,
    Operand,
    TArray,
    TPrimitive,
    TRecord,
    TStaticArray,
    Value,
    VariableBase,
    VariableSpec,
    VariableType,
)
from refinery.lib.inno.installation import (
    CODE_CONSTANT,
    ArgumentKind,
    CurrentFile,
    CurrentFolder,
    Installation,
    InstalledPaths,
    SetupCall,
    SetupEntries,
    user_info_edit_change,
)
from refinery.lib.inno.pascal import IFPSException, PSError, signed32, type_mismatch
from refinery.lib.inno.pathfunc import PathFuncError, expand_file_name
from refinery.lib.inno.symbols import parse_function
from refinery.lib.inno.wizard import (
    CheckList,
    CheckOperation,
    Expander,
    PageEvent,
    PageStyle,
    SetupHandler,
    StandIns,
    TEditCharCase,
    TextBox,
    WizardForm,
    WizardPage,
    comma_strings,
    comma_text,
    list_contains,
)
from refinery.lib.patterns import formats
from refinery.lib.resources import datapath
from refinery.lib.tools import copy_exception, exception_to_string

if TYPE_CHECKING:
    from typing import ParamSpec
    _P = ParamSpec('_P')

_T = TypeVar('_T')
_Y = TypeVar('_Y')


ERROR_NO_MORE_FILES = 0x100123

_OUT_OF_STRING_RANGE = 'Out of string range'
_INVALID_FLOAT = 'Invalid float'
_NIL_INVOKE = 'Variant is null, cannot invoke'
_LONGEST_WIDE_STRING = (0x7FFFFFFF - 14) // 2


def _index_error(array: Variable, index: int, dialect: Dialect) -> IFPSException:
    """
    The error of an index outside of an array: Pascal Script names the index and the length from
    Inno Setup 7 on.
    """
    if dialect is Dialect.Inno6:
        return IFPSException(kind=PSError.OutOfRange)
    kind = 'Static array' if array.type.code is TC.StaticArray else 'Array'
    message = F'{kind} index out of bounds ({signed32(index)}). Length is {len(array)}.'
    return IFPSException(message, PSError.CustomError)


class OleObject:
    """
    A dummy object representing an OLE interface created by an IFPS script. All it does so far is
    to remember the name of the object that was requested.
    """
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return F'OleObject({self.name!r})'

    def __str__(self):
        return self.name


_VariantType = TPrimitive(TC.Variant)
_StringType = TPrimitive(TC.UnicodeString)


class _DynamicArray(list['Variable']):
    """
    The elements of a dynamic array of Pascal Script. Assignment shares the elements, and the
    number of variables that hold them decides whether a change of the length is made in place.
    """
    __slots__ = 'holders',

    def __init__(self, elements=()):
        super().__init__(elements)
        self.holders = 1


class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: list[Variable] | _T | None
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = v

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: VariableSpec | str | None = None,
        path: tuple[int, ...] = (),
        data: _T | list | None = None
    ):
        super().__init__(type, spec)
        self.path = path
        self.setdefault()
        if data is not None:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return _DynamicArray()
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            elif type.code == TC.Pointer:
                return Variable(_VariantType)
            else:
                return pascal.default(type)

        self.data = default(self.type)

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables, or none if `n` is negative. A
        dynamic array that another variable holds too is copied before its length changes.
        """
        t = self.type
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if not isinstance(t, TArray) or not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        n = max(n, 0)
        if len(data) == n:
            return
        if data.holders > 1:
            data.holders -= 1
            kept = data[:n]
            self.data = data = _DynamicArray()
            for k, element in enumerate(kept):
                copy = Variable(t.type, self.spec, (*self.path, k))
                copy._copy_from(element)
                data.append(copy)
        del data[n:]
        for k in range(len(data), n):
            data.append(Variable(t.type, self.spec, (*self.path, k)))

    def _share(self, elements: _DynamicArray):
        if not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        if data is elements:
            return
        data.holders -= 1
        elements.holders += 1
        self.data = elements

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself. As
        in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the
        target of that pointer, or a copy of that target with its type.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        var = var.deref()
        if copy:
            var = Variable(var.type, data=var)
        self.data = var

    def set(self, value: _T | Sequence | Variable):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those. A container variable is copied
        element by element, and a pointer element as Pascal Script copies it: A pointer to a
        variable of the script points to the same variable, and a pointer that owns its target
        points to a copy of that target. A dynamic array shares the elements of the dynamic array
        that it is assigned. A value of Python code is converted as
        `refinery.lib.inno.pascal.representation` converts it; the interpreter converts between the
        types of the script.
        """
        if self.pointer:
            return self.deref().set(value)
        assigned = value
        if isinstance(value, Variable):
            source = value.deref()
            if self.container and source.container:
                return self._copy_elements(source)
            if source.type == self.type and not source.container:
                self.data = source.data
                return
            assigned = source.get()
        elif isinstance(value, Value):
            assigned = value.value
        elif isinstance(value, Enum):
            assigned = value.value
        if self.container:
            if not isinstance(assigned, (list, tuple)):
                raise TypeError
            if self.type.code is TC.Array:
                self._share(_DynamicArray())
            self.resize(len(assigned))
            for k, v in enumerate(assigned):
                self.data[k].set(v)
        else:
            self.data = pascal.representation(self.type, assigned)

    def _copy_elements(self, source: Variable):
        if (
            not isinstance(elements := source.data, list)
            or not isinstance(self.data, list)
        ):
            raise TypeError
        if self.type.code is TC.Array:
            if isinstance(elements, _DynamicArray):
                return self._share(elements)
            self._share(_DynamicArray())
        self.resize(len(elements))
        for element, copied in zip(self.data, elements):
            element._copy_from(copied)

    def _copy_from(self, copied: Variable):
        if not self.pointer:
            self.set(copied)
        elif isinstance(target := copied.data, Variable):
            self.setptr(target, copy=target.spec is None)
        else:
            raise type_mismatch()

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: list[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'


def _variables_held(type: IFPSType) -> int:
    """
    The number of variables that a variable of the given type holds, itself included: a record or
    a static array holds its elements, and a dynamic array holds none until it gets a length.
    """
    if isinstance(type, TStaticArray):
        return 1 + type.size * _variables_held(type.type)
    if isinstance(type, TRecord):
        return 1 + sum(_variables_held(member) for member in type.members)
    return 1


class _CallState(NamedTuple):
    fn: Function
    ip: int
    sp: int


@dataclass
class ExceptionHandler:
    """
    An exception handler of the IFPS runtime: one try statement whose try block runs or whose
    blocks have not all ended. A block that an error enters can no longer be entered by an error.
    The second finally block that the end of the try block enters can still be entered once by an
    error, as in Pascal Script.
    """
    finally_one: int | None
    """
    Code offset of the first finally block, while it can be entered.
    """
    catch_error: int | None
    """
    Code offset of the except block, while it can be entered.
    """
    finally_two: int | None
    """
    Code offset of the second finally block, while it can be entered.
    """
    handler_end: int
    """
    Code offset of the first instruction after the try statement.
    """
    function: Function
    """
    The function that contains the try statement.
    """
    frame: int
    """
    The number of call frames below the function that contains the try statement.
    """
    sp: int
    """
    The stack base of the function that contains the try statement.
    """
    depth: int
    """
    The size of the data stack when the handler was installed. The stack is cut back to this size
    when an error enters one of the blocks of the handler.
    """
    exception: IFPSException | None = None
    """
    The error that entered the except block.
    """
    exit_point: int | None = None
    """
    From Inno Setup 7 on, the offset of the return that entered a finally block of this handler.
    """

    def resume(self, ip: int) -> _CallState:
        """
        The position at the given offset of the function that contains the try statement.
        """
        return _CallState(self.function, ip, self.sp)


class _ErrorInFlight:
    """
    The error that passes through a finally block, raised again when that block ends. A run of
    the script and all runs nested in it have one error in flight.
    """
    __slots__ = 'error',

    def __init__(self):
        self.error: IFPSException | None = None


class _ScriptRun:
    """
    One run of the script from the call of one of its functions until that function returns, as
    Pascal Script runs it: the data stack, the frames of the functions that called the running one,
    the try statements that have not ended, and the return that waits for finally blocks. A run
    that starts while an emulated function runs for another run, as one that expands a code
    constant, is nested in that run: it shares the error in flight and the script sees the
    handlers of both, but an error that no handler of the nested run takes ends the nested run.
    """
    def __init__(self, dialect: Dialect, caller: _ScriptRun | None = None):
        self.dialect = dialect
        self.stack: list[Variable] = []
        self.frames: list[_CallState] = []
        self.handlers: list[ExceptionHandler] = []
        self.exit_point: int | None = None
        self.caller = caller
        if caller is None:
            self.in_flight = _ErrorInFlight()
            self.depth = 0
        else:
            self.in_flight = caller.in_flight
            self.in_flight.error = None
            self.depth = caller.depth + len(caller.frames) + 1

    @property
    def current_exception(self) -> IFPSException | None:
        """
        The error that `GetExceptionMessage` reads: the one that entered the except block of the
        innermost try statement if an error did, else the error in flight when no try statement
        is left.
        """
        run = self
        while run is not None:
            if handlers := run.handlers:
                return handlers[-1].exception
            run = run.caller
        return self.in_flight.error

    def install(self, operands: Sequence, function: Function, sp: int):
        """
        Installs the handler of a try statement that begins in the given function.
        """
        finally_one, catch_error, finally_two, handler_end = operands
        if handler_end is None:
            raise RuntimeError('The try statement has no end.')
        self.handlers.append(ExceptionHandler(
            finally_one,
            catch_error,
            finally_two,
            handler_end,
            function,
            len(self.frames),
            sp,
            len(self.stack),
        ))

    def throw(self, error: IFPSException):
        """
        Puts the given error in flight. The run keeps a copy without traceback.
        """
        self.in_flight.error = copy_exception(error)

    def unwind(self) -> _CallState:
        """
        Moves the error in flight to the innermost handler of this run that can take it, which is
        the first of its finally block, its except block or its second finally block that can
        still be entered. Handlers with none left are removed. An error that enters the except
        block is no longer in flight. When no handler of this run is left, the error ends the run.
        """
        in_flight = self.in_flight
        handlers = self.handlers
        while handlers:
            eh = handlers[-1]
            del self.stack[eh.depth:]
            del self.frames[eh.frame:]
            if (ip := eh.finally_one) is not None:
                eh.finally_one = None
            elif (ip := eh.catch_error) is not None:
                eh.catch_error = None
                eh.exception, in_flight.error = in_flight.error, None
            elif (ip := eh.finally_two) is not None:
                eh.finally_two = None
            else:
                handlers.pop()
                continue
            return eh.resume(ip)
        error, in_flight.error = in_flight.error, None
        if error is None:
            raise RuntimeError('No error is in flight.')
        try:
            raise error
        finally:
            del error

    def end_block(self, block: EHType) -> _CallState | None:
        """
        Ends a block of the innermost try statement, which must belong to the running function.
        The result is where the run goes on, or `None` when the error in flight unwinds.
        """
        handlers = self.handlers
        if not handlers or (eh := handlers[-1]).frame != len(self.frames):
            raise RuntimeError(F'The end of a {block.name} block has no try statement in this function.')
        in_flight = self.in_flight
        if block is EHType.Try:
            if (ip := eh.finally_one) is not None:
                eh.finally_one = None
                return eh.resume(ip)
            if (ip := eh.finally_two) is not None:
                eh.catch_error = None
                return eh.resume(ip)
        elif block is EHType.Finally:
            if in_flight.error is not None and (ip := eh.catch_error) is not None:
                eh.catch_error = None
                eh.exception, in_flight.error = in_flight.error, None
                return eh.resume(ip)
            if (ip := eh.finally_two) is not None:
                eh.finally_two = None
                return eh.resume(ip)
        elif block is EHType.Catch:
            eh.catch_error = None
            if (ip := eh.finally_two) is not None:
                eh.finally_two = None
                return eh.resume(ip)
            handlers.pop()
            return eh.resume(self._after(eh))
        handlers.pop()
        if in_flight.error is not None:
            return None
        return eh.resume(self._after(eh))

    def exit(self, function: Function, offset: int, sp: int) -> _CallState | None:
        """
        Begins the return of the running function at the given offset: it first runs each finally
        block of its try statements, and the end of each comes back to the return. The result is
        the next finally block, or `None` when the function returns. Before Inno Setup 7, Pascal
        Script keeps one return for the run, which any return of a function that a finally block
        calls clears; the end of that block then goes on after its try statement. From Inno Setup 7
        on, each try statement keeps the return that entered its finally block.
        """
        per_handler = self.dialect is Dialect.Inno7
        if not per_handler:
            self.exit_point = offset
        handlers = self.handlers
        frame = len(self.frames)
        while handlers and (eh := handlers[-1]).frame >= frame:
            del self.stack[eh.depth:]
            if (ip := eh.finally_one) is not None:
                eh.finally_one = None
            elif (ip := eh.finally_two) is not None:
                eh.finally_two = None
            else:
                handlers.pop()
                continue
            if per_handler:
                eh.exit_point = offset
            return _CallState(function, ip, sp)
        self.exit_point = None
        return None

    def _after(self, eh: ExceptionHandler) -> int:
        exit_point = eh.exit_point if self.dialect is Dialect.Inno7 else self.exit_point
        if exit_point is not None:
            return exit_point
        return eh.handler_end


def _outside_file_entry(name: str, condition: str = '') -> InternalError:
    """
    Setup's error for a function of the current file called outside a function of a file entry
    that meets the given condition.
    """
    return InternalError(
        F'An attempt was made to call the "{name}" function from outside a "Check", '
        F'"BeforeInstall" or "AfterInstall" event function belonging to a "[Files]" entry{condition}'
    )


def _selected(names: list[str] | None, expression: str) -> bool:
    """
    Setup's test of the selected components or tasks in `WizardIsComponentSelected` and
    `WizardIsTaskSelected`: as `ShouldProcessEntry`, it tests no names without a list of them or
    for an empty expression.
    """
    expression = expression.replace('/', '\\')
    if names is None or not expression:
        return True
    return expressions.decide(expression, lambda name: list_contains(names, name))


def _wizard_page(page: object) -> WizardPage:
    """
    The page that the script passes, which the form gave it.
    """
    if not isinstance(page, WizardPage):
        raise TypeError(F'The emulator does not model the wizard page {page!r}.')
    return page


def _incompatible_call(handler: IFPSEmulatedFunction, argc: int, void: bool) -> str:
    def shape(argc: int, void: bool):
        arguments = 'argument' if argc == 1 else 'arguments'
        result = 'no result' if void else 'a result'
        return F'{argc} {arguments} and {result}'
    return F'the emulation takes {shape(handler.argc, handler.void)}, the call passes {shape(argc, void)}'


def _check_agreement(setup: HostSetup, ifps: IFPSFile, config: IFPSEmulatorConfig, source: str):
    """
    Refuses a script that its Setup cannot run: a script of a 64-bit Setup on 32-bit Windows, a
    release that contradicts the Pascal Script that compiled the script, and an installer whose
    platform is not the script's.
    """
    platform = ifps.platform
    if platform is Platform.Win64 and not config.x64:
        raise ValueError('The script runs in a 64-bit Setup, which 32-bit Windows does not run.')
    release = setup.release
    dialect = ifps.dialect
    if not release.is_current and release.since((7, 0, 0)) != (dialect is Dialect.Inno7):
        compiler = 'Inno Setup 7 or later' if dialect is Dialect.Inno7 else 'a version of Inno Setup before 7'
        raise ValueError(F'{source} names Inno Setup {release}, but the script was compiled by {compiler}.')
    if setup.platform is not platform:
        kind = '64-bit' if setup.platform is Platform.Win64 else '32-bit'
        raise ValueError(F'The installer is a {kind} Setup, but its script was compiled for {platform.name}.')


class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)


_OPEN_ARRAY_KINDS = {
    TC.U08           : VType.Integer,
    TC.S08           : VType.Integer,
    TC.U16           : VType.Integer,
    TC.S16           : VType.Integer,
    TC.U32           : VType.Integer,
    TC.S32           : VType.Integer,
    TC.S64           : VType.Int64,
    TC.U64           : VType.Int64,
    TC.Single        : VType.Extended,
    TC.Double        : VType.Extended,
    TC.Extended      : VType.Extended,
    TC.Char          : VType.Char,
    TC.WideChar      : VType.WideChar,
    TC.AnsiString    : VType.AnsiString,
    TC.PChar         : VType.PChar,
    TC.WideString    : VType.WideString,
    TC.UnicodeString : VType.UnicodeString,
    TC.Variant       : VType.Variant,
    TC.Class         : VType.Object,
    TC.Interface     : VType.Interface,
}


def _holds_integer(type: IFPSType) -> bool:
    """
    Whether a variable of the type holds an integer: an integer type, an enumeration, or a set.
    """
    code = type.code
    return code.integer or code is TC.Enum or code is TC.Set


def _split_spans(string: str, separators: list[str], quote: str) -> Iterator[tuple[int, int]]:
    """
    The start and end of each part of a string that Delphi's `string.Split` finds: it splits at the
    first of the separators that starts at the earliest position outside of quotes, and a quote
    character opens a quote that the next one closes.
    """
    alternatives = [re.escape(s) for s in separators if s]
    if len(quote) == 1:
        q = re.escape(quote)
        alternatives.insert(0, F'(?P<quoted>{q}[^{q}]*(?:{q}|\\Z))')
    start = 0
    if alternatives:
        for match in re.finditer('|'.join(alternatives), string):
            if match.lastgroup == 'quoted':
                continue
            yield start, match.start()
            start = match.end()
    yield start, len(string)


def _without_empty_last(spans: Iterable[tuple[int, int]]) -> Iterator[tuple[int, int]]:
    previous = None
    for span in spans:
        if previous is not None:
            yield previous
        previous = span
    if previous is not None and previous[0] < previous[1]:
        yield previous


def _failed_instruction(error: Exception, function: Function, insn: Instruction, clock: int) -> EmulatorException:
    """
    The error that ends an emulation whose instruction failed; a result of Delphi that the model
    does not have ends it as undefined.
    """
    position = F'In {function.symbol} at 0x{insn.offset:X} (cycle {clock})'
    if isinstance(error, Undefined):
        return EmulatorUndefined(F'{position}: {error!s}')
    return EmulatorException(F'{position}, emulation of {insn!r} failed: {error!s}')


def _open_array_element(element: Variable) -> VarRec:
    """
    Converts an element of an `array of const` into the argument that Pascal Script passes for it
    to a function of Delphi. Pascal Script passes a UInt64 as the Int64 of its bits, and an element
    of any other type as the Integer 0.
    """
    target = element.deref()
    if (kind := _OPEN_ARRAY_KINDS.get(target.type.code)) is None:
        return VarRec(VType.Integer, 0)
    value = target.get()
    if kind is VType.Integer:
        value = signed32(value)
    elif kind is VType.Int64:
        value = ((value + 0x8000000000000000) & 0xFFFFFFFFFFFFFFFF) - 0x8000000000000000
    return VarRec(kind, value)


def _now_in_whole_seconds() -> datetime:
    """
    The current time without its fraction of a second. The emulated clock starts there, so that what
    a script computes from the clock does not depend on the fraction of the second in which the
    emulation starts.
    """
    return datetime.now().replace(microsecond=0)


@dataclass
class IFPSEmulatorConfig:
    """
    The configuration for `refinery.lib.inno.emulator.IFPSEmulator`s.
    """
    x64: bool = True
    admin: bool = True
    windows_os_version: tuple[int, int, int] = (10, 0, 10240)
    windows_sp_version: tuple[int, int] = (2, 0)
    throw_abort: bool = False
    log_calls: bool = False
    log_passwords: bool = True
    log_mutexes: bool = True
    log_opcodes: bool = False
    wizard_silent: bool = True
    max_opcodes: int = 0
    max_seconds: int = 10
    start_time: datetime = field(default_factory=_now_in_whole_seconds)
    milliseconds_per_instruction: float = 0.0001
    sleep_scale: float = 0.0
    max_data_stack: int = 1_000_000
    max_array_length: int = 0x100000
    max_string_length: int = 0x1000000
    max_call_stack: int = 4096
    environment: dict[str, str] = field(default_factory=dict)
    user_name: str = 'Frank'
    temp_path: str = ''
    host_name: str = 'Frank-PC'
    inno_name: str = 'ThisInstall'
    language: str = 'en'
    executable: str = 'C:\\Install.exe'
    install_to: str = 'I:\\'
    allow_process_listing: bool = False
    processes = {
        1001: 'explorer.exe',
        1002: None, # represents self
        1023: 'svchost.exe',
        1024: 'svchost.exe',
        1060: 'rundll32.exe',
        1061: 'rundll32.exe',
        2223: 'csrss.exe',
        1200: 'winlogon.exe',
        1300: 'wininit.exe',
        1301: 'dllhost.exe',
        1305: 'notepad.exe',
        5305: 'chrome.exe',
    }
    lcid: int = 0x0409
    setup_version: tuple[int, int, int] | None = None


class TSplitType(int, Enum):
    """
    An IFPS enumeration that classifies different strategies for splitting strings.
    """
    stAll = 0
    stExcludeEmpty = auto()
    stExcludeLastEmpty = auto()


class TUninstallStep(int, Enum):
    """
    An IFPS enumeration that classifies uninstaller steps.
    """
    usAppMutexCheck = 0
    usUninstall = auto()
    usPostUninstall = auto()
    usDone = auto()


class EventCall(Generic[_Y, _T]):
    """
    This class is a wrapper for generator functions that can also capture their return value.
    It is used for `refinery.lib.inno.emulator.IFPSEmulator.emulate_function`.
    """
    def __init__(self, call: Generator[_Y, Any, _T]):
        self._call = call
        self._done = False
        self._buffer: list[_Y] = []
        self._value = None

    @classmethod
    def Wrap(cls, method: Callable[_P, Generator[_Y, Any, _T]]) -> Callable[_P, EventCall[_Y, _T]]:
        """
        Used for decorating generator functions.
        """
        @wraps(method)
        def wrapped(*args, **kwargs):
            return cls(method(*args, **kwargs))
        return wrapped

    @property
    def value(self) -> _T | None:
        """
        The return value of the wrapped function.
        """
        if not self._done:
            self._buffer = list(self)
        return self._value

    def __iter__(self):
        if self._done:
            yield from self._buffer
            assert self._value is not None
            self._buffer.clear()
        else:
            self._value = yield from self._call
            self._done = True
        return self._value


class _Stopwatch:
    """
    The processor time that one emulation spends. It runs only while the emulator works, not while
    the caller handles the events that the emulation yields.
    """
    def __init__(self, limit: float):
        self.limit = limit
        self.spent = 0.0
        self.started = process_time()

    def resume(self):
        self.started = process_time()

    def pause(self):
        self.spent += process_time() - self.started

    def expired(self) -> bool:
        return 0 < self.limit < self.spent + process_time() - self.started


class IFPSEmulator:
    """
    The core IFPS emulator.
    """
    def __init__(
        self,
        archive: InnoArchive | IFPSFile,
        options: IFPSEmulatorConfig | None = None,
        **more
    ):
        with datapath('winapi.json').open('rb') as stream:
            self.winapi: dict[str, list[str]] = json.loads(stream.read())
            base = 0x70000000
            self.winapi_module_to_addr = _ml = {name: base + k * 0x100000 for k, name in enumerate(self.winapi)}
            self.winapi_addr_to_module = {addr: module for module, addr in _ml.items()}
            self.winapi_symbol_to_addr = _sl = {
                symbol: _ml[module] + k * 0x1000 for module, symbols in self.winapi.items() for k, symbol in enumerate(symbols)}
            self.winapi_addr_to_symbol = {addr: module for module, addr in _sl.items()}

        self.config = config = options or IFPSEmulatorConfig(**more)
        if isinstance(archive, InnoArchive):
            self.setup_info = archive.setup_info
            ifps = archive.ifps
            if ifps is None:
                raise ValueError('The input archive does not contain a script.')
            self.setup = HostSetup.of_installer(archive, config)
            source = 'The installer'
        else:
            self.setup_info = None
            ifps = archive
            self.setup = HostSetup.of_bare_script(ifps.platform, config)
            source = 'The configured Setup version'
        _check_agreement(self.setup, ifps, config, source)
        self.machine_folders = machine_folders(self.setup, config)
        self.launch_folder = self.setup.path_func.extract_dir(config.executable)
        self.dialect = ifps.dialect
        self.ifps = ifps
        self._form_variables = [
            (index, name) for index, variable in enumerate(ifps.globals)
            if isinstance(variable.spec, str) and (name := variable.spec.upper()) in _FORM_VARIABLES
        ]
        self._stopwatch: _Stopwatch | None = None
        self._variables_held: dict[int, int] = {}
        self._globals: list[Variable] | None = None
        self.mutexes: set[str] = set()
        self.symbols: dict[str, Function] = CaseInsensitiveDict()
        self.reset()
        for pfn in ifps.functions:
            self.symbols[pfn.name] = pfn

    def __repr__(self):
        return self.__class__.__name__

    def reset(self, walked: bool = False):
        """
        Reset the emulator timing, the state of the Delphi run-time library, and mutexes. The
        script starts over with new global variables that hold their default values, in a new Setup
        process with a new wizard form, which Setup walks through when `walked` is set.
        """
        self.seconds_slept = 0.0
        self.clock = 0
        self.runtime = self.setup.runtime(numbers.FormatSettings.of_locale(self.config.lcid))
        self.machine = pascal.Machine(self.dialect, self.runtime)
        self.mutexes.clear()
        self.wizard_form = wizard = WizardForm(
            StandIns.of(self.config),
            walked=walked,
            entries_known=self.setup_info is not None,
            uninstallable=self.setup.uninstallable.answer is not False,
        )
        self.constants = SetupConstants(self.setup, self.config, wizard, self.machine_folders)
        self.current_folder = CurrentFolder(self.launch_folder)
        self.installed = InstalledPaths(self.current_folder)
        self.current_file: CurrentFile | None = None
        self._globals = None
        self._win32api_last_error = 0
        self._win32api_sorted_process_ids = sorted(self.config.processes)
        return self

    @property
    def globals(self) -> list[Variable]:
        """
        The global variables of the script.
        """
        if (script_globals := self._globals) is None:
            for v in self.ifps.globals:
                self._reserve_variables(v.type)
            script_globals = self._globals = [Variable(v.type, v.spec) for v in self.ifps.globals]
        return script_globals

    def _refresh_form_variables(self):
        """
        Sets the variables in which a script of Inno Setup 6 or later finds Setup's forms, as Setup
        does before each call into the script: each is empty until Setup created its form, and
        Setup never shows the form of the uninstaller.
        """
        if not (slots := self._form_variables):
            return
        form = self.wizard_form
        values = {
            'WIZARDFORM': form if form.created else None,
            'MAINFORM': form.main,
            'UNINSTALLPROGRESSFORM': None,
        }
        script_globals = self.globals
        for index, name in slots:
            script_globals[index].set(values[name])

    def _reserve_variables(self, type: IFPSType, count: int = 1):
        """
        Checks before they are built that the given number of variables of the given type, and the
        variables that they hold, stay within the array length limit.
        """
        if (limit := self.config.max_array_length) <= 0:
            return
        if not type.code.container:
            held = 1
        elif (held := self._variables_held.get(id(type))) is None:
            held = self._variables_held[id(type)] = _variables_held(type)
        if limit < (n := count * held):
            raise EmulatorMaxArray(F'Requested {n} variables of type {type}, beyond the limit {limit}.')

    def _reserve_text(self, length: int, wide: bool = True):
        """
        Checks a string of the given length before it is built: Delphi cannot compute the size of
        a UTF-16 string of more than `_LONGEST_WIDE_STRING` characters and raises an integer
        overflow, and a string beyond the configured limit ends the emulation.
        """
        if wide and length > _LONGEST_WIDE_STRING:
            raise pascal.delphi_error(EIntOverflow())
        if 0 < (limit := self.config.max_string_length) < length:
            raise EmulatorMaxString(F'Requested string length {length} exceeds the limit {limit}.')

    def _reserve_results(self, value: Any, arguments: list, result: Variable | None):
        """
        Checks what an emulated function returned before the script receives it: the text of its
        result and of the strings that it wrote into its arguments, and the elements of a list that
        becomes an array.
        """
        if isinstance(value, str):
            self._reserve_text(len(value))
        for var in arguments:
            if isinstance(var, Variable) and isinstance(text := var.deref().data, str):
                self._reserve_text(len(text))
        if (
            result is not None
            and isinstance(value, (list, tuple))
            and isinstance(array := result.deref().type, TArray)
        ):
            self._reserve_variables(array.type, len(value))

    def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
        """
        The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
        symbol is unimplemented. Child classes can override this function to handle the missing
        symbol differently; it receives the arguments of the call and the variable for its result,
        if the symbol is a function.
        """
        raise NeedSymbol(function.name)

    @EventCall.Wrap
    def emulate_function(self, function: Function, *args):
        """
        Emulate a function call to the given function, passing the given arguments. The method
        returns the return value of the emulated function call if it is not a procedure.
        """
        return (yield from self._timed(self._execute(_ScriptRun(self.dialect), function, args)))

    def _timed(self, emulation: Generator[Any, Any, _T], max_seconds: float | None = None) -> Generator[Any, Any, _T]:
        """
        Runs one emulation under a time limit: the given number of seconds or else the number in
        the configuration, no limit when it is not positive. Only the time that the emulator works
        counts, and the runs that the emulation nests share the limit. An emulation that starts while
        another one works and is given no limit of its own runs under the limit of that one.
        """
        if max_seconds is None and self._stopwatch is not None:
            return (yield from emulation)
        watch = _Stopwatch(self.config.max_seconds if max_seconds is None else max_seconds)
        try:
            while True:
                outer, self._stopwatch = self._stopwatch, watch
                watch.resume()
                try:
                    event = next(emulation)
                except StopIteration as stop:
                    return stop.value
                finally:
                    watch.pause()
                    self._stopwatch = outer
                yield event
        finally:
            emulation.close()

    def _execute(
        self,
        run: _ScriptRun,
        function: Function,
        args: Sequence,
        types: Sequence[IFPSType] | None = None,
        result_type: IFPSType | None = None,
    ):
        """
        Runs the function with the given values. The caller that creates the variables it passes,
        as Setup does, gives their types and the type of the result for a declaration that does
        not record them.
        """
        decl = function.decl
        if decl is None:
            raise NotImplementedError(F'Do not know how to call {function!s}.')
        self._refresh_form_variables()
        if (n := len(decl.parameters)) != (m := len(args)):
            raise ValueError(
                F'Function {function!s} expects {n} arguments, only {m} were given.')
        stack = run.stack
        for index, (argument, parameter) in enumerate(zip(args, decl.parameters), 1):
            kind = parameter.type
            if kind is None and types is not None:
                kind = types[index - 1]
            variable = Variable(kind, VariableSpec(index, VariableType.Local))
            variable.set(argument)
            stack.append(variable)
        stack.reverse()
        if not decl.void:
            result = Variable(decl.return_type or result_type, VariableSpec(0, VariableType.Argument))
            stack.append(result)
        yield from self._interpret(run, function)
        if not decl.void:
            return result.get()

    def _interpret(self, run: _ScriptRun, function: Function):
        """
        Emulates the given run of the script from the start of the given function.
        """

        def resolve(spec: VariableSpec) -> Variable:
            t, i = spec.type, spec.index
            if t is VariableType.Argument:
                if function.decl.void:
                    i -= 1
                return stack[sp - i]
            if t is VariableType.Global:
                return script_globals[i]
            if t is VariableType.Local:
                return stack[sp + i]
            raise TypeError

        def getvar(op: VariableSpec | Operand) -> Variable:
            if not isinstance(op, Operand):
                return resolve(op)
            if (spec := op.variable) is None:
                raise TypeError('Attempting to retrieve variable for an immediate operand.')
            var = resolve(spec)
            if isinstance(k := op.index, VariableSpec):
                k = resolve(k).get()
            if k is None:
                return var
            if not 0 <= k < len(var := var.deref()):
                raise _index_error(var, k, dialect)
            return var.at(k)

        def getval(op: Operand):
            if op.immediate:
                return op.value.value
            return getvar(op).get()

        def gettyped(op: Operand) -> tuple[IFPSType, Any]:
            if op.immediate and (constant := op.value) is not None:
                return constant.type, constant.value
            var = getvar(op).deref()
            return var.type, var.get()

        frames = run.frames
        depth = run.depth
        machine = self.machine
        dialect = machine.dialect
        script_globals = self.globals
        watch = self._stopwatch
        stack = run.stack
        _cfg_max_call_stack = self.config.max_call_stack
        _cfg_max_data_stack = self.config.max_data_stack
        _cfg_max_opcodes = self.config.max_opcodes
        _cfg_log_opcodes = self.config.log_opcodes

        ip: int = 0
        sp: int = len(stack) - 1
        raised: IFPSException | None = None
        failed_call = False
        unwind = False
        jump = False

        while True:
            if raised is not None:
                run.throw(raised)
                failed_call = raised.failed_call
                raised = None
                unwind = True
            if unwind:
                unwind = False
                function, ip, sp = run.unwind()
                if failed_call and run.in_flight.error is None:
                    raised = IFPSException(kind=PSError.CouldNotCallProc)
                    continue
                failed_call = False
            if 0 < _cfg_max_call_stack < depth + len(frames):
                raise EmulatorMaxCalls

            if function.body is None:
                namespace = ''

                if decl := function.decl:
                    namespace = decl.classname or decl.module or ''

                name = function.name
                registry: dict[str, IFPSEmulatedFunction] = self.external_symbols.get(namespace, {})
                handler = registry.get(name)

                if decl:
                    void = decl.void
                    argc = decl.argc + decl.method
                elif handler:
                    void = handler.void
                    argc = handler.argc
                else:
                    void = True
                    argc = 0

                try:
                    rpos = 0 if void else 1
                    args: list = [stack[~k] for k in range(rpos, argc + rpos)]
                except IndexError:
                    raise EmulatorException(
                        F'Cannot call {function!s}; {argc} arguments + {rpos} return values expected,'
                        F' but stack size is only {len(stack)}.')

                if self.config.log_calls:
                    yield NewFunctionCall(str(function), tuple(a.get() for a in args))

                if namespace in _FORM_CLASSES and decl is not None and args and args[0].get() is None:
                    kind = PSError.NullPointerException if decl.method else PSError.CouldNotCallProc
                    raised = IFPSException(kind=kind, called=name, failed_call=True)
                elif handler is None:
                    self.unimplemented(function, args, None if void else stack[-1])
                elif handler.argc != argc or handler.void and not void:
                    failure = _incompatible_call(handler, argc, void)
                    raised = IFPSException(failure, called=name)
                    yield NewEmulationError(name, failure)
                else:
                    for k, (var, byref) in enumerate(zip(args, handler.spec)):
                        if not byref:
                            args[k] = var.get()
                    args.insert(0, self)
                    keywords = {'caller': run} if handler.caller else {}
                    failure = None
                    try:
                        return_value = handler.call(*args, **keywords)
                        if inspect.isgenerator(return_value):
                            return_value = yield from return_value
                        self._reserve_results(return_value, args, None if void else stack[-1])
                    except EmulatorException:
                        raise
                    except RecursionError as RE:
                        raise EmulatorMaxCalls(F'Calling {name} exceeded the recursion limit of Python.') from RE
                    except MemoryError as ME:
                        raise EmulatorMemoryError(F'Calling {name} exhausted the memory of Python.') from ME
                    except Undefined as UE:
                        raise EmulatorUndefined(F'Calling {name}: {UE}') from UE
                    except IFPSException as error:
                        raised = IFPSException(error.message, error.kind, name, error.failed_call)
                    except DelphiException as error:
                        raised = IFPSException(self.setup.error_text(error), called=name)
                    except Exception as error:
                        failure = exception_to_string(error)
                        raised = IFPSException(failure, called=name)
                    else:
                        if not void:
                            stack[-1].set(return_value)
                    if failure is not None:
                        yield NewEmulationError(name, failure)
                if not frames:
                    if raised is None:
                        return
                    continue
                function, ip, sp = frames.pop()
                continue

            while insn := function.code.get(ip, None):
                if watch is not None and watch.expired():
                    raise EmulatorTimeout
                if 0 < _cfg_max_opcodes < self.clock:
                    raise EmulatorExecutionLimit
                if 0 < _cfg_max_data_stack < len(stack):
                    raise EmulatorMaxStack
                if _cfg_log_opcodes:
                    yield NewInstruction(function, insn, ip)
                try:
                    opc = insn.opcode
                    ip += insn.size
                    self.clock += 1

                    if opc == Op.Nop:
                        continue
                    elif opc == Op.Assign:
                        dst = getvar(insn.op(0)).deref()
                        src = insn.op(1)
                        if src.immediate:
                            source = src.value
                            st, sv = source.type, source.value
                        else:
                            source = getvar(src).deref()
                            st, sv = source.type, source.data
                        if dst.container or st.code is TC.Pointer:
                            if not pascal.assignable(dst.type, st, machine):
                                raise type_mismatch()
                            dst.set(source)
                        else:
                            if isinstance(source, Variable) and source.container:
                                sv = source.get()
                            dst.data = pascal.store(dst.type, st, sv, machine)
                    elif opc == Op.Calculate:
                        if not isinstance((aop := insn.operator), AOp):
                            raise TypeError(F'Operator for {opc} was of type {aop.__class__.__name__} rather than AOp.')
                        target = getvar(insn.op(0)).deref()
                        st, sv = gettyped(insn.op(1))
                        result = pascal.calculate(aop, target.type, target.data, st, sv, machine)
                        if isinstance(result, str):
                            self._reserve_text(len(result))
                        elif isinstance(result, Variant) and isinstance(result.value, str):
                            self._reserve_text(len(result.value))
                        target.data = result
                    elif opc == Op.Push:
                        # TODO: I do not actually know how this works
                        stack.append(getval(insn.op(0)))
                    elif opc == Op.PushVar:
                        stack.append(getvar(insn.op(0)))
                    elif opc == Op.Pop:
                        stack.pop()
                    elif opc == Op.Call:
                        frames.append(_CallState(function, ip, sp))
                        function = insn.callee()
                        ip = 0
                        sp = len(stack) - 1
                        break
                    elif opc == Op.Jump:
                        ip = insn.operands[0]
                    elif opc == Op.JumpTrue:
                        if pascal.truth(*gettyped(insn.op(1))):
                            ip = insn.operands[0]
                    elif opc == Op.JumpFalse:
                        if not pascal.truth(*gettyped(insn.op(1))):
                            ip = insn.operands[0]
                    elif opc == Op.Ret:
                        if (position := run.exit(function, insn.offset, sp)) is not None:
                            function, ip, sp = position
                            continue
                        del stack[sp + 1:]
                        if not frames:
                            if run.in_flight.error is None:
                                return
                            unwind = True
                            break
                        function, ip, sp = frames.pop()
                        break
                    elif opc == Op.StackType:
                        raise OpCodeNotImplemented(str(opc))
                    elif opc == Op.PushType:
                        self._reserve_variables(insn.operands[0])
                        stack.append(Variable(
                            insn.operands[0],
                            VariableSpec(len(stack) - sp, VariableType.Local)
                        ))
                    elif opc == Op.Compare:
                        if not isinstance((cop := insn.operator), COp):
                            raise TypeError(F'Operator for {opc} was of type {cop.__class__.__name__} rather than COp.')
                        lt, lv = gettyped(insn.op(1))
                        rt, rv = gettyped(insn.op(2))
                        truth = pascal.compare(cop, lt, lv, rt, rv, machine)
                        into = getvar(insn.op(0)).deref()
                        into.data = pascal.boolean(into.type, truth, machine)
                    elif opc == Op.CallVar:
                        call = getval(insn.op(0))
                        if isinstance(call, int):
                            call = self.ifps.functions[call]
                        if isinstance(call, Function):
                            frames.append(_CallState(function, ip, sp))
                            function = call
                            ip = 0
                            sp = len(stack) - 1
                            break
                    elif opc == Op.SetPtr:
                        getvar(insn.op(0)).setptr(getvar(insn.op(1)))
                    elif opc == Op.SetPtrToCopy:
                        dst = getvar(insn.op(0))
                        src = insn.op(1)
                        if (constant := src.value) is None:
                            dst.setptr(getvar(src), copy=True)
                        else:
                            dst.setptr(Variable(constant.type, data=constant.value))
                    elif opc == Op.BooleanNot:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.logical_not(var.type, var.data, machine.runtime)
                    elif opc == Op.IntegerNot:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.invert(var.type, var.data, machine.runtime)
                    elif opc == Op.Neg:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.negate(var.type, var.data, machine.runtime)
                    elif opc == Op.SetFlag:
                        condition, negated = insn.operands
                        jump = pascal.truth(*gettyped(condition)) ^ negated
                    elif opc == Op.JumpFlag:
                        if jump:
                            ip = insn.operands[0]
                    elif opc == Op.PushEH:
                        run.install(insn.operands, function, sp)
                    elif opc == Op.PopEH:
                        if (position := run.end_block(EHType(insn.operands[0]))) is None:
                            unwind = True
                            break
                        function, ip, sp = position
                    elif opc == Op.Inc:
                        counter = getvar(insn.op(0)).deref()
                        counter.data = pascal.step(counter.type, counter.data, True, machine)
                    elif opc == Op.Dec:
                        counter = getvar(insn.op(0)).deref()
                        counter.data = pascal.step(counter.type, counter.data, False, machine)
                    elif opc == Op.JumpPop1:
                        stack.pop()
                        ip = insn.operands[0]
                    elif opc == Op.JumpPop2:
                        stack.pop()
                        stack.pop()
                        ip = insn.operands[0]
                    else:
                        raise RuntimeError(F'Function contains invalid opcode at 0x{ip:X}.')
                except IFPSException as EE:
                    raised = copy_exception(EE)
                    break
                except EmulatorException:
                    raise
                except Exception as RE:
                    raise _failed_instruction(RE, function, insn, self.clock) from RE
            else:
                raise EmulatorException(F'In {function.symbol}, no instruction starts at 0x{ip:X}.')

    external_symbols: ClassVar[
        dict[str,                        # class name for methods or empty string for functions
        dict[str, IFPSEmulatedFunction]] # method or function name to emulation info
    ] = CaseInsensitiveDict()

    def external(*args, __reg: dict = external_symbols, **kwargs):
        def decorator(pfn):
            signature = inspect.signature(pfn)
            name: str = kwargs.get('name', pfn.__name__)
            csep: str = '.'
            if csep not in name:
                csep = '__'
            classname, _, name = name.rpartition(csep)
            if (registry := __reg.get(classname)) is None:
                registry = __reg[classname] = CaseInsensitiveDict()

            docs = F'{classname}::{name}' if classname else name
            docs = F'An emulated handler for the external symbol {docs}.'
            pfn.__doc__ = docs

            void = kwargs.get('void', signature.return_annotation == signature.empty)
            caller = kwargs.get('caller', False)
            parameters: list[bool] = []
            specs = iter(signature.parameters.values())
            next(specs) # skip self
            for spec in specs:
                if caller and spec.name == 'caller':
                    continue
                try:
                    hint = eval(spec.annotation, globals(), locals())
                except Exception as E:
                    raise RuntimeError(F'Invalid signature: {signature}') from E
                if not isinstance(hint, type):
                    hint = get_origin(hint)
                var = isinstance(hint, type) and issubclass(hint, Variable)
                parameters.append(var)
            registry[name] = e = IFPSEmulatedFunction(pfn, parameters, void, caller)
            aliases = kwargs.get('alias', [])
            if isinstance(aliases, str):
                aliases = [aliases]
            for name in aliases:
                registry[name] = e
            return pfn
        return decorator(args[0]) if args else decorator

    @external()
    def TInputDirWizardPage__GetValues(self, this: object, k: int) -> str:
        return F'$InputDir{k}'

    @external()
    def TInputFileWizardPage__GetValues(self, this: object, k: int) -> str:
        return F'$InputFile{k}'

    @external()
    def TWizardForm__GetPasswordEdit(self, this: object) -> object:
        return self.wizard_form.password_box

    @external()
    def TWizardForm__GetDirEdit(self, this: object) -> object:
        return self.wizard_form.dir_edit

    @external()
    def TWizardForm__GetGroupEdit(self, this: object) -> object:
        return self.wizard_form.group_edit

    @external()
    def TWizardForm__GetUserInfoNameEdit(self, this: object) -> object:
        return self.wizard_form.user_info_name_edit

    @external()
    def TWizardForm__GetUserInfoOrgEdit(self, this: object) -> object:
        return self.wizard_form.user_info_org_edit

    @external()
    def TWizardForm__GetUserInfoSerialEdit(self, this: object) -> object:
        return self.wizard_form.user_info_serial_edit

    @external()
    def TWizardForm__GetPrevAppDir(self, this: object) -> str:
        return ''

    def text_changed(self, box: TextBox) -> Generator[Any, Any, None]:
        """
        Runs the handler of a text box whose text changed, as the box's `OnChange` runs: a function
        of the script, or Setup's own handler. An error reaches the code that changed the text.
        """
        handler = box.on_change
        if isinstance(handler, Function):
            yield from self.call_from_setup(handler, (box,), _NOTIFY_CALL)
        elif handler is SetupHandler.UserInfoEditChange:
            yield from user_info_edit_change(self, box)

    def _box_text(self, this: object) -> str:
        return this.text if isinstance(this, TextBox) else ''

    def _set_box_text(self, this: object, value: str) -> Generator[Any, Any, None]:
        if isinstance(this, TextBox) and this.set_text(value):
            yield from self.text_changed(this)

    def _replace_box_selection(self, this: object, value: str) -> Generator[Any, Any, None]:
        if isinstance(this, TextBox):
            this.replace_selection(value)
            yield from self.text_changed(this)

    @external()
    def TEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TCustomEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TPasswordEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)

    @external()
    def TCustomEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)

    @external()
    def TPasswordEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)
        if value:
            yield NewPassword(value)

    @external()
    def TCustomEdit__Clear(self, this: object):
        if isinstance(this, TextBox):
            this.clear()
            yield from self.text_changed(this)

    @external()
    def TCustomEdit__ClearSelection(self, this: object):
        if isinstance(this, TextBox) and this.clear_selection():
            yield from self.text_changed(this)

    @external()
    def TCustomEdit__GetSelText(self, this: object) -> str:
        return this.selected_text if isinstance(this, TextBox) else ''

    @external()
    def TCustomEdit__SetSelText(self, this: object, value: str):
        yield from self._replace_box_selection(this, value)

    @external()
    def TCustomEdit__SelectAll(self, this: object):
        if isinstance(this, TextBox):
            this.select(0, -1)

    @external()
    def TCustomEdit__GetSelStart(self, this: object) -> int:
        return this.selection_start if isinstance(this, TextBox) else 0

    @external()
    def TCustomEdit__SetSelStart(self, this: object, value: int):
        if isinstance(this, TextBox):
            this.select(value, value)

    @external()
    def TCustomEdit__GetSelLength(self, this: object) -> int:
        return this.selection_length if isinstance(this, TextBox) else 0

    @external()
    def TCustomEdit__SetSelLength(self, this: object, value: int):
        if isinstance(this, TextBox):
            start = this.selection_start
            this.select(start, start + value)

    @external()
    def TEdit__SetCharCase(self, this: object, value: int):
        if isinstance(this, TextBox) and value in _EDIT_CHAR_CASES:
            this.set_case(TEditCharCase(value))

    def _set_box_handler(self, this: object, handler: object):
        if isinstance(this, TextBox):
            this.on_change = handler if isinstance(handler, Function) else None

    @external()
    def TEdit__SetOnChange(self, this: object, handler: object):
        self._set_box_handler(this, handler)

    @external()
    def TPasswordEdit__SetOnChange(self, this: object, handler: object):
        self._set_box_handler(this, handler)

    @external(alias=['ShellExecuteA', 'ShellExecuteW'])
    def shell32__ShellExecute(self, hwnd: int, operation: str, file: str, args: str, cwd: str, show: int) -> int:
        if file == self.config.executable:
            for arg in shlex.split(args):
                if arg.upper().startswith('/PASSWORD='):
                    yield NewPassword(arg[10:])
        return 0x40

    def _PROCESSENTRY32(self, record: list, pid: int):
        path = self.config.processes[pid]
        if path is None:
            _, path = ntpath.split(self.config.executable)
        ...                 # DWORD     dwSize
        record[1] = 0       # DWORD     cntUsage
        record[2] = pid     # DWORD     th32ProcessID
        record[3] = 0       # ULONG_PTR th32DefaultHeapID
        record[4] = 0       # DWORD     th32ModuleID
        record[5] = 1       # DWORD     cntThreads
        record[6] = 0       # DWORD     th32ParentProcessID
        record[7] = 0       # LONG      pcPriClassBase
        record[8] = 0       # DWORD     dwFlags
        szExe = record[9]   # CHAR      szExeFile[MAX_PATH]
        szExe[:len(path)] = path # noqa
        szExe[ len(path)] = '\0' # noqa

    @external()
    def kernel32__GetLastError(self) -> int:
        return self._win32api_last_error

    @external()
    def kernel32__SetLastError(self, code: int):
        self._win32api_last_error = code

    @external()
    def kernel32__CreateToolhelp32Snapshot(self, flags: int, process_id: int) -> int:
        if not self.config.allow_process_listing:
            return -1
        return random.randrange(0x10, 0x100000)

    @external(alias=['Process32FirstW'])
    def kernel32__Process32First(self, handle: int, p_record: Variable[list]) -> bool:
        if not (pids := self._win32api_sorted_process_ids):
            return False
        record = p_record.get()
        pid = pids[0]
        self._PROCESSENTRY32(record, pid)
        p_record.set(record)
        return True

    @external(alias=[
        'LoadLibraryW',
        'LoadLibraryA',
    ])
    def kernel32__LoadLibrary(self, name: str) -> int:
        name = name.lower()
        base, dot, ext = name.rpartition('.')
        if dot and ext.lower() == 'dll':
            name = base
        return self.winapi_module_to_addr.get(name, 0)

    @external(alias=['GetProcAddressA'])
    def kernel32__GetProcAddress(self, module: int, name: str) -> int:
        if module not in self.winapi_addr_to_module:
            return 0
        return self.winapi_symbol_to_addr.get(name, 0)

    @external(alias=['Process32NextW'])
    def kernel32__Process32Next(self, handle: int, p_record: Variable[list]) -> bool:
        record = p_record.get()
        pid = record[2]
        pids = self._win32api_sorted_process_ids
        k = bisect.bisect_right(pids, pid)
        if k < len(pids):
            pid = pids[k]
            self._PROCESSENTRY32(record, pid)
            p_record.set(record)
            return True
        else:
            self.kernel32__SetLastError(ERROR_NO_MORE_FILES)
            return False

    @external()
    def kernel32__GetTickCount(self) -> int:
        tick = self.clock
        tick *= self.config.milliseconds_per_instruction
        tick += self.seconds_slept * 1000
        return int(tick)

    @external()
    def user32__GetSystemMetrics(self, index: int) -> int:
        if index == 80:
            return 1
        if index == 43:
            return 2
        return 0

    @external()
    def IsArm32Compatible(self) -> bool:
        return architecture('arm32compatible', self.config.x64)

    @external()
    def IsArm64(self) -> bool:
        return architecture('arm64', self.config.x64)

    @external()
    def IsX64(self) -> bool:
        return architecture('x64', self.config.x64)

    @external()
    def IsX64OS(self) -> bool:
        return architecture('x64os', self.config.x64)

    @external()
    def IsX64Compatible(self) -> bool:
        return architecture('x64compatible', self.config.x64)

    @external()
    def IsX86(self) -> bool:
        return architecture('x86', self.config.x64)

    @external()
    def IsX86OS(self) -> bool:
        return architecture('x86os', self.config.x64)

    @external()
    def IsX86Compatible(self) -> bool:
        return architecture('x86compatible', self.config.x64)

    @external(alias=[
        'Debugging',
        'IsUninstaller',
    ])
    def Terminated(self) -> bool:
        return False

    @external()
    def IsAdmin(self) -> bool:
        return self.config.admin

    @external(name='Sleep')
    @external()
    def kernel32__Sleep(self, ms: int):
        seconds = ms / 1000.0
        self.seconds_slept += seconds

    @external()
    def Random(self, top: int) -> int:
        return random.randrange(0, top)

    @external(alias='StrGet')
    def WStrGet(self, string: Variable[str], index: int) -> str:
        text = string.get()
        if not 1 <= index <= len(text):
            raise IFPSException(
                _OUT_OF_STRING_RANGE,
                PSError.CustomError,
                failed_call=self.dialect is Dialect.Inno6,
            )
        return text[index - 1]

    @external(alias='StrSet')
    def WStrSet(self, char: str, index: int, dst: Variable[str]):
        old = dst.get()
        if not 1 <= index <= len(old):
            raise IFPSException(_OUT_OF_STRING_RANGE, PSError.CustomError)
        dst.set(old[:index - 1] + char + old[index:])

    @external()
    def GetEnv(self, name: str) -> str:
        return self.config.environment.get(name, F'%{name}%')

    @external()
    def Beep(self):
        pass

    @external()
    def Abort(self):
        if self.config.throw_abort:
            raise AbortEmulation
        raise IFPSException(SysConst.SOperationAborted)

    @external()
    def DirExists(self, path: str) -> bool:
        return self.installed.directory_exists(path)

    @external()
    def FileOrDirExists(self, path: str) -> bool:
        return self.installed.exists(path)

    @external()
    def ForceDirectories(self, path: str) -> bool:
        return True

    @external(alias='LoadStringFromLockedFile')
    def LoadStringFromFile(self, path: str, out: Variable[str]) -> bool:
        return True

    @external(alias='LoadStringsFromLockedFile')
    def LoadStringsFromFile(self, path: str, out: Variable[str]) -> bool:
        return True

    def code_runner(self, caller: _ScriptRun | None) -> constants.CodeRunner:
        """
        Runs the function of a code constant inside the run of the caller, or in a new run of the
        script without one.
        """
        def run_code(function: str, argument: str) -> Generator[Any, Any, str]:
            value = yield from self.call_by_name(function, [argument], CODE_CONSTANT, caller)
            return str(value)
        return run_code

    @external(caller=True)
    def ExpandConstant(self, string: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        return self.constants.expand(string, self.code_runner(caller))

    @external(caller=True)
    def ExpandConstantEx(
        self,
        string: str,
        custom_var: str,
        custom_val: str,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, str]:
        return self.constants.expand(string, self.code_runner(caller), {custom_var: custom_val})

    def expand_constant(self, string: str, custom_var: str | None = None, custom_val: str = '') -> str:
        """
        Expands the constants in the given string as Setup expands them, with one optional custom
        constant. A code constant runs a function of the script; its calls are not reported.
        """
        custom = None if custom_var is None else {custom_var: custom_val}
        expansion = EventCall(self._timed(self.constants.expand(string, self.code_runner(None), custom)))
        value = expansion.value
        assert value is not None
        return value

    @external()
    def DeleteFile(self, path: str) -> bool:
        self.installed.remove(path)
        return True

    @external()
    def FileExists(self, file_name: str) -> bool:
        return self.installed.file_exists(file_name)

    @external()
    def Log(self, log: str):
        ...

    @external()
    def Inc(self, p: Variable[int]):
        if not p.deref().type.code.integer:
            raise IFPSException(kind=PSError.CouldNotCallProc)
        p.set(p.get() + 1)

    @external()
    def Dec(self, p: Variable[int]):
        if not p.deref().type.code.integer:
            raise IFPSException(kind=PSError.CouldNotCallProc)
        p.set(p.get() - 1)

    @external()
    def FindFirst(self, file_name: str, frec: Variable) -> bool:
        return False

    @external()
    def Round(self, x: x87.Real) -> int:
        if self.setup.release.since((6, 6, 0)):
            return self.runtime.fpu.integer(x, x87.Rounding.Nearest)
        return self.runtime.fpu.integer(x)

    @external()
    def Trunc(self, x: x87.Real) -> int:
        return self.runtime.fpu.integer(x, x87.Rounding.TowardZero)

    @external()
    def Int(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.round_integral(x, x87.Rounding.TowardZero)

    @external()
    def Abs(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.absolute(x)

    @external()
    def Pi(self) -> x87.Real:
        return self.runtime.fpu.pi()

    @external()
    def Sqrt(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.square_root(x)

    @external()
    def Sin(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.sine(x)

    @external()
    def Cos(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.cosine(x)

    @external()
    def GetSpaceOnDisk(
        self,
        path: str,
        in_megabytes: bool,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        _a = 3_000_000
        _t = 5_000_000
        if not in_megabytes:
            _a *= 1000
            _t *= 1000
        avail.set(_a)
        space.set(_t)
        return True

    @external()
    def GetSpaceOnDisk64(
        self,
        path: str,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        avail.set(3_000_000_000)
        space.set(5_000_000_000)
        return True

    @external()
    def Exec(
        self,
        exe: str,
        cmd: str,
        cwd: str,
        show: int,
        wait: int,
        out: Variable[int],
    ) -> bool:
        out.set(0)
        return True

    @external()
    def GetCmdTail(self) -> str:
        return ''

    @external()
    def ParamCount(self) -> int:
        return 0

    @external()
    def ParamStr(self, index: int) -> str:
        return ''

    @external()
    def ActiveLanguage(self) -> str:
        return self.setup.language_name

    @external()
    def CustomMessage(self, msg_name: str) -> str:
        if (message := self.setup.custom_message(msg_name)) is None:
            raise InternalError(F'Unknown custom message name "{msg_name}"')
        return message

    @external()
    def FmtMessage(self, fmt: str, args: list[str]) -> str:
        return constants.fmt_message(fmt, args)

    @external()
    def Format(self, fmt: str, args: Variable[list]) -> str:
        elements = args.deref()
        arguments = [_open_array_element(elements.at(k)) for k in range(len(elements))]
        return formatting.format(fmt, arguments, self.runtime, self._reserve_text)

    @external()
    def SetupMessage(self, id: int) -> str:
        return self.setup.message(id)

    @external()
    def IsWin64(self) -> bool:
        return self.config.x64

    @external()
    def Is64BitInstallMode(self) -> bool:
        return self.setup.install_mode.decided()

    @external()
    def RaiseException(self, msg: str):
        raise IFPSException(msg)

    @external(caller=True)
    def GetExceptionMessage(self, caller: _ScriptRun | None = None) -> str:
        if caller is None or (error := caller.current_exception) is None:
            return '(There is no current exception)'
        return error.message

    @external()
    def ProcessorArchitecture(self) -> int:
        return processor(self.config.x64).value

    @external()
    def GetUserNameString(self) -> str:
        return self.config.user_name

    @external()
    def GetComputerNameString(self) -> str:
        return self.config.host_name

    @external()
    def GetUILanguage(self) -> int:
        return self.config.lcid

    @external()
    def GetArrayLength(self, array: Variable) -> int:
        array = array.deref()
        return len(array)

    @external()
    def SetArrayLength(self, array: Variable, n: int):
        array = array.deref()
        if isinstance(t := array.type, TArray):
            self._reserve_variables(t.type, n)
        array.resize(n)

    @external()
    def WizardForm(self) -> object:
        return self._form()

    @external()
    def MainForm(self) -> object:
        """
        The function of Inno Setup 5 that gives Setup's main form; before Setup created the form,
        it raises an internal error.
        """
        if (main := self.wizard_form.main) is None:
            raise InternalError('An attempt was made to access MainForm before it has been created')
        return main

    def _form(self) -> WizardForm:
        """
        The wizard form, for a function that Setup runs through `GetWizardForm`.
        """
        form = self.wizard_form
        form.require()
        return form

    @external()
    def CreateCustomPage(self, after: int, caption: str, description: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputQueryPage(self, after: int, caption: str, description: str, subcaption: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputOptionPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        exclusive: bool,
        listbox: bool,
    ) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputDirPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        append: bool,
        folder: str,
    ) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputFilePage(self, after: int, caption: str, description: str, subcaption: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateOutputMsgPage(self, after: int, caption: str, description: str, message: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateOutputMsgMemoPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        message: str,
    ) -> object:
        return self._form().add_page(after)

    @external(alias='CreateOutputMarqueeProgressPage')
    def CreateOutputProgressPage(self, caption: str, description: str) -> object:
        return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)

    @external(alias='CreateExtractionPage')
    def CreateDownloadPage(self, caption: str, description: str, progress: object) -> object:
        return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)

    @external()
    def TWizardPage__GetID(self, this: object) -> int:
        return _wizard_page(this).id

    @external()
    def Class__SetNil(self, value: object) -> object:
        """
        Pascal Script's assignment of nil to a variable of a class.
        """
        return None

    @external()
    def Class__CastToType(self, source: object) -> object:
        """
        A cast of an object to a class; the emulator's objects do not record the classes that the
        script knows, and the cast keeps the object.
        """
        return source

    @external()
    def GetWizardForm(self) -> object:
        return self._form()

    @external()
    def PageFromID(self, id: int) -> object:
        return self._form().page(id)

    @external()
    def PageIndexFromID(self, id: int) -> int:
        return self._form().page_index(id)

    @external()
    def WizardDirValue(self) -> str:
        return self.setup.path_func.remove_backslash_unless_root(self._form().dir_edit.text)

    @external()
    def WizardGroupValue(self) -> str:
        return self.setup.path_func.remove_backslash_unless_root(self._form().group_edit.text)

    @external()
    def IsAdminInstallMode(self) -> bool:
        return self.setup.admin_install_mode

    @external(caller=True)
    def GetPreviousData(self, name: str, default: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        app_id = '' if (info := self.setup_info) is None else info.Header.text(TextDirective.AppId) or ''
        yield from self.constants.expand(app_id, self.code_runner(caller))
        return default

    @external()
    def TWizardForm__GetCurPageID(self, this: object) -> int:
        return self.wizard_form.current

    def _set_page_handler(self, this: object, event: PageEvent, handler: object):
        page = _wizard_page(this)
        if isinstance(handler, Function):
            page.handlers[event] = handler
        else:
            page.handlers.pop(event, None)

    @external()
    def TWizardPage__SetOnActivate(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnActivate, handler)

    @external()
    def TWizardPage__SetOnShouldSkipPage(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnShouldSkipPage, handler)

    @external()
    def TWizardPage__SetOnNextButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnNextButtonClick, handler)

    @external()
    def TWizardPage__SetOnBackButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnBackButtonClick, handler)

    @external()
    def TWizardPage__SetOnCancelButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnCancelButtonClick, handler)

    @external()
    def Unassigned(self) -> Variant:
        return variants.EMPTY

    @external()
    def Null(self) -> Variant:
        return variants.NULL

    @external()
    def VarType(self, v: Variant) -> int:
        return v.type

    @external()
    def VarIsNull(self, v: Variant) -> bool:
        return v.type == VarType.Null

    @external()
    def VarIsEmpty(self, v: Variant) -> bool:
        return v.type == VarType.Empty

    @external()
    def VarIsClear(self, v: Variant) -> bool:
        return v.type == VarType.Empty or v.type in (VarType.Dispatch, VarType.Unknown) and v.value is None

    @external()
    def Set8087CW(self, cw: int):
        self.runtime.control_word = cw & 0xFFFF

    @external()
    def Get8087CW(self) -> int:
        return self.runtime.control_word | x87.RESERVED_BIT

    @external()
    def GetDateTimeString(
        self,
        fmt: str,
        date_separator: str,
        time_separator: str,
    ) -> str:

        now = self.config.start_time
        now = now + timedelta(
            milliseconds=(self.config.milliseconds_per_instruction * self.clock))
        now = now + timedelta(seconds=self.seconds_slept)

        date_separator = date_separator.lstrip('\0')
        time_separator = time_separator.lstrip('\0')

        def dt(m: re.Match[str]):
            spec = m[1]
            ampm = m[2]
            if ampm:
                am, _, pm = ampm.partition('/')
                spec = spec.upper()
                suffix = now.strftime('%p').lower()
                suffix = {'am': am, 'pm': pm}[suffix]
            else:
                suffix = ''
            if spec == 'dddddd' or spec == 'ddddd':
                return now.date().isoformat()
            if spec == 't':
                return now.time().isoformat('minutes')
            if spec == 'tt':
                return now.time().isoformat('seconds')
            if spec == 'd':
                return str(now.day)
            if spec == 'm':
                return str(now.month)
            if spec == 'h':
                return str(now.hour)
            if spec == 'n':
                return str(now.minute)
            if spec == 's':
                return str(now.second)
            if spec == 'H':
                return now.strftime('%I').lstrip('0') + suffix
            if spec == '/':
                return date_separator or spec
            if spec == ':':
                return time_separator or spec
            return now.strftime({
                'dddd'  : '%A',
                'ddd'   : '%a',
                'dd'    : '%d',
                'mmmm'  : '%B',
                'mmm'   : '%b',
                'mm'    : '%m',
                'yyyy'  : '%Y',
                'yy'    : '%y',
                'hh'    : '%H',
                'HH'    : '%I' + suffix,
                'nn'    : '%M',
                'ss'    : '%S',
            }.get(spec, m[0]))

        split = re.split(F'({formats.string!s})', fmt)
        for k in range(0, len(split), 2):
            split[k] = re.sub('(d+|m+|y+|h+|n+|s+|t+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
        for k in range(1, len(split), 2):
            split[k] = split[k][1:-1]
        return ''.join(split)

    @external()
    def Chr(self, b: int) -> str:
        return chr(b)

    @external()
    def Ord(self, c: str) -> int:
        return ord(c)

    @external()
    def Copy(self, string: str, index: int, count: int) -> str:
        index -= 1
        return string[index:index + count]

    @external()
    def Length(self, string: str) -> int:
        return len(string)

    @external(alias='AnsiLowercase')
    def Lowercase(self, string: str) -> str:
        return string.lower()

    @external(alias='AnsiUppercase')
    def Uppercase(self, string: str) -> str:
        return string.upper()

    @external(alias='Replicate')
    def StringOfChar(self, c: str, count: int) -> str:
        self._reserve_text(count, wide=False)
        return c * count

    @external()
    def Delete(self, string: Variable[str], index: int, count: int):
        index -= 1
        old = string.get()
        string.set(old[:index] + old[index + count:])

    @external()
    def Insert(self, string: str, dest: Variable[str], index: int):
        index -= 1
        old = dest.get()
        dest.set(old[:index] + string + old[index:])

    @external()
    def StringChange(self, string: Variable[str], old: str, new: str) -> int:
        return self.StringChangeEx(string, old, new, False)

    @external()
    def StringChangeEx(self, string: Variable[str], old: str, new: str, _: bool) -> int:
        haystack = string.get()
        count = haystack.count(old)
        self._reserve_text(len(haystack) + count * (len(new) - len(old)))
        string.set(haystack.replace(old, new))
        return count

    @external()
    def Pos(self, sub: str, string: str) -> int:
        if not sub:
            return 0
        return string.find(sub) + 1

    @external()
    def AddQuotes(self, string: str) -> str:
        if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
            string = F'"{string}"'
        return string

    @external()
    def RemoveQuotes(self, string: str) -> str:
        if string and string[0] == '"' and string[~0] == '"':
            string = string[1:-1]
        return string

    @external()
    def CompareText(self, a: str, b: str) -> int:
        return self.CompareStr(a.casefold(), b.casefold())

    @external()
    def CompareStr(self, a: str, b: str) -> int:
        if a > b:
            return +1
        if a < b:
            return -1
        return 0

    @external()
    def SameText(self, a: str, b: str) -> bool:
        return a.casefold() == b.casefold()

    @external()
    def SameStr(self, a: str, b: str) -> bool:
        return a == b

    @external()
    def IsWildcard(self, pattern: str) -> bool:
        return '*' in pattern or '?' in pattern

    @external()
    def WildcardMatch(self, text: str, pattern: str) -> bool:
        return fnmatch.fnmatch(text, pattern)

    @external()
    def Trim(self, string: str) -> str:
        return strings.trim(string)

    @external()
    def TrimLeft(self, string: str) -> str:
        return strings.trim_left(string)

    @external()
    def TrimRight(self, string: str) -> str:
        return strings.trim_right(string)

    @external()
    def StringJoin(self, sep: str, values: list[str]) -> str:
        self._reserve_text(sum(map(len, values)) + len(sep) * max(len(values) - 1, 0))
        return sep.join(values)

    @external()
    def StringSplitEx(self, string: str, separators: list[str], quote: str, how: TSplitType) -> list[str]:
        def spans():
            found = _split_spans(string, separators, quote)
            if how == TSplitType.stExcludeEmpty:
                return (span for span in found if span[0] < span[1])
            if how == TSplitType.stExcludeLastEmpty:
                return _without_empty_last(found)
            return found
        self._reserve_variables(_StringType, sum(1 for _ in spans()))
        return [string[start:end] for start, end in spans()]

    @external()
    def StringSplit(self, string: str, separators: list[str], how: TSplitType) -> list[str]:
        return self.StringSplitEx(string, separators, '', how)

    @external()
    def StrToInt(self, s: str) -> int:
        value, error = numbers.val_integer(s, 32)
        return -1 if error else value

    @external()
    def StrToIntDef(self, s: str, d: int) -> int:
        value, error = numbers.val_integer(s, 32)
        return d if error else value

    @external()
    def StrToInt64(self, s: str) -> int:
        return numbers.str_to_int64(s)

    @external()
    def StrToInt64Def(self, s: str, d: int) -> int:
        return numbers.str_to_int64_default(s, d)

    @external()
    def StrToFloat(self, s: str) -> x87.Real:
        runtime = self.runtime
        value, error = numbers.val_float(s, runtime.library)
        if error or value is None:
            raise IFPSException(_INVALID_FLOAT)
        return runtime.fpu.store(value, x87.Format.Extended)

    @external()
    def FloatToStr(self, e: x87.Real) -> str:
        if self.dialect is Dialect.Inno6:
            return numbers.str_fixed(e, 12, self.runtime.library)
        return numbers.float_to_str(e, self.runtime)

    @external()
    def StrToUInt64(self, s: str) -> int:
        return numbers.str_to_uint64(s)

    @external()
    def StrToUInt64Def(self, s: str, d: int) -> int:
        return numbers.str_to_uint64_default(s, d)

    @external(alias='UInt64ToStr')
    def UIntToStr(self, i: int) -> str:
        return str(i & 0xFFFFFFFFFFFFFFFF)

    @external(alias='Int64ToStr')
    def IntToStr(self, i: int) -> str:
        return str(i)

    @external()
    def PadL(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.rjust(n)

    @external()
    def PadR(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.ljust(n)

    @external()
    def PadZ(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.rjust(n, '0')

    @external()
    def StrToVersion(self, s: str, v: Variable[int]) -> bool:
        try:
            packed = bytes(map(int, s.split('.')))
        except Exception:
            return False
        if len(packed) != 4:
            return False
        v.set(int.from_bytes(packed, 'little'))
        return True

    @external()
    def CharLength(self, string: str, index: int) -> int:
        return 1

    @external()
    def AddBackslash(self, string: str) -> str:
        return self.setup.path_func.add_backslash(string)

    @external()
    def AddPeriod(self, string: str) -> str:
        if string and string[~0] != '.':
            string = F'{string}.'
        return string

    @external()
    def RemoveBackslashUnlessRoot(self, string: str) -> str:
        return self.setup.path_func.remove_backslash_unless_root(string)

    @external()
    def RemoveBackslash(self, string: str) -> str:
        return self.setup.path_func.remove_backslash(string)

    @external()
    def ChangeFileExt(self, name: str, ext: str) -> str:
        return self.setup.path_func.change_ext(name, ext)

    @external()
    def ExtractFileExt(self, name: str) -> str:
        return self.setup.path_func.extract_ext(name)

    @external()
    def ExtractFileDir(self, name: str) -> str:
        return self.setup.path_func.extract_dir(name)

    @external()
    def ExtractFilePath(self, name: str) -> str:
        return self.setup.path_func.extract_path(name)

    @external()
    def ExtractFileName(self, name: str) -> str:
        return self.setup.path_func.extract_name(name)

    @external()
    def ExtractFileDrive(self, name: str) -> str:
        return self.setup.path_func.extract_drive(name)

    @external()
    def ExtractRelativePath(self, base: str, dst: str) -> str:
        return self.setup.path_func.extract_relative_path(base, dst)

    @external()
    def ExpandFileName(self, name: str) -> str:
        try:
            return self.setup.path_func.expand(name, self.current_folder.path)
        except PathFuncError as error:
            raise DelphiException(str(error)) from error

    @external()
    def ExpandUNCFileName(self, name: str) -> str:
        return expand_file_name(name, self.current_folder.path)

    @external()
    def GetCurrentDir(self) -> str:
        return self.current_folder.path

    @external()
    def SetLength(self, target: Variable, size: int):
        target = target.deref()
        if target.container:
            self.SetArrayLength(target, size)
            return
        size = max(size, 0)
        self._reserve_text(size, wide=target.type.code not in (TC.AnsiString, TC.PChar))
        old = target.get()
        old = old.ljust(size, '\0')
        target.set(old[:size])

    @external(alias='OemToCharBuff')
    def CharToOemBuff(self, string: Variable[str]):
        # TODO
        ...

    @external()
    def Utf8Encode(self, string: str) -> str:
        return string.encode('utf8').decode('latin1')

    @external()
    def Utf8Decode(self, string: str) -> str:
        return string.encode('latin1').decode('utf8')

    @external()
    def GetMD5OfString(self, string: str) -> str:
        return hashlib.md5(string.encode('latin1')).hexdigest()

    @external()
    def GetMD5OfUnicodeString(self, string: str) -> str:
        return hashlib.md5(string.encode('utf8')).hexdigest()

    @external()
    def GetSHA1OfString(self, string: str) -> str:
        return hashlib.sha1(string.encode('latin1')).hexdigest()

    @external()
    def GetSHA1OfUnicodeString(self, string: str) -> str:
        return hashlib.sha1(string.encode('utf8')).hexdigest()

    @external()
    def GetSHA256OfString(self, string: str) -> str:
        return hashlib.sha256(string.encode('latin1')).hexdigest()

    @external()
    def GetSHA256OfUnicodeString(self, string: str) -> str:
        return hashlib.sha256(string.encode('utf8')).hexdigest()

    @external()
    def SysErrorMessage(self, code: int) -> str:
        return F'[description for error {code & 0xFFFFFFFF:08X}]'

    @external()
    def MinimizePathName(self, path: str, font: object, max_len: int) -> str:
        return path

    @external()
    def CheckForMutexes(self, mutexes: str) -> bool:
        return any(m in self.mutexes for m in mutexes.split(','))

    @external()
    def CreateMutex(self, name: str):
        if self.config.log_mutexes:
            yield NewMutex(name)
        self.mutexes.add(name)

    @external()
    def GetWinDir(self) -> str:
        return self.expand_constant('{win}')

    @external()
    def GetSystemDir(self) -> str:
        return self.expand_constant('{sys}')

    @external()
    def GetWindowsVersion(self) -> int:
        version = int.from_bytes(
            struct.pack('>BBH', *self.config.windows_os_version), 'big')
        return version

    @external()
    def GetWindowsVersionEx(self, tv: Variable[int | bool]):
        tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
        tv[3], tv[4]        = self.config.windows_sp_version # noqa
        tv[5], tv[6], tv[7] = True, 0, 0

    @external()
    def GetWindowsVersionString(self) -> str:
        return '{}.{:02d}.{:04d}'.format(*self.config.windows_os_version)

    @external()
    def CreateOleObject(self, name: str) -> OleObject:
        return OleObject(name)

    @external()
    def GetActiveOleObject(self, name: str) -> OleObject:
        return OleObject(name)

    @external()
    def IDispatchInvoke(self, ole: OleObject | None, prop_set: bool, name: str, value: Any) -> Variant:
        if ole is None:
            raise IFPSException(_NIL_INVOKE)
        return variants.EMPTY

    @external()
    def FindWindowByClassName(self, name: str) -> int:
        return 0

    @external()
    def FindWindowByWindowName(self, name: str) -> int:
        return 0

    @external()
    def WizardSilent(self) -> bool:
        return self.config.wizard_silent

    @external()
    def WizardNoIcons(self) -> bool:
        return self._form().icons_off

    @external()
    def WizardSetupType(self, descriptions: bool) -> str:
        form = self._form()
        if (setup_type := form.setup_type) is None:
            return ''
        return form.type_description(setup_type) if descriptions else setup_type.Name

    def _field_expander(self, caller: _ScriptRun | None) -> Expander:
        """
        Expands the fields of the entries in the lists of the wizard for a support function that the
        script calls: a code constant runs inside the run of the caller, as in `ExpandConstant`.
        """
        def expand(entry: object, field: EntryField, text: str) -> Generator[Any, Any, str]:
            return self.constants.expand(text, self.code_runner(caller))
        return expand

    @external(caller=True)
    def WizardSelectedComponents(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        expand = self._field_expander(caller)
        return comma_text((yield from self._form().selected_components(descriptions, False, expand)))

    @external(caller=True)
    def WizardSelectedTasks(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        expand = self._field_expander(caller)
        return comma_text((yield from self._form().selected_tasks(descriptions, False, False, expand)))

    @external(alias=['IsComponentSelected'])
    def WizardIsComponentSelected(self, components: str) -> bool:
        return _selected(self._form().selection().components, components)

    @external(alias=['IsTaskSelected'])
    def WizardIsTaskSelected(self, tasks: str) -> bool:
        return _selected(self._form().selection().tasks, tasks)

    @external()
    def WizardSelectComponents(self, components: str):
        self._form().select_components(comma_strings(components.replace('/', '\\')))

    @external()
    def WizardSelectTasks(self, tasks: str):
        self._form().select_tasks(comma_strings(tasks.replace('/', '\\')))

    @external()
    def TWizardForm__GetComponentsList(self, this: object) -> object:
        return self.wizard_form.components

    @external()
    def TWizardForm__GetTasksList(self, this: object) -> object:
        return self.wizard_form.tasks

    @external()
    def TWizardForm__GetRunList(self, this: object) -> object:
        return self.wizard_form.run_list

    @external()
    def TWizardForm__GetTypesCombo(self, this: object) -> object:
        return self.wizard_form.types_combo

    @external()
    def TWizardForm__GetNoIconsCheck(self, this: object) -> object:
        return self.wizard_form.no_icons_check

    @external()
    def TNewCheckListBox__GetChecked(self, this: object, index: int) -> bool:
        if isinstance(this, CheckList):
            return this.checked(index)
        return True

    @external()
    def TNewCheckListBox__SetChecked(self, this: object, index: int, checked: bool):
        if isinstance(this, CheckList):
            this.set_checked(index, checked)

    @external()
    def TNewCheckListBox__GetState(self, this: object, index: int) -> int:
        if isinstance(this, CheckList):
            return this.state(index)
        return 1

    @external()
    def TNewCheckListBox__GetItemEnabled(self, this: object, index: int) -> bool:
        if isinstance(this, CheckList):
            return this.enabled(index)
        return True

    @external()
    def TNewCheckListBox__CheckItem(self, this: object, index: int, operation: int) -> bool:
        if isinstance(this, CheckList):
            return this.check(index, CheckOperation(operation))
        return True

    @external()
    def CurrentFilename(self) -> str:
        if (file := self.current_file) is None or not file.destination:
            raise _outside_file_entry('CurrentFilename')
        return file.destination

    @external()
    def CurrentSourceFilename(self) -> str:
        if (file := self.current_file) is None or not file.source:
            raise _outside_file_entry('CurrentSourceFilename', ' with flag "external"')
        return file.source

    def expand(self, text: str, custom: Mapping[str, str] | None = None) -> Generator[Any, Any, str]:
        """
        Expands the constants in a text of the setup where Setup expands it, with the custom
        constants that Setup passes for it; each code constant runs in a new run of the script.
        """
        return self.constants.expand(text, self.code_runner(None), custom)

    def registry_key_exists(self, key: str) -> bool:
        """
        Whether a key of the emulated registry exists.
        """
        return self.constants.registry('', None, key)

    @property
    def temporary_directory(self) -> str:
        """
        The temporary directory of Setup, the value of `{tmp}`.
        """
        return self.constants.temporary_directory

    def check_time(self):
        """
        Ends the emulation when its time is up.
        """
        if (watch := self._stopwatch) is not None and watch.expired():
            raise EmulatorTimeout

    def _setup_codes(self) -> dict[ArgumentKind, TC]:
        """
        The type codes of the variables that Setup creates for the values that it passes to the
        functions of the script and for the results that it takes from them.
        """
        return {
            ArgumentKind.String  : TC.UnicodeString if self.ifps.unicode else TC.AnsiString,
            ArgumentKind.Integer : TC.S32,
            ArgumentKind.Boolean : TC.U08,
            ArgumentKind.Step    : TC.U08,
            ArgumentKind.Page    : TC.Class,
            ArgumentKind.Object  : TC.Class,
        }

    def _fits(self, function: Function, call: SetupCall) -> bool:
        """
        Whether the declaration of a function of the script takes the call that Setup makes: the
        same number of parameters, each passed the same way and of the type that Setup passes, and
        a result of the type that Setup takes. A parameter or a result whose type the script does
        not record is taken to fit, as Pascal Script records none for a support function that Inno
        Setup does not document, such as `UsingWinNT`.
        """
        if (decl := function.decl) is None or len(decl.parameters) != len(call.parameters):
            return False
        codes = self._setup_codes()
        for parameter, (kind, by_value) in zip(decl.parameters, call.parameters):
            if parameter.const != by_value:
                return False
            if parameter.type is not None and parameter.type.code != codes[kind]:
                return False
        if call.result is None:
            return decl.void
        if decl.void:
            return False
        return (result := decl.return_type) is None or result.code == codes[call.result]

    def call_from_setup(
        self,
        function: Function,
        arguments: Sequence,
        call: SetupCall,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, Any]:
        """
        Calls a function of the script as Setup does: Pascal Script runs a function whose
        declaration does not take the call on a stack that does not fit it, and the emulation ends
        there. Setup creates the variables of the values that it passes and of the result that it
        takes.
        """
        if not self._fits(function, call):
            raise EmulatorUndefined(F'Setup calls {function.name} as {call}, which its declaration does not take.')
        codes = self._setup_codes()
        types = [TPrimitive(codes[kind]) for kind, _ in call.parameters]
        result = None if call.result is None else TPrimitive(codes[call.result])
        run = _ScriptRun(self.dialect, caller)
        return (yield from self._execute(run, function, arguments, types, result))

    def call_by_name(
        self,
        name: str,
        arguments: list,
        call: SetupCall,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, Any]:
        """
        Calls a function of the script by name as Setup's `CodeRunner` finds it: a function of the
        script or a support function that the script imports.
        """
        if (function := self.symbols.get(name)) is None:
            raise DelphiException('Script error: Could not call proc')
        return (yield from self.call_from_setup(function, arguments, call, caller))

    @external()
    def Low(self, var: Variable) -> int:
        return self._bound(var.deref(), False)

    @external()
    def High(self, var: Variable) -> int:
        return self._bound(var.deref(), True)

    def _bound(self, var: Variable, high: bool) -> int:
        t = var.type
        code = t.code
        if code is TC.Array:
            return len(var) - 1 if high else 0
        if isinstance(t, TStaticArray):
            start = t.offset or 0
            return start + t.size - 1 if high else start
        if code is TC.AnsiString:
            return len(var.get()) if high else 1
        if (valid := code.integer_range) is not None:
            return valid.stop - 1 if high else valid.start
        raise IFPSException(kind=PSError.CouldNotCallProc)

    @external()
    def SizeOf(self, var: Variable) -> int:
        return pascal.size_of(var.type, self.runtime)

    del external


_FORM_CLASSES = frozenset({
    'TWizardForm',
    'TMainForm',
    'TUninstallProgressForm',
})
"""
The classes of Setup's forms, which the emulator holds itself: a member of one called on an empty
variable fails as Pascal Script fails it, twice, so that the innermost except block does not end
the error.
"""

_FORM_VARIABLES = frozenset({
    'WIZARDFORM',
    'MAINFORM',
    'UNINSTALLPROGRESSFORM',
})
"""
The global variables of a script of Inno Setup 6 or later that hold Setup's forms.
"""

_NOTIFY_CALL = SetupCall.of(parse_function('procedure OnChange(Sender: TObject)'))

_EDIT_CHAR_CASES = frozenset(TEditCharCase)

_LAYOUT_CLASSES = frozenset({
    'TObject',
    'TPersistent',
    'TComponent',
    'TControl',
    'TWinControl',
    'TCustomControl',
    'TGraphicControl',
})
"""
The classes whose members every control of the wizard has, such as its position and visibility.
"""

_APPEARANCE = frozenset({
    'anchors',
    'borderstyle',
    'color',
    'ctl3d',
    'cursor',
    'flat',
    'font',
    'hint',
    'itemcaption',
    'itemfontstyle',
    'itemlevel',
    'itemsubitem',
    'minitemheight',
    'offset',
    'parentcolor',
    'parentfont',
    'showhint',
    'showlines',
    'subitemfontstyle',
    'taborder',
    'tabstop',
    'wanttabs',
})
"""
The properties of the lists, combo boxes and check boxes of the wizard that only change how they
look.
"""


def _changes_choices(function: Function) -> bool:
    """
    Whether a member of a control of the wizard that the emulator does not model may change the
    choices that the control holds: any member but those of every control, those that only change
    how it looks, and the casts of Pascal Script, whose class is `Class`.
    """
    classname = function.decl.classname if function.decl is not None else None
    name = function.name
    if not classname:
        classname, dot, name = name.partition('.')
        if not dot:
            return False
    if classname == 'Class' or classname in _LAYOUT_CLASSES:
        return False
    if name[:3].lower() in ('get', 'set') and name[3:].lower() in _APPEARANCE:
        return False
    return True


class InnoSetupEmulator(IFPSEmulator):
    """
    A specialized `IFPSEmulator` that can emulate the InnoSetup installation with a focus on
    continuing execution as much as possible.
    """

    def __init__(
        self,
        archive: InnoArchive | IFPSFile,
        options: IFPSEmulatorConfig | None = None,
        **more,
    ):
        super().__init__(archive, options, **more)
        cfg = self.config
        self.entries: SetupEntries | None = None
        if (info := self.setup_info) is not None:
            self.entries = SetupEntries.of(info, cfg.windows_os_version, cfg.windows_sp_version)

    def emulate_installation(
        self,
        password: str = '',
        max_seconds: float | None = None,
        password_test: Callable[[str], bool] | None = None,
    ):
        """
        Emulates the calls into the script that Setup makes during an installation in which the user
        clicks Next on every page until Setup finishes; a silent installation when the configuration
        says so. Each installation starts the script over. The given password is typed into the box
        on the password page. An event or entry that ends with a script error is reported as a
        `refinery.lib.inno.emulation.NewScriptError` and the installation goes on, as described
        for `refinery.lib.inno.installation.Installation`. Any other exception ends the emulation.
        A call of an emulated function that fails is reported as a
        `refinery.lib.inno.emulation.NewEmulationError`, also when the script handles its error.
        The time limit counts for the whole installation; the given number of seconds replaces the
        one of the configuration. Setup tests a password against its own before it asks the
        script; the caller can give that test as `password_test`, without it the test fails.
        """
        self.reset(walked=True)
        self.wizard_form.needs_restart = self.setup.always_restart
        installation = Installation(self, self.entries, password, password_test)
        return (yield from self._timed(installation.run(), max_seconds))

    def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
        """
        Any unimplemented function is essentially skipped. Any arguments passed by reference and
        all return values that are of type integer are set to `1` in an attempt to indicate success
        wherever possible. When the function is a member of a control of the wizard that holds
        choices of the installation, called on that control, the emulator no longer knows those
        choices, unless the member belongs to every control, such as its position, or only changes
        how the control looks.
        """
        if arguments and _changes_choices(function):
            self.wizard_form.touched(arguments[0].deref().data)
        if result is not None and not result.container and _holds_integer(result.type):
            result.set(1)
        for ptr in arguments:
            if not ptr.pointer:
                continue
            var = ptr.deref()
            if not var.container and _holds_integer(var.type):
                var.set(1)

Classes

class OleObject (name)

A dummy object representing an OLE interface created by an IFPS script. All it does so far is to remember the name of the object that was requested.

Expand source code Browse git
class OleObject:
    """
    A dummy object representing an OLE interface created by an IFPS script. All it does so far is
    to remember the name of the object that was requested.
    """
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return F'OleObject({self.name!r})'

    def __str__(self):
        return self.name
class Variable (type, spec=None, path=(), data=None)

This class represents a global or stack variable in the IFPS runtime.

Expand source code Browse git
class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: list[Variable] | _T | None
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = v

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: VariableSpec | str | None = None,
        path: tuple[int, ...] = (),
        data: _T | list | None = None
    ):
        super().__init__(type, spec)
        self.path = path
        self.setdefault()
        if data is not None:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return _DynamicArray()
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            elif type.code == TC.Pointer:
                return Variable(_VariantType)
            else:
                return pascal.default(type)

        self.data = default(self.type)

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables, or none if `n` is negative. A
        dynamic array that another variable holds too is copied before its length changes.
        """
        t = self.type
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if not isinstance(t, TArray) or not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        n = max(n, 0)
        if len(data) == n:
            return
        if data.holders > 1:
            data.holders -= 1
            kept = data[:n]
            self.data = data = _DynamicArray()
            for k, element in enumerate(kept):
                copy = Variable(t.type, self.spec, (*self.path, k))
                copy._copy_from(element)
                data.append(copy)
        del data[n:]
        for k in range(len(data), n):
            data.append(Variable(t.type, self.spec, (*self.path, k)))

    def _share(self, elements: _DynamicArray):
        if not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        if data is elements:
            return
        data.holders -= 1
        elements.holders += 1
        self.data = elements

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself. As
        in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the
        target of that pointer, or a copy of that target with its type.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        var = var.deref()
        if copy:
            var = Variable(var.type, data=var)
        self.data = var

    def set(self, value: _T | Sequence | Variable):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those. A container variable is copied
        element by element, and a pointer element as Pascal Script copies it: A pointer to a
        variable of the script points to the same variable, and a pointer that owns its target
        points to a copy of that target. A dynamic array shares the elements of the dynamic array
        that it is assigned. A value of Python code is converted as
        `refinery.lib.inno.pascal.representation` converts it; the interpreter converts between the
        types of the script.
        """
        if self.pointer:
            return self.deref().set(value)
        assigned = value
        if isinstance(value, Variable):
            source = value.deref()
            if self.container and source.container:
                return self._copy_elements(source)
            if source.type == self.type and not source.container:
                self.data = source.data
                return
            assigned = source.get()
        elif isinstance(value, Value):
            assigned = value.value
        elif isinstance(value, Enum):
            assigned = value.value
        if self.container:
            if not isinstance(assigned, (list, tuple)):
                raise TypeError
            if self.type.code is TC.Array:
                self._share(_DynamicArray())
            self.resize(len(assigned))
            for k, v in enumerate(assigned):
                self.data[k].set(v)
        else:
            self.data = pascal.representation(self.type, assigned)

    def _copy_elements(self, source: Variable):
        if (
            not isinstance(elements := source.data, list)
            or not isinstance(self.data, list)
        ):
            raise TypeError
        if self.type.code is TC.Array:
            if isinstance(elements, _DynamicArray):
                return self._share(elements)
            self._share(_DynamicArray())
        self.resize(len(elements))
        for element, copied in zip(self.data, elements):
            element._copy_from(copied)

    def _copy_from(self, copied: Variable):
        if not self.pointer:
            self.set(copied)
        elif isinstance(target := copied.data, Variable):
            self.setptr(target, copy=target.spec is None)
        else:
            raise type_mismatch()

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: list[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'

Ancestors

Instance variables

var container

A boolean indicating whether the given variable is a container.

Expand source code Browse git
@property
def container(self):
    """
    A boolean indicating whether the given variable is a container.
    """
    return self.type.container
var pointer

A boolean indicating whether the given variable is a pointer.

Expand source code Browse git
@property
def pointer(self):
    """
    A boolean indicating whether the given variable is a pointer.
    """
    return self.type.code == TC.Pointer
var name

Return the name of the variable as given by its spec.

Expand source code Browse git
@property
def name(self):
    """
    Return the name of the variable as given by its spec.
    """
    if self.spec is None:
        return 'Unbound'
    name = F'{self.spec!s}'
    for k in self.path:
        name = F'{name}[{k}]'
    return name
var data

The variable's value. This is a list of Variables for container types, a Variable for pointer types, and a basic type otherwise.

Expand source code Browse git
class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: list[Variable] | _T | None
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = v

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: VariableSpec | str | None = None,
        path: tuple[int, ...] = (),
        data: _T | list | None = None
    ):
        super().__init__(type, spec)
        self.path = path
        self.setdefault()
        if data is not None:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return _DynamicArray()
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            elif type.code == TC.Pointer:
                return Variable(_VariantType)
            else:
                return pascal.default(type)

        self.data = default(self.type)

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables, or none if `n` is negative. A
        dynamic array that another variable holds too is copied before its length changes.
        """
        t = self.type
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if not isinstance(t, TArray) or not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        n = max(n, 0)
        if len(data) == n:
            return
        if data.holders > 1:
            data.holders -= 1
            kept = data[:n]
            self.data = data = _DynamicArray()
            for k, element in enumerate(kept):
                copy = Variable(t.type, self.spec, (*self.path, k))
                copy._copy_from(element)
                data.append(copy)
        del data[n:]
        for k in range(len(data), n):
            data.append(Variable(t.type, self.spec, (*self.path, k)))

    def _share(self, elements: _DynamicArray):
        if not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        if data is elements:
            return
        data.holders -= 1
        elements.holders += 1
        self.data = elements

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself. As
        in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the
        target of that pointer, or a copy of that target with its type.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        var = var.deref()
        if copy:
            var = Variable(var.type, data=var)
        self.data = var

    def set(self, value: _T | Sequence | Variable):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those. A container variable is copied
        element by element, and a pointer element as Pascal Script copies it: A pointer to a
        variable of the script points to the same variable, and a pointer that owns its target
        points to a copy of that target. A dynamic array shares the elements of the dynamic array
        that it is assigned. A value of Python code is converted as
        `refinery.lib.inno.pascal.representation` converts it; the interpreter converts between the
        types of the script.
        """
        if self.pointer:
            return self.deref().set(value)
        assigned = value
        if isinstance(value, Variable):
            source = value.deref()
            if self.container and source.container:
                return self._copy_elements(source)
            if source.type == self.type and not source.container:
                self.data = source.data
                return
            assigned = source.get()
        elif isinstance(value, Value):
            assigned = value.value
        elif isinstance(value, Enum):
            assigned = value.value
        if self.container:
            if not isinstance(assigned, (list, tuple)):
                raise TypeError
            if self.type.code is TC.Array:
                self._share(_DynamicArray())
            self.resize(len(assigned))
            for k, v in enumerate(assigned):
                self.data[k].set(v)
        else:
            self.data = pascal.representation(self.type, assigned)

    def _copy_elements(self, source: Variable):
        if (
            not isinstance(elements := source.data, list)
            or not isinstance(self.data, list)
        ):
            raise TypeError
        if self.type.code is TC.Array:
            if isinstance(elements, _DynamicArray):
                return self._share(elements)
            self._share(_DynamicArray())
        self.resize(len(elements))
        for element, copied in zip(self.data, elements):
            element._copy_from(copied)

    def _copy_from(self, copied: Variable):
        if not self.pointer:
            self.set(copied)
        elif isinstance(target := copied.data, Variable):
            self.setptr(target, copy=target.spec is None)
        else:
            raise type_mismatch()

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: list[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'
var path

A tuple of integers that specify the seuqnce of indices required to access it, relative to the base variable given via spec.

Expand source code Browse git
class Variable(VariableBase, Generic[_T]):
    """
    This class represents a global or stack variable in the IFPS runtime.
    """
    data: list[Variable] | _T | None
    """
    The variable's value. This is a list of `refinery.lib.inno.emulator.Variable`s for container
    types, a `refinery.lib.inno.emulator.Variable` for pointer types, and a basic type otherwise.
    """
    path: tuple[int, ...]
    """
    A tuple of integers that specify the seuqnce of indices required to access it, relative to the
    base variable given via `spec`.
    """

    __slots__ = (
        'data',
        'path',
    )

    @property
    def container(self):
        """
        A boolean indicating whether the given variable is a container.
        """
        return self.type.container

    @property
    def pointer(self):
        """
        A boolean indicating whether the given variable is a pointer.
        """
        return self.type.code == TC.Pointer

    def __len__(self):
        return len(self.data)

    def __bool__(self):
        return True

    def __getitem__(self, key: int):
        var = self.deref()
        if var.container:
            return var.at(key).get()
        else:
            return var.data[key]

    def __setitem__(self, key: int, v: _T):
        var = self.deref()
        if var.container:
            var.at(key).set(v)
        else:
            var.data[key] = v

    def at(self, k: int):
        """
        Provides index access for the variable. If the variable is a pointer, it is dereferenced
        before accessing the data.
        """
        return self.deref().data[k]

    def deref(var):
        """
        Dereferences the variable until it is no longer a pointer and returns the result. If the
        variable is not a pointer, this function returns the variable itself.
        """
        while True:
            val = var.data
            if not isinstance(val, Variable):
                return var
            var = val

    def __init__(
        self,
        type: IFPSType,
        spec: VariableSpec | str | None = None,
        path: tuple[int, ...] = (),
        data: _T | list | None = None
    ):
        super().__init__(type, spec)
        self.path = path
        self.setdefault()
        if data is not None:
            self.set(data)

    def setdefault(self):
        """
        Set this variable's data to the default value for its type. This also initializes the
        values of any contained variables recursively.
        """
        spec = self.spec
        path = self.path

        def default(type: IFPSType, *sub_path):
            if isinstance(type, TRecord):
                return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
            if isinstance(type, TStaticArray):
                t = type.type
                return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
            if isinstance(type, TArray):
                return _DynamicArray()
            if sub_path:
                return Variable(type, spec, (*path, *sub_path))
            elif type.code == TC.Pointer:
                return Variable(_VariantType)
            else:
                return pascal.default(type)

        self.data = default(self.type)

    def resize(self, n: int):
        """
        This function is only valid for container type variables. It re-sizes the data list to
        ensure that the container stores exactly `n` sub-variables, or none if `n` is negative. A
        dynamic array that another variable holds too is copied before its length changes.
        """
        t = self.type
        if t.code != TC.Array:
            if t.code not in (TC.StaticArray, TC.Record):
                raise TypeError
            if n == t.size:
                return
            raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
        if not isinstance(t, TArray) or not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        n = max(n, 0)
        if len(data) == n:
            return
        if data.holders > 1:
            data.holders -= 1
            kept = data[:n]
            self.data = data = _DynamicArray()
            for k, element in enumerate(kept):
                copy = Variable(t.type, self.spec, (*self.path, k))
                copy._copy_from(element)
                data.append(copy)
        del data[n:]
        for k in range(len(data), n):
            data.append(Variable(t.type, self.spec, (*self.path, k)))

    def _share(self, elements: _DynamicArray):
        if not isinstance(data := self.data, _DynamicArray):
            raise TypeError
        if data is elements:
            return
        data.holders -= 1
        elements.holders += 1
        self.data = elements

    def setptr(self, var: Variable, copy: bool = False):
        """
        This method is used to point a pointer variable to a target. This is different from calling
        the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
        the pointer and assign to its target; this method sets the value of the pointer itself. As
        in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the
        target of that pointer, or a copy of that target with its type.
        """
        if not self.pointer:
            raise TypeError
        if not isinstance(var, Variable):
            raise TypeError
        var = var.deref()
        if copy:
            var = Variable(var.type, data=var)
        self.data = var

    def set(self, value: _T | Sequence | Variable):
        """
        Assign a new value to the variable. This can either be an immediate value or a variable.
        For container types, it can also be a sequence of those. A container variable is copied
        element by element, and a pointer element as Pascal Script copies it: A pointer to a
        variable of the script points to the same variable, and a pointer that owns its target
        points to a copy of that target. A dynamic array shares the elements of the dynamic array
        that it is assigned. A value of Python code is converted as
        `refinery.lib.inno.pascal.representation` converts it; the interpreter converts between the
        types of the script.
        """
        if self.pointer:
            return self.deref().set(value)
        assigned = value
        if isinstance(value, Variable):
            source = value.deref()
            if self.container and source.container:
                return self._copy_elements(source)
            if source.type == self.type and not source.container:
                self.data = source.data
                return
            assigned = source.get()
        elif isinstance(value, Value):
            assigned = value.value
        elif isinstance(value, Enum):
            assigned = value.value
        if self.container:
            if not isinstance(assigned, (list, tuple)):
                raise TypeError
            if self.type.code is TC.Array:
                self._share(_DynamicArray())
            self.resize(len(assigned))
            for k, v in enumerate(assigned):
                self.data[k].set(v)
        else:
            self.data = pascal.representation(self.type, assigned)

    def _copy_elements(self, source: Variable):
        if (
            not isinstance(elements := source.data, list)
            or not isinstance(self.data, list)
        ):
            raise TypeError
        if self.type.code is TC.Array:
            if isinstance(elements, _DynamicArray):
                return self._share(elements)
            self._share(_DynamicArray())
        self.resize(len(elements))
        for element, copied in zip(self.data, elements):
            element._copy_from(copied)

    def _copy_from(self, copied: Variable):
        if not self.pointer:
            self.set(copied)
        elif isinstance(target := copied.data, Variable):
            self.setptr(target, copy=target.spec is None)
        else:
            raise type_mismatch()

    def get(self) -> _T:
        """
        Return a representation of this variable that consists only of base types. For example, the
        result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
        but a list of their contents.
        """
        if self.pointer:
            return self.deref().get()
        if self.container:
            data: list[Variable] = self.data
            return [v.get() for v in data]
        return self.data

    @property
    def name(self):
        """
        Return the name of the variable as given by its spec.
        """
        if self.spec is None:
            return 'Unbound'
        name = F'{self.spec!s}'
        for k in self.path:
            name = F'{name}[{k}]'
        return name

    def __repr__(self):
        rep = self.name
        if (val := self.data) is None:
            return rep
        if self.type.code is TC.Set:
            val = F'{val:b}'
        elif self.pointer:
            val: Variable
            return F'{rep} -> {val.name}'
        elif isinstance(val, (str, int, float, list)):
            val = repr(self.get())
        else:
            return rep
        return F'{rep} = {val}'

Methods

def at(self, k)

Provides index access for the variable. If the variable is a pointer, it is dereferenced before accessing the data.

Expand source code Browse git
def at(self, k: int):
    """
    Provides index access for the variable. If the variable is a pointer, it is dereferenced
    before accessing the data.
    """
    return self.deref().data[k]
def deref(var)

Dereferences the variable until it is no longer a pointer and returns the result. If the variable is not a pointer, this function returns the variable itself.

Expand source code Browse git
def deref(var):
    """
    Dereferences the variable until it is no longer a pointer and returns the result. If the
    variable is not a pointer, this function returns the variable itself.
    """
    while True:
        val = var.data
        if not isinstance(val, Variable):
            return var
        var = val
def setdefault(self)

Set this variable's data to the default value for its type. This also initializes the values of any contained variables recursively.

Expand source code Browse git
def setdefault(self):
    """
    Set this variable's data to the default value for its type. This also initializes the
    values of any contained variables recursively.
    """
    spec = self.spec
    path = self.path

    def default(type: IFPSType, *sub_path):
        if isinstance(type, TRecord):
            return [Variable(t, spec, (*path, *sub_path, k)) for k, t in enumerate(type.members)]
        if isinstance(type, TStaticArray):
            t = type.type
            return [Variable(t, spec, (*path, *sub_path, k)) for k in range(type.size)]
        if isinstance(type, TArray):
            return _DynamicArray()
        if sub_path:
            return Variable(type, spec, (*path, *sub_path))
        elif type.code == TC.Pointer:
            return Variable(_VariantType)
        else:
            return pascal.default(type)

    self.data = default(self.type)
def resize(self, n)

This function is only valid for container type variables. It re-sizes the data list to ensure that the container stores exactly n sub-variables, or none if n is negative. A dynamic array that another variable holds too is copied before its length changes.

Expand source code Browse git
def resize(self, n: int):
    """
    This function is only valid for container type variables. It re-sizes the data list to
    ensure that the container stores exactly `n` sub-variables, or none if `n` is negative. A
    dynamic array that another variable holds too is copied before its length changes.
    """
    t = self.type
    if t.code != TC.Array:
        if t.code not in (TC.StaticArray, TC.Record):
            raise TypeError
        if n == t.size:
            return
        raise ValueError(F'Attempt to resize {t} of size {t.size} to {n}.')
    if not isinstance(t, TArray) or not isinstance(data := self.data, _DynamicArray):
        raise TypeError
    n = max(n, 0)
    if len(data) == n:
        return
    if data.holders > 1:
        data.holders -= 1
        kept = data[:n]
        self.data = data = _DynamicArray()
        for k, element in enumerate(kept):
            copy = Variable(t.type, self.spec, (*self.path, k))
            copy._copy_from(element)
            data.append(copy)
    del data[n:]
    for k in range(len(data), n):
        data.append(Variable(t.type, self.spec, (*self.path, k)))
def setptr(self, var, copy=False)

This method is used to point a pointer variable to a target. This is different from calling the Variable.set() method as the latter would try to dereference the pointer and assign to its target; this method sets the value of the pointer itself. As in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the target of that pointer, or a copy of that target with its type.

Expand source code Browse git
def setptr(self, var: Variable, copy: bool = False):
    """
    This method is used to point a pointer variable to a target. This is different from calling
    the `refinery.lib.inno.emulator.Variable.set` method as the latter would try to dereference
    the pointer and assign to its target; this method sets the value of the pointer itself. As
    in Pascal Script, a pointer never points to a pointer: Set to a pointer, it receives the
    target of that pointer, or a copy of that target with its type.
    """
    if not self.pointer:
        raise TypeError
    if not isinstance(var, Variable):
        raise TypeError
    var = var.deref()
    if copy:
        var = Variable(var.type, data=var)
    self.data = var
def set(self, value)

Assign a new value to the variable. This can either be an immediate value or a variable. For container types, it can also be a sequence of those. A container variable is copied element by element, and a pointer element as Pascal Script copies it: A pointer to a variable of the script points to the same variable, and a pointer that owns its target points to a copy of that target. A dynamic array shares the elements of the dynamic array that it is assigned. A value of Python code is converted as representation() converts it; the interpreter converts between the types of the script.

Expand source code Browse git
def set(self, value: _T | Sequence | Variable):
    """
    Assign a new value to the variable. This can either be an immediate value or a variable.
    For container types, it can also be a sequence of those. A container variable is copied
    element by element, and a pointer element as Pascal Script copies it: A pointer to a
    variable of the script points to the same variable, and a pointer that owns its target
    points to a copy of that target. A dynamic array shares the elements of the dynamic array
    that it is assigned. A value of Python code is converted as
    `refinery.lib.inno.pascal.representation` converts it; the interpreter converts between the
    types of the script.
    """
    if self.pointer:
        return self.deref().set(value)
    assigned = value
    if isinstance(value, Variable):
        source = value.deref()
        if self.container and source.container:
            return self._copy_elements(source)
        if source.type == self.type and not source.container:
            self.data = source.data
            return
        assigned = source.get()
    elif isinstance(value, Value):
        assigned = value.value
    elif isinstance(value, Enum):
        assigned = value.value
    if self.container:
        if not isinstance(assigned, (list, tuple)):
            raise TypeError
        if self.type.code is TC.Array:
            self._share(_DynamicArray())
        self.resize(len(assigned))
        for k, v in enumerate(assigned):
            self.data[k].set(v)
    else:
        self.data = pascal.representation(self.type, assigned)
def get(self)

Return a representation of this variable that consists only of base types. For example, the result for a container type will not be a list of Variables, but a list of their contents.

Expand source code Browse git
def get(self) -> _T:
    """
    Return a representation of this variable that consists only of base types. For example, the
    result for a container type will not be a list of `refinery.lib.inno.emulator.Variable`s,
    but a list of their contents.
    """
    if self.pointer:
        return self.deref().get()
    if self.container:
        data: list[Variable] = self.data
        return [v.get() for v in data]
    return self.data

Inherited members

class ExceptionHandler (finally_one, catch_error, finally_two, handler_end, function, frame, sp, depth, exception=None, exit_point=None)

An exception handler of the IFPS runtime: one try statement whose try block runs or whose blocks have not all ended. A block that an error enters can no longer be entered by an error. The second finally block that the end of the try block enters can still be entered once by an error, as in Pascal Script.

Expand source code Browse git
@dataclass
class ExceptionHandler:
    """
    An exception handler of the IFPS runtime: one try statement whose try block runs or whose
    blocks have not all ended. A block that an error enters can no longer be entered by an error.
    The second finally block that the end of the try block enters can still be entered once by an
    error, as in Pascal Script.
    """
    finally_one: int | None
    """
    Code offset of the first finally block, while it can be entered.
    """
    catch_error: int | None
    """
    Code offset of the except block, while it can be entered.
    """
    finally_two: int | None
    """
    Code offset of the second finally block, while it can be entered.
    """
    handler_end: int
    """
    Code offset of the first instruction after the try statement.
    """
    function: Function
    """
    The function that contains the try statement.
    """
    frame: int
    """
    The number of call frames below the function that contains the try statement.
    """
    sp: int
    """
    The stack base of the function that contains the try statement.
    """
    depth: int
    """
    The size of the data stack when the handler was installed. The stack is cut back to this size
    when an error enters one of the blocks of the handler.
    """
    exception: IFPSException | None = None
    """
    The error that entered the except block.
    """
    exit_point: int | None = None
    """
    From Inno Setup 7 on, the offset of the return that entered a finally block of this handler.
    """

    def resume(self, ip: int) -> _CallState:
        """
        The position at the given offset of the function that contains the try statement.
        """
        return _CallState(self.function, ip, self.sp)

Instance variables

var finally_one

Code offset of the first finally block, while it can be entered.

var catch_error

Code offset of the except block, while it can be entered.

var finally_two

Code offset of the second finally block, while it can be entered.

var handler_end

Code offset of the first instruction after the try statement.

var function

The function that contains the try statement.

var frame

The number of call frames below the function that contains the try statement.

var sp

The stack base of the function that contains the try statement.

var depth

The size of the data stack when the handler was installed. The stack is cut back to this size when an error enters one of the blocks of the handler.

var exception

The error that entered the except block.

var exit_point

From Inno Setup 7 on, the offset of the return that entered a finally block of this handler.

Methods

def resume(self, ip)

The position at the given offset of the function that contains the try statement.

Expand source code Browse git
def resume(self, ip: int) -> _CallState:
    """
    The position at the given offset of the function that contains the try statement.
    """
    return _CallState(self.function, ip, self.sp)
class IFPSEmulatedFunction (call, spec, void=False, caller=False)

Represents an emulated external symbol.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)

Ancestors

  • builtins.tuple

Instance variables

var call

The actual callable function that implements the symbol.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)
var spec

A list of boolean values, one for each parameter of the function. Each boolean indicates whether the parameter at that index is passed by reference.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)
var void

Indicates whether the handler implements a procedure rather than a function in the IFPS runtime.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)
var caller

Indicates whether the handler receives the run of the script that calls it as its keyword argument caller. Inno Setup passes its functions the runtime that calls them.

Expand source code Browse git
class IFPSEmulatedFunction(NamedTuple):
    """
    Represents an emulated external symbol.
    """
    call: Callable
    """
    The actual callable function that implements the symbol.
    """
    spec: list[bool]
    """
    A list of boolean values, one for each parameter of the function. Each boolean indicates
    whether the parameter at that index is passed by reference.
    """
    void: bool = False
    """
    Indicates whether the handler implements a procedure rather than a function in the IFPS
    runtime.
    """
    caller: bool = False
    """
    Indicates whether the handler receives the run of the script that calls it as its keyword
    argument `caller`. Inno Setup passes its functions the runtime that calls them.
    """

    @property
    def argc(self):
        """
        The argument count for this handler.
        """
        return len(self.spec)
var argc

The argument count for this handler.

Expand source code Browse git
@property
def argc(self):
    """
    The argument count for this handler.
    """
    return len(self.spec)
class IFPSEmulatorConfig (x64=True, admin=True, windows_os_version=(10, 0, 10240), windows_sp_version=(2, 0), throw_abort=False, log_calls=False, log_passwords=True, log_mutexes=True, log_opcodes=False, wizard_silent=True, max_opcodes=0, max_seconds=10, start_time=<factory>, milliseconds_per_instruction=0.0001, sleep_scale=0.0, max_data_stack=1000000, max_array_length=1048576, max_string_length=16777216, max_call_stack=4096, environment=<factory>, user_name='Frank', temp_path='', host_name='Frank-PC', inno_name='ThisInstall', language='en', executable='C:\\Install.exe', install_to='I:\\', allow_process_listing=False, lcid=1033, setup_version=None)

The configuration for IFPSEmulators.

Expand source code Browse git
@dataclass
class IFPSEmulatorConfig:
    """
    The configuration for `refinery.lib.inno.emulator.IFPSEmulator`s.
    """
    x64: bool = True
    admin: bool = True
    windows_os_version: tuple[int, int, int] = (10, 0, 10240)
    windows_sp_version: tuple[int, int] = (2, 0)
    throw_abort: bool = False
    log_calls: bool = False
    log_passwords: bool = True
    log_mutexes: bool = True
    log_opcodes: bool = False
    wizard_silent: bool = True
    max_opcodes: int = 0
    max_seconds: int = 10
    start_time: datetime = field(default_factory=_now_in_whole_seconds)
    milliseconds_per_instruction: float = 0.0001
    sleep_scale: float = 0.0
    max_data_stack: int = 1_000_000
    max_array_length: int = 0x100000
    max_string_length: int = 0x1000000
    max_call_stack: int = 4096
    environment: dict[str, str] = field(default_factory=dict)
    user_name: str = 'Frank'
    temp_path: str = ''
    host_name: str = 'Frank-PC'
    inno_name: str = 'ThisInstall'
    language: str = 'en'
    executable: str = 'C:\\Install.exe'
    install_to: str = 'I:\\'
    allow_process_listing: bool = False
    processes = {
        1001: 'explorer.exe',
        1002: None, # represents self
        1023: 'svchost.exe',
        1024: 'svchost.exe',
        1060: 'rundll32.exe',
        1061: 'rundll32.exe',
        2223: 'csrss.exe',
        1200: 'winlogon.exe',
        1300: 'wininit.exe',
        1301: 'dllhost.exe',
        1305: 'notepad.exe',
        5305: 'chrome.exe',
    }
    lcid: int = 0x0409
    setup_version: tuple[int, int, int] | None = None

Class variables

var processes

The type of the None singleton.

Instance variables

var start_time

The type of the None singleton.

var environment

The type of the None singleton.

var x64

The type of the None singleton.

var admin

The type of the None singleton.

var windows_os_version

The type of the None singleton.

var windows_sp_version

The type of the None singleton.

var throw_abort

The type of the None singleton.

var log_calls

The type of the None singleton.

var log_passwords

The type of the None singleton.

var log_mutexes

The type of the None singleton.

var log_opcodes

The type of the None singleton.

var wizard_silent

The type of the None singleton.

var max_opcodes

The type of the None singleton.

var max_seconds

The type of the None singleton.

var milliseconds_per_instruction

The type of the None singleton.

var sleep_scale

The type of the None singleton.

var max_data_stack

The type of the None singleton.

var max_array_length

The type of the None singleton.

var max_string_length

The type of the None singleton.

var max_call_stack

The type of the None singleton.

var user_name

The type of the None singleton.

var temp_path

The type of the None singleton.

var host_name

The type of the None singleton.

var inno_name

The type of the None singleton.

var language

The type of the None singleton.

var executable

The type of the None singleton.

var install_to

The type of the None singleton.

var allow_process_listing

The type of the None singleton.

var lcid

The type of the None singleton.

var setup_version

The type of the None singleton.

class TSplitType (*args, **kwds)

An IFPS enumeration that classifies different strategies for splitting strings.

Expand source code Browse git
class TSplitType(int, Enum):
    """
    An IFPS enumeration that classifies different strategies for splitting strings.
    """
    stAll = 0
    stExcludeEmpty = auto()
    stExcludeLastEmpty = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var stAll

The type of the None singleton.

var stExcludeEmpty

The type of the None singleton.

var stExcludeLastEmpty

The type of the None singleton.

class TUninstallStep (*args, **kwds)

An IFPS enumeration that classifies uninstaller steps.

Expand source code Browse git
class TUninstallStep(int, Enum):
    """
    An IFPS enumeration that classifies uninstaller steps.
    """
    usAppMutexCheck = 0
    usUninstall = auto()
    usPostUninstall = auto()
    usDone = auto()

Ancestors

  • builtins.int
  • enum.Enum

Class variables

var usAppMutexCheck

The type of the None singleton.

var usUninstall

The type of the None singleton.

var usPostUninstall

The type of the None singleton.

var usDone

The type of the None singleton.

class EventCall (call)

This class is a wrapper for generator functions that can also capture their return value. It is used for IFPSEmulator.emulate_function().

Expand source code Browse git
class EventCall(Generic[_Y, _T]):
    """
    This class is a wrapper for generator functions that can also capture their return value.
    It is used for `refinery.lib.inno.emulator.IFPSEmulator.emulate_function`.
    """
    def __init__(self, call: Generator[_Y, Any, _T]):
        self._call = call
        self._done = False
        self._buffer: list[_Y] = []
        self._value = None

    @classmethod
    def Wrap(cls, method: Callable[_P, Generator[_Y, Any, _T]]) -> Callable[_P, EventCall[_Y, _T]]:
        """
        Used for decorating generator functions.
        """
        @wraps(method)
        def wrapped(*args, **kwargs):
            return cls(method(*args, **kwargs))
        return wrapped

    @property
    def value(self) -> _T | None:
        """
        The return value of the wrapped function.
        """
        if not self._done:
            self._buffer = list(self)
        return self._value

    def __iter__(self):
        if self._done:
            yield from self._buffer
            assert self._value is not None
            self._buffer.clear()
        else:
            self._value = yield from self._call
            self._done = True
        return self._value

Ancestors

  • typing.Generic

Static methods

def Wrap(method)

Used for decorating generator functions.

Instance variables

var value

The return value of the wrapped function.

Expand source code Browse git
@property
def value(self) -> _T | None:
    """
    The return value of the wrapped function.
    """
    if not self._done:
        self._buffer = list(self)
    return self._value
class IFPSEmulator (archive, options=None, **more)

The core IFPS emulator.

Expand source code Browse git
class IFPSEmulator:
    """
    The core IFPS emulator.
    """
    def __init__(
        self,
        archive: InnoArchive | IFPSFile,
        options: IFPSEmulatorConfig | None = None,
        **more
    ):
        with datapath('winapi.json').open('rb') as stream:
            self.winapi: dict[str, list[str]] = json.loads(stream.read())
            base = 0x70000000
            self.winapi_module_to_addr = _ml = {name: base + k * 0x100000 for k, name in enumerate(self.winapi)}
            self.winapi_addr_to_module = {addr: module for module, addr in _ml.items()}
            self.winapi_symbol_to_addr = _sl = {
                symbol: _ml[module] + k * 0x1000 for module, symbols in self.winapi.items() for k, symbol in enumerate(symbols)}
            self.winapi_addr_to_symbol = {addr: module for module, addr in _sl.items()}

        self.config = config = options or IFPSEmulatorConfig(**more)
        if isinstance(archive, InnoArchive):
            self.setup_info = archive.setup_info
            ifps = archive.ifps
            if ifps is None:
                raise ValueError('The input archive does not contain a script.')
            self.setup = HostSetup.of_installer(archive, config)
            source = 'The installer'
        else:
            self.setup_info = None
            ifps = archive
            self.setup = HostSetup.of_bare_script(ifps.platform, config)
            source = 'The configured Setup version'
        _check_agreement(self.setup, ifps, config, source)
        self.machine_folders = machine_folders(self.setup, config)
        self.launch_folder = self.setup.path_func.extract_dir(config.executable)
        self.dialect = ifps.dialect
        self.ifps = ifps
        self._form_variables = [
            (index, name) for index, variable in enumerate(ifps.globals)
            if isinstance(variable.spec, str) and (name := variable.spec.upper()) in _FORM_VARIABLES
        ]
        self._stopwatch: _Stopwatch | None = None
        self._variables_held: dict[int, int] = {}
        self._globals: list[Variable] | None = None
        self.mutexes: set[str] = set()
        self.symbols: dict[str, Function] = CaseInsensitiveDict()
        self.reset()
        for pfn in ifps.functions:
            self.symbols[pfn.name] = pfn

    def __repr__(self):
        return self.__class__.__name__

    def reset(self, walked: bool = False):
        """
        Reset the emulator timing, the state of the Delphi run-time library, and mutexes. The
        script starts over with new global variables that hold their default values, in a new Setup
        process with a new wizard form, which Setup walks through when `walked` is set.
        """
        self.seconds_slept = 0.0
        self.clock = 0
        self.runtime = self.setup.runtime(numbers.FormatSettings.of_locale(self.config.lcid))
        self.machine = pascal.Machine(self.dialect, self.runtime)
        self.mutexes.clear()
        self.wizard_form = wizard = WizardForm(
            StandIns.of(self.config),
            walked=walked,
            entries_known=self.setup_info is not None,
            uninstallable=self.setup.uninstallable.answer is not False,
        )
        self.constants = SetupConstants(self.setup, self.config, wizard, self.machine_folders)
        self.current_folder = CurrentFolder(self.launch_folder)
        self.installed = InstalledPaths(self.current_folder)
        self.current_file: CurrentFile | None = None
        self._globals = None
        self._win32api_last_error = 0
        self._win32api_sorted_process_ids = sorted(self.config.processes)
        return self

    @property
    def globals(self) -> list[Variable]:
        """
        The global variables of the script.
        """
        if (script_globals := self._globals) is None:
            for v in self.ifps.globals:
                self._reserve_variables(v.type)
            script_globals = self._globals = [Variable(v.type, v.spec) for v in self.ifps.globals]
        return script_globals

    def _refresh_form_variables(self):
        """
        Sets the variables in which a script of Inno Setup 6 or later finds Setup's forms, as Setup
        does before each call into the script: each is empty until Setup created its form, and
        Setup never shows the form of the uninstaller.
        """
        if not (slots := self._form_variables):
            return
        form = self.wizard_form
        values = {
            'WIZARDFORM': form if form.created else None,
            'MAINFORM': form.main,
            'UNINSTALLPROGRESSFORM': None,
        }
        script_globals = self.globals
        for index, name in slots:
            script_globals[index].set(values[name])

    def _reserve_variables(self, type: IFPSType, count: int = 1):
        """
        Checks before they are built that the given number of variables of the given type, and the
        variables that they hold, stay within the array length limit.
        """
        if (limit := self.config.max_array_length) <= 0:
            return
        if not type.code.container:
            held = 1
        elif (held := self._variables_held.get(id(type))) is None:
            held = self._variables_held[id(type)] = _variables_held(type)
        if limit < (n := count * held):
            raise EmulatorMaxArray(F'Requested {n} variables of type {type}, beyond the limit {limit}.')

    def _reserve_text(self, length: int, wide: bool = True):
        """
        Checks a string of the given length before it is built: Delphi cannot compute the size of
        a UTF-16 string of more than `_LONGEST_WIDE_STRING` characters and raises an integer
        overflow, and a string beyond the configured limit ends the emulation.
        """
        if wide and length > _LONGEST_WIDE_STRING:
            raise pascal.delphi_error(EIntOverflow())
        if 0 < (limit := self.config.max_string_length) < length:
            raise EmulatorMaxString(F'Requested string length {length} exceeds the limit {limit}.')

    def _reserve_results(self, value: Any, arguments: list, result: Variable | None):
        """
        Checks what an emulated function returned before the script receives it: the text of its
        result and of the strings that it wrote into its arguments, and the elements of a list that
        becomes an array.
        """
        if isinstance(value, str):
            self._reserve_text(len(value))
        for var in arguments:
            if isinstance(var, Variable) and isinstance(text := var.deref().data, str):
                self._reserve_text(len(text))
        if (
            result is not None
            and isinstance(value, (list, tuple))
            and isinstance(array := result.deref().type, TArray)
        ):
            self._reserve_variables(array.type, len(value))

    def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
        """
        The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
        symbol is unimplemented. Child classes can override this function to handle the missing
        symbol differently; it receives the arguments of the call and the variable for its result,
        if the symbol is a function.
        """
        raise NeedSymbol(function.name)

    @EventCall.Wrap
    def emulate_function(self, function: Function, *args):
        """
        Emulate a function call to the given function, passing the given arguments. The method
        returns the return value of the emulated function call if it is not a procedure.
        """
        return (yield from self._timed(self._execute(_ScriptRun(self.dialect), function, args)))

    def _timed(self, emulation: Generator[Any, Any, _T], max_seconds: float | None = None) -> Generator[Any, Any, _T]:
        """
        Runs one emulation under a time limit: the given number of seconds or else the number in
        the configuration, no limit when it is not positive. Only the time that the emulator works
        counts, and the runs that the emulation nests share the limit. An emulation that starts while
        another one works and is given no limit of its own runs under the limit of that one.
        """
        if max_seconds is None and self._stopwatch is not None:
            return (yield from emulation)
        watch = _Stopwatch(self.config.max_seconds if max_seconds is None else max_seconds)
        try:
            while True:
                outer, self._stopwatch = self._stopwatch, watch
                watch.resume()
                try:
                    event = next(emulation)
                except StopIteration as stop:
                    return stop.value
                finally:
                    watch.pause()
                    self._stopwatch = outer
                yield event
        finally:
            emulation.close()

    def _execute(
        self,
        run: _ScriptRun,
        function: Function,
        args: Sequence,
        types: Sequence[IFPSType] | None = None,
        result_type: IFPSType | None = None,
    ):
        """
        Runs the function with the given values. The caller that creates the variables it passes,
        as Setup does, gives their types and the type of the result for a declaration that does
        not record them.
        """
        decl = function.decl
        if decl is None:
            raise NotImplementedError(F'Do not know how to call {function!s}.')
        self._refresh_form_variables()
        if (n := len(decl.parameters)) != (m := len(args)):
            raise ValueError(
                F'Function {function!s} expects {n} arguments, only {m} were given.')
        stack = run.stack
        for index, (argument, parameter) in enumerate(zip(args, decl.parameters), 1):
            kind = parameter.type
            if kind is None and types is not None:
                kind = types[index - 1]
            variable = Variable(kind, VariableSpec(index, VariableType.Local))
            variable.set(argument)
            stack.append(variable)
        stack.reverse()
        if not decl.void:
            result = Variable(decl.return_type or result_type, VariableSpec(0, VariableType.Argument))
            stack.append(result)
        yield from self._interpret(run, function)
        if not decl.void:
            return result.get()

    def _interpret(self, run: _ScriptRun, function: Function):
        """
        Emulates the given run of the script from the start of the given function.
        """

        def resolve(spec: VariableSpec) -> Variable:
            t, i = spec.type, spec.index
            if t is VariableType.Argument:
                if function.decl.void:
                    i -= 1
                return stack[sp - i]
            if t is VariableType.Global:
                return script_globals[i]
            if t is VariableType.Local:
                return stack[sp + i]
            raise TypeError

        def getvar(op: VariableSpec | Operand) -> Variable:
            if not isinstance(op, Operand):
                return resolve(op)
            if (spec := op.variable) is None:
                raise TypeError('Attempting to retrieve variable for an immediate operand.')
            var = resolve(spec)
            if isinstance(k := op.index, VariableSpec):
                k = resolve(k).get()
            if k is None:
                return var
            if not 0 <= k < len(var := var.deref()):
                raise _index_error(var, k, dialect)
            return var.at(k)

        def getval(op: Operand):
            if op.immediate:
                return op.value.value
            return getvar(op).get()

        def gettyped(op: Operand) -> tuple[IFPSType, Any]:
            if op.immediate and (constant := op.value) is not None:
                return constant.type, constant.value
            var = getvar(op).deref()
            return var.type, var.get()

        frames = run.frames
        depth = run.depth
        machine = self.machine
        dialect = machine.dialect
        script_globals = self.globals
        watch = self._stopwatch
        stack = run.stack
        _cfg_max_call_stack = self.config.max_call_stack
        _cfg_max_data_stack = self.config.max_data_stack
        _cfg_max_opcodes = self.config.max_opcodes
        _cfg_log_opcodes = self.config.log_opcodes

        ip: int = 0
        sp: int = len(stack) - 1
        raised: IFPSException | None = None
        failed_call = False
        unwind = False
        jump = False

        while True:
            if raised is not None:
                run.throw(raised)
                failed_call = raised.failed_call
                raised = None
                unwind = True
            if unwind:
                unwind = False
                function, ip, sp = run.unwind()
                if failed_call and run.in_flight.error is None:
                    raised = IFPSException(kind=PSError.CouldNotCallProc)
                    continue
                failed_call = False
            if 0 < _cfg_max_call_stack < depth + len(frames):
                raise EmulatorMaxCalls

            if function.body is None:
                namespace = ''

                if decl := function.decl:
                    namespace = decl.classname or decl.module or ''

                name = function.name
                registry: dict[str, IFPSEmulatedFunction] = self.external_symbols.get(namespace, {})
                handler = registry.get(name)

                if decl:
                    void = decl.void
                    argc = decl.argc + decl.method
                elif handler:
                    void = handler.void
                    argc = handler.argc
                else:
                    void = True
                    argc = 0

                try:
                    rpos = 0 if void else 1
                    args: list = [stack[~k] for k in range(rpos, argc + rpos)]
                except IndexError:
                    raise EmulatorException(
                        F'Cannot call {function!s}; {argc} arguments + {rpos} return values expected,'
                        F' but stack size is only {len(stack)}.')

                if self.config.log_calls:
                    yield NewFunctionCall(str(function), tuple(a.get() for a in args))

                if namespace in _FORM_CLASSES and decl is not None and args and args[0].get() is None:
                    kind = PSError.NullPointerException if decl.method else PSError.CouldNotCallProc
                    raised = IFPSException(kind=kind, called=name, failed_call=True)
                elif handler is None:
                    self.unimplemented(function, args, None if void else stack[-1])
                elif handler.argc != argc or handler.void and not void:
                    failure = _incompatible_call(handler, argc, void)
                    raised = IFPSException(failure, called=name)
                    yield NewEmulationError(name, failure)
                else:
                    for k, (var, byref) in enumerate(zip(args, handler.spec)):
                        if not byref:
                            args[k] = var.get()
                    args.insert(0, self)
                    keywords = {'caller': run} if handler.caller else {}
                    failure = None
                    try:
                        return_value = handler.call(*args, **keywords)
                        if inspect.isgenerator(return_value):
                            return_value = yield from return_value
                        self._reserve_results(return_value, args, None if void else stack[-1])
                    except EmulatorException:
                        raise
                    except RecursionError as RE:
                        raise EmulatorMaxCalls(F'Calling {name} exceeded the recursion limit of Python.') from RE
                    except MemoryError as ME:
                        raise EmulatorMemoryError(F'Calling {name} exhausted the memory of Python.') from ME
                    except Undefined as UE:
                        raise EmulatorUndefined(F'Calling {name}: {UE}') from UE
                    except IFPSException as error:
                        raised = IFPSException(error.message, error.kind, name, error.failed_call)
                    except DelphiException as error:
                        raised = IFPSException(self.setup.error_text(error), called=name)
                    except Exception as error:
                        failure = exception_to_string(error)
                        raised = IFPSException(failure, called=name)
                    else:
                        if not void:
                            stack[-1].set(return_value)
                    if failure is not None:
                        yield NewEmulationError(name, failure)
                if not frames:
                    if raised is None:
                        return
                    continue
                function, ip, sp = frames.pop()
                continue

            while insn := function.code.get(ip, None):
                if watch is not None and watch.expired():
                    raise EmulatorTimeout
                if 0 < _cfg_max_opcodes < self.clock:
                    raise EmulatorExecutionLimit
                if 0 < _cfg_max_data_stack < len(stack):
                    raise EmulatorMaxStack
                if _cfg_log_opcodes:
                    yield NewInstruction(function, insn, ip)
                try:
                    opc = insn.opcode
                    ip += insn.size
                    self.clock += 1

                    if opc == Op.Nop:
                        continue
                    elif opc == Op.Assign:
                        dst = getvar(insn.op(0)).deref()
                        src = insn.op(1)
                        if src.immediate:
                            source = src.value
                            st, sv = source.type, source.value
                        else:
                            source = getvar(src).deref()
                            st, sv = source.type, source.data
                        if dst.container or st.code is TC.Pointer:
                            if not pascal.assignable(dst.type, st, machine):
                                raise type_mismatch()
                            dst.set(source)
                        else:
                            if isinstance(source, Variable) and source.container:
                                sv = source.get()
                            dst.data = pascal.store(dst.type, st, sv, machine)
                    elif opc == Op.Calculate:
                        if not isinstance((aop := insn.operator), AOp):
                            raise TypeError(F'Operator for {opc} was of type {aop.__class__.__name__} rather than AOp.')
                        target = getvar(insn.op(0)).deref()
                        st, sv = gettyped(insn.op(1))
                        result = pascal.calculate(aop, target.type, target.data, st, sv, machine)
                        if isinstance(result, str):
                            self._reserve_text(len(result))
                        elif isinstance(result, Variant) and isinstance(result.value, str):
                            self._reserve_text(len(result.value))
                        target.data = result
                    elif opc == Op.Push:
                        # TODO: I do not actually know how this works
                        stack.append(getval(insn.op(0)))
                    elif opc == Op.PushVar:
                        stack.append(getvar(insn.op(0)))
                    elif opc == Op.Pop:
                        stack.pop()
                    elif opc == Op.Call:
                        frames.append(_CallState(function, ip, sp))
                        function = insn.callee()
                        ip = 0
                        sp = len(stack) - 1
                        break
                    elif opc == Op.Jump:
                        ip = insn.operands[0]
                    elif opc == Op.JumpTrue:
                        if pascal.truth(*gettyped(insn.op(1))):
                            ip = insn.operands[0]
                    elif opc == Op.JumpFalse:
                        if not pascal.truth(*gettyped(insn.op(1))):
                            ip = insn.operands[0]
                    elif opc == Op.Ret:
                        if (position := run.exit(function, insn.offset, sp)) is not None:
                            function, ip, sp = position
                            continue
                        del stack[sp + 1:]
                        if not frames:
                            if run.in_flight.error is None:
                                return
                            unwind = True
                            break
                        function, ip, sp = frames.pop()
                        break
                    elif opc == Op.StackType:
                        raise OpCodeNotImplemented(str(opc))
                    elif opc == Op.PushType:
                        self._reserve_variables(insn.operands[0])
                        stack.append(Variable(
                            insn.operands[0],
                            VariableSpec(len(stack) - sp, VariableType.Local)
                        ))
                    elif opc == Op.Compare:
                        if not isinstance((cop := insn.operator), COp):
                            raise TypeError(F'Operator for {opc} was of type {cop.__class__.__name__} rather than COp.')
                        lt, lv = gettyped(insn.op(1))
                        rt, rv = gettyped(insn.op(2))
                        truth = pascal.compare(cop, lt, lv, rt, rv, machine)
                        into = getvar(insn.op(0)).deref()
                        into.data = pascal.boolean(into.type, truth, machine)
                    elif opc == Op.CallVar:
                        call = getval(insn.op(0))
                        if isinstance(call, int):
                            call = self.ifps.functions[call]
                        if isinstance(call, Function):
                            frames.append(_CallState(function, ip, sp))
                            function = call
                            ip = 0
                            sp = len(stack) - 1
                            break
                    elif opc == Op.SetPtr:
                        getvar(insn.op(0)).setptr(getvar(insn.op(1)))
                    elif opc == Op.SetPtrToCopy:
                        dst = getvar(insn.op(0))
                        src = insn.op(1)
                        if (constant := src.value) is None:
                            dst.setptr(getvar(src), copy=True)
                        else:
                            dst.setptr(Variable(constant.type, data=constant.value))
                    elif opc == Op.BooleanNot:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.logical_not(var.type, var.data, machine.runtime)
                    elif opc == Op.IntegerNot:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.invert(var.type, var.data, machine.runtime)
                    elif opc == Op.Neg:
                        var = getvar(insn.op(0)).deref()
                        var.data = pascal.negate(var.type, var.data, machine.runtime)
                    elif opc == Op.SetFlag:
                        condition, negated = insn.operands
                        jump = pascal.truth(*gettyped(condition)) ^ negated
                    elif opc == Op.JumpFlag:
                        if jump:
                            ip = insn.operands[0]
                    elif opc == Op.PushEH:
                        run.install(insn.operands, function, sp)
                    elif opc == Op.PopEH:
                        if (position := run.end_block(EHType(insn.operands[0]))) is None:
                            unwind = True
                            break
                        function, ip, sp = position
                    elif opc == Op.Inc:
                        counter = getvar(insn.op(0)).deref()
                        counter.data = pascal.step(counter.type, counter.data, True, machine)
                    elif opc == Op.Dec:
                        counter = getvar(insn.op(0)).deref()
                        counter.data = pascal.step(counter.type, counter.data, False, machine)
                    elif opc == Op.JumpPop1:
                        stack.pop()
                        ip = insn.operands[0]
                    elif opc == Op.JumpPop2:
                        stack.pop()
                        stack.pop()
                        ip = insn.operands[0]
                    else:
                        raise RuntimeError(F'Function contains invalid opcode at 0x{ip:X}.')
                except IFPSException as EE:
                    raised = copy_exception(EE)
                    break
                except EmulatorException:
                    raise
                except Exception as RE:
                    raise _failed_instruction(RE, function, insn, self.clock) from RE
            else:
                raise EmulatorException(F'In {function.symbol}, no instruction starts at 0x{ip:X}.')

    external_symbols: ClassVar[
        dict[str,                        # class name for methods or empty string for functions
        dict[str, IFPSEmulatedFunction]] # method or function name to emulation info
    ] = CaseInsensitiveDict()

    def external(*args, __reg: dict = external_symbols, **kwargs):
        def decorator(pfn):
            signature = inspect.signature(pfn)
            name: str = kwargs.get('name', pfn.__name__)
            csep: str = '.'
            if csep not in name:
                csep = '__'
            classname, _, name = name.rpartition(csep)
            if (registry := __reg.get(classname)) is None:
                registry = __reg[classname] = CaseInsensitiveDict()

            docs = F'{classname}::{name}' if classname else name
            docs = F'An emulated handler for the external symbol {docs}.'
            pfn.__doc__ = docs

            void = kwargs.get('void', signature.return_annotation == signature.empty)
            caller = kwargs.get('caller', False)
            parameters: list[bool] = []
            specs = iter(signature.parameters.values())
            next(specs) # skip self
            for spec in specs:
                if caller and spec.name == 'caller':
                    continue
                try:
                    hint = eval(spec.annotation, globals(), locals())
                except Exception as E:
                    raise RuntimeError(F'Invalid signature: {signature}') from E
                if not isinstance(hint, type):
                    hint = get_origin(hint)
                var = isinstance(hint, type) and issubclass(hint, Variable)
                parameters.append(var)
            registry[name] = e = IFPSEmulatedFunction(pfn, parameters, void, caller)
            aliases = kwargs.get('alias', [])
            if isinstance(aliases, str):
                aliases = [aliases]
            for name in aliases:
                registry[name] = e
            return pfn
        return decorator(args[0]) if args else decorator

    @external()
    def TInputDirWizardPage__GetValues(self, this: object, k: int) -> str:
        return F'$InputDir{k}'

    @external()
    def TInputFileWizardPage__GetValues(self, this: object, k: int) -> str:
        return F'$InputFile{k}'

    @external()
    def TWizardForm__GetPasswordEdit(self, this: object) -> object:
        return self.wizard_form.password_box

    @external()
    def TWizardForm__GetDirEdit(self, this: object) -> object:
        return self.wizard_form.dir_edit

    @external()
    def TWizardForm__GetGroupEdit(self, this: object) -> object:
        return self.wizard_form.group_edit

    @external()
    def TWizardForm__GetUserInfoNameEdit(self, this: object) -> object:
        return self.wizard_form.user_info_name_edit

    @external()
    def TWizardForm__GetUserInfoOrgEdit(self, this: object) -> object:
        return self.wizard_form.user_info_org_edit

    @external()
    def TWizardForm__GetUserInfoSerialEdit(self, this: object) -> object:
        return self.wizard_form.user_info_serial_edit

    @external()
    def TWizardForm__GetPrevAppDir(self, this: object) -> str:
        return ''

    def text_changed(self, box: TextBox) -> Generator[Any, Any, None]:
        """
        Runs the handler of a text box whose text changed, as the box's `OnChange` runs: a function
        of the script, or Setup's own handler. An error reaches the code that changed the text.
        """
        handler = box.on_change
        if isinstance(handler, Function):
            yield from self.call_from_setup(handler, (box,), _NOTIFY_CALL)
        elif handler is SetupHandler.UserInfoEditChange:
            yield from user_info_edit_change(self, box)

    def _box_text(self, this: object) -> str:
        return this.text if isinstance(this, TextBox) else ''

    def _set_box_text(self, this: object, value: str) -> Generator[Any, Any, None]:
        if isinstance(this, TextBox) and this.set_text(value):
            yield from self.text_changed(this)

    def _replace_box_selection(self, this: object, value: str) -> Generator[Any, Any, None]:
        if isinstance(this, TextBox):
            this.replace_selection(value)
            yield from self.text_changed(this)

    @external()
    def TEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TCustomEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TPasswordEdit__GetText(self, this: object) -> str:
        return self._box_text(this)

    @external()
    def TEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)

    @external()
    def TCustomEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)

    @external()
    def TPasswordEdit__SetText(self, this: object, value: str):
        yield from self._set_box_text(this, value)
        if value:
            yield NewPassword(value)

    @external()
    def TCustomEdit__Clear(self, this: object):
        if isinstance(this, TextBox):
            this.clear()
            yield from self.text_changed(this)

    @external()
    def TCustomEdit__ClearSelection(self, this: object):
        if isinstance(this, TextBox) and this.clear_selection():
            yield from self.text_changed(this)

    @external()
    def TCustomEdit__GetSelText(self, this: object) -> str:
        return this.selected_text if isinstance(this, TextBox) else ''

    @external()
    def TCustomEdit__SetSelText(self, this: object, value: str):
        yield from self._replace_box_selection(this, value)

    @external()
    def TCustomEdit__SelectAll(self, this: object):
        if isinstance(this, TextBox):
            this.select(0, -1)

    @external()
    def TCustomEdit__GetSelStart(self, this: object) -> int:
        return this.selection_start if isinstance(this, TextBox) else 0

    @external()
    def TCustomEdit__SetSelStart(self, this: object, value: int):
        if isinstance(this, TextBox):
            this.select(value, value)

    @external()
    def TCustomEdit__GetSelLength(self, this: object) -> int:
        return this.selection_length if isinstance(this, TextBox) else 0

    @external()
    def TCustomEdit__SetSelLength(self, this: object, value: int):
        if isinstance(this, TextBox):
            start = this.selection_start
            this.select(start, start + value)

    @external()
    def TEdit__SetCharCase(self, this: object, value: int):
        if isinstance(this, TextBox) and value in _EDIT_CHAR_CASES:
            this.set_case(TEditCharCase(value))

    def _set_box_handler(self, this: object, handler: object):
        if isinstance(this, TextBox):
            this.on_change = handler if isinstance(handler, Function) else None

    @external()
    def TEdit__SetOnChange(self, this: object, handler: object):
        self._set_box_handler(this, handler)

    @external()
    def TPasswordEdit__SetOnChange(self, this: object, handler: object):
        self._set_box_handler(this, handler)

    @external(alias=['ShellExecuteA', 'ShellExecuteW'])
    def shell32__ShellExecute(self, hwnd: int, operation: str, file: str, args: str, cwd: str, show: int) -> int:
        if file == self.config.executable:
            for arg in shlex.split(args):
                if arg.upper().startswith('/PASSWORD='):
                    yield NewPassword(arg[10:])
        return 0x40

    def _PROCESSENTRY32(self, record: list, pid: int):
        path = self.config.processes[pid]
        if path is None:
            _, path = ntpath.split(self.config.executable)
        ...                 # DWORD     dwSize
        record[1] = 0       # DWORD     cntUsage
        record[2] = pid     # DWORD     th32ProcessID
        record[3] = 0       # ULONG_PTR th32DefaultHeapID
        record[4] = 0       # DWORD     th32ModuleID
        record[5] = 1       # DWORD     cntThreads
        record[6] = 0       # DWORD     th32ParentProcessID
        record[7] = 0       # LONG      pcPriClassBase
        record[8] = 0       # DWORD     dwFlags
        szExe = record[9]   # CHAR      szExeFile[MAX_PATH]
        szExe[:len(path)] = path # noqa
        szExe[ len(path)] = '\0' # noqa

    @external()
    def kernel32__GetLastError(self) -> int:
        return self._win32api_last_error

    @external()
    def kernel32__SetLastError(self, code: int):
        self._win32api_last_error = code

    @external()
    def kernel32__CreateToolhelp32Snapshot(self, flags: int, process_id: int) -> int:
        if not self.config.allow_process_listing:
            return -1
        return random.randrange(0x10, 0x100000)

    @external(alias=['Process32FirstW'])
    def kernel32__Process32First(self, handle: int, p_record: Variable[list]) -> bool:
        if not (pids := self._win32api_sorted_process_ids):
            return False
        record = p_record.get()
        pid = pids[0]
        self._PROCESSENTRY32(record, pid)
        p_record.set(record)
        return True

    @external(alias=[
        'LoadLibraryW',
        'LoadLibraryA',
    ])
    def kernel32__LoadLibrary(self, name: str) -> int:
        name = name.lower()
        base, dot, ext = name.rpartition('.')
        if dot and ext.lower() == 'dll':
            name = base
        return self.winapi_module_to_addr.get(name, 0)

    @external(alias=['GetProcAddressA'])
    def kernel32__GetProcAddress(self, module: int, name: str) -> int:
        if module not in self.winapi_addr_to_module:
            return 0
        return self.winapi_symbol_to_addr.get(name, 0)

    @external(alias=['Process32NextW'])
    def kernel32__Process32Next(self, handle: int, p_record: Variable[list]) -> bool:
        record = p_record.get()
        pid = record[2]
        pids = self._win32api_sorted_process_ids
        k = bisect.bisect_right(pids, pid)
        if k < len(pids):
            pid = pids[k]
            self._PROCESSENTRY32(record, pid)
            p_record.set(record)
            return True
        else:
            self.kernel32__SetLastError(ERROR_NO_MORE_FILES)
            return False

    @external()
    def kernel32__GetTickCount(self) -> int:
        tick = self.clock
        tick *= self.config.milliseconds_per_instruction
        tick += self.seconds_slept * 1000
        return int(tick)

    @external()
    def user32__GetSystemMetrics(self, index: int) -> int:
        if index == 80:
            return 1
        if index == 43:
            return 2
        return 0

    @external()
    def IsArm32Compatible(self) -> bool:
        return architecture('arm32compatible', self.config.x64)

    @external()
    def IsArm64(self) -> bool:
        return architecture('arm64', self.config.x64)

    @external()
    def IsX64(self) -> bool:
        return architecture('x64', self.config.x64)

    @external()
    def IsX64OS(self) -> bool:
        return architecture('x64os', self.config.x64)

    @external()
    def IsX64Compatible(self) -> bool:
        return architecture('x64compatible', self.config.x64)

    @external()
    def IsX86(self) -> bool:
        return architecture('x86', self.config.x64)

    @external()
    def IsX86OS(self) -> bool:
        return architecture('x86os', self.config.x64)

    @external()
    def IsX86Compatible(self) -> bool:
        return architecture('x86compatible', self.config.x64)

    @external(alias=[
        'Debugging',
        'IsUninstaller',
    ])
    def Terminated(self) -> bool:
        return False

    @external()
    def IsAdmin(self) -> bool:
        return self.config.admin

    @external(name='Sleep')
    @external()
    def kernel32__Sleep(self, ms: int):
        seconds = ms / 1000.0
        self.seconds_slept += seconds

    @external()
    def Random(self, top: int) -> int:
        return random.randrange(0, top)

    @external(alias='StrGet')
    def WStrGet(self, string: Variable[str], index: int) -> str:
        text = string.get()
        if not 1 <= index <= len(text):
            raise IFPSException(
                _OUT_OF_STRING_RANGE,
                PSError.CustomError,
                failed_call=self.dialect is Dialect.Inno6,
            )
        return text[index - 1]

    @external(alias='StrSet')
    def WStrSet(self, char: str, index: int, dst: Variable[str]):
        old = dst.get()
        if not 1 <= index <= len(old):
            raise IFPSException(_OUT_OF_STRING_RANGE, PSError.CustomError)
        dst.set(old[:index - 1] + char + old[index:])

    @external()
    def GetEnv(self, name: str) -> str:
        return self.config.environment.get(name, F'%{name}%')

    @external()
    def Beep(self):
        pass

    @external()
    def Abort(self):
        if self.config.throw_abort:
            raise AbortEmulation
        raise IFPSException(SysConst.SOperationAborted)

    @external()
    def DirExists(self, path: str) -> bool:
        return self.installed.directory_exists(path)

    @external()
    def FileOrDirExists(self, path: str) -> bool:
        return self.installed.exists(path)

    @external()
    def ForceDirectories(self, path: str) -> bool:
        return True

    @external(alias='LoadStringFromLockedFile')
    def LoadStringFromFile(self, path: str, out: Variable[str]) -> bool:
        return True

    @external(alias='LoadStringsFromLockedFile')
    def LoadStringsFromFile(self, path: str, out: Variable[str]) -> bool:
        return True

    def code_runner(self, caller: _ScriptRun | None) -> constants.CodeRunner:
        """
        Runs the function of a code constant inside the run of the caller, or in a new run of the
        script without one.
        """
        def run_code(function: str, argument: str) -> Generator[Any, Any, str]:
            value = yield from self.call_by_name(function, [argument], CODE_CONSTANT, caller)
            return str(value)
        return run_code

    @external(caller=True)
    def ExpandConstant(self, string: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        return self.constants.expand(string, self.code_runner(caller))

    @external(caller=True)
    def ExpandConstantEx(
        self,
        string: str,
        custom_var: str,
        custom_val: str,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, str]:
        return self.constants.expand(string, self.code_runner(caller), {custom_var: custom_val})

    def expand_constant(self, string: str, custom_var: str | None = None, custom_val: str = '') -> str:
        """
        Expands the constants in the given string as Setup expands them, with one optional custom
        constant. A code constant runs a function of the script; its calls are not reported.
        """
        custom = None if custom_var is None else {custom_var: custom_val}
        expansion = EventCall(self._timed(self.constants.expand(string, self.code_runner(None), custom)))
        value = expansion.value
        assert value is not None
        return value

    @external()
    def DeleteFile(self, path: str) -> bool:
        self.installed.remove(path)
        return True

    @external()
    def FileExists(self, file_name: str) -> bool:
        return self.installed.file_exists(file_name)

    @external()
    def Log(self, log: str):
        ...

    @external()
    def Inc(self, p: Variable[int]):
        if not p.deref().type.code.integer:
            raise IFPSException(kind=PSError.CouldNotCallProc)
        p.set(p.get() + 1)

    @external()
    def Dec(self, p: Variable[int]):
        if not p.deref().type.code.integer:
            raise IFPSException(kind=PSError.CouldNotCallProc)
        p.set(p.get() - 1)

    @external()
    def FindFirst(self, file_name: str, frec: Variable) -> bool:
        return False

    @external()
    def Round(self, x: x87.Real) -> int:
        if self.setup.release.since((6, 6, 0)):
            return self.runtime.fpu.integer(x, x87.Rounding.Nearest)
        return self.runtime.fpu.integer(x)

    @external()
    def Trunc(self, x: x87.Real) -> int:
        return self.runtime.fpu.integer(x, x87.Rounding.TowardZero)

    @external()
    def Int(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.round_integral(x, x87.Rounding.TowardZero)

    @external()
    def Abs(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.absolute(x)

    @external()
    def Pi(self) -> x87.Real:
        return self.runtime.fpu.pi()

    @external()
    def Sqrt(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.square_root(x)

    @external()
    def Sin(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.sine(x)

    @external()
    def Cos(self, x: x87.Real) -> x87.Real:
        return self.runtime.fpu.cosine(x)

    @external()
    def GetSpaceOnDisk(
        self,
        path: str,
        in_megabytes: bool,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        _a = 3_000_000
        _t = 5_000_000
        if not in_megabytes:
            _a *= 1000
            _t *= 1000
        avail.set(_a)
        space.set(_t)
        return True

    @external()
    def GetSpaceOnDisk64(
        self,
        path: str,
        avail: Variable[int],
        space: Variable[int],
    ) -> bool:
        avail.set(3_000_000_000)
        space.set(5_000_000_000)
        return True

    @external()
    def Exec(
        self,
        exe: str,
        cmd: str,
        cwd: str,
        show: int,
        wait: int,
        out: Variable[int],
    ) -> bool:
        out.set(0)
        return True

    @external()
    def GetCmdTail(self) -> str:
        return ''

    @external()
    def ParamCount(self) -> int:
        return 0

    @external()
    def ParamStr(self, index: int) -> str:
        return ''

    @external()
    def ActiveLanguage(self) -> str:
        return self.setup.language_name

    @external()
    def CustomMessage(self, msg_name: str) -> str:
        if (message := self.setup.custom_message(msg_name)) is None:
            raise InternalError(F'Unknown custom message name "{msg_name}"')
        return message

    @external()
    def FmtMessage(self, fmt: str, args: list[str]) -> str:
        return constants.fmt_message(fmt, args)

    @external()
    def Format(self, fmt: str, args: Variable[list]) -> str:
        elements = args.deref()
        arguments = [_open_array_element(elements.at(k)) for k in range(len(elements))]
        return formatting.format(fmt, arguments, self.runtime, self._reserve_text)

    @external()
    def SetupMessage(self, id: int) -> str:
        return self.setup.message(id)

    @external()
    def IsWin64(self) -> bool:
        return self.config.x64

    @external()
    def Is64BitInstallMode(self) -> bool:
        return self.setup.install_mode.decided()

    @external()
    def RaiseException(self, msg: str):
        raise IFPSException(msg)

    @external(caller=True)
    def GetExceptionMessage(self, caller: _ScriptRun | None = None) -> str:
        if caller is None or (error := caller.current_exception) is None:
            return '(There is no current exception)'
        return error.message

    @external()
    def ProcessorArchitecture(self) -> int:
        return processor(self.config.x64).value

    @external()
    def GetUserNameString(self) -> str:
        return self.config.user_name

    @external()
    def GetComputerNameString(self) -> str:
        return self.config.host_name

    @external()
    def GetUILanguage(self) -> int:
        return self.config.lcid

    @external()
    def GetArrayLength(self, array: Variable) -> int:
        array = array.deref()
        return len(array)

    @external()
    def SetArrayLength(self, array: Variable, n: int):
        array = array.deref()
        if isinstance(t := array.type, TArray):
            self._reserve_variables(t.type, n)
        array.resize(n)

    @external()
    def WizardForm(self) -> object:
        return self._form()

    @external()
    def MainForm(self) -> object:
        """
        The function of Inno Setup 5 that gives Setup's main form; before Setup created the form,
        it raises an internal error.
        """
        if (main := self.wizard_form.main) is None:
            raise InternalError('An attempt was made to access MainForm before it has been created')
        return main

    def _form(self) -> WizardForm:
        """
        The wizard form, for a function that Setup runs through `GetWizardForm`.
        """
        form = self.wizard_form
        form.require()
        return form

    @external()
    def CreateCustomPage(self, after: int, caption: str, description: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputQueryPage(self, after: int, caption: str, description: str, subcaption: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputOptionPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        exclusive: bool,
        listbox: bool,
    ) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputDirPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        append: bool,
        folder: str,
    ) -> object:
        return self._form().add_page(after)

    @external()
    def CreateInputFilePage(self, after: int, caption: str, description: str, subcaption: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateOutputMsgPage(self, after: int, caption: str, description: str, message: str) -> object:
        return self._form().add_page(after)

    @external()
    def CreateOutputMsgMemoPage(
        self,
        after: int,
        caption: str,
        description: str,
        subcaption: str,
        message: str,
    ) -> object:
        return self._form().add_page(after)

    @external(alias='CreateOutputMarqueeProgressPage')
    def CreateOutputProgressPage(self, caption: str, description: str) -> object:
        return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)

    @external(alias='CreateExtractionPage')
    def CreateDownloadPage(self, caption: str, description: str, progress: object) -> object:
        return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)

    @external()
    def TWizardPage__GetID(self, this: object) -> int:
        return _wizard_page(this).id

    @external()
    def Class__SetNil(self, value: object) -> object:
        """
        Pascal Script's assignment of nil to a variable of a class.
        """
        return None

    @external()
    def Class__CastToType(self, source: object) -> object:
        """
        A cast of an object to a class; the emulator's objects do not record the classes that the
        script knows, and the cast keeps the object.
        """
        return source

    @external()
    def GetWizardForm(self) -> object:
        return self._form()

    @external()
    def PageFromID(self, id: int) -> object:
        return self._form().page(id)

    @external()
    def PageIndexFromID(self, id: int) -> int:
        return self._form().page_index(id)

    @external()
    def WizardDirValue(self) -> str:
        return self.setup.path_func.remove_backslash_unless_root(self._form().dir_edit.text)

    @external()
    def WizardGroupValue(self) -> str:
        return self.setup.path_func.remove_backslash_unless_root(self._form().group_edit.text)

    @external()
    def IsAdminInstallMode(self) -> bool:
        return self.setup.admin_install_mode

    @external(caller=True)
    def GetPreviousData(self, name: str, default: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        app_id = '' if (info := self.setup_info) is None else info.Header.text(TextDirective.AppId) or ''
        yield from self.constants.expand(app_id, self.code_runner(caller))
        return default

    @external()
    def TWizardForm__GetCurPageID(self, this: object) -> int:
        return self.wizard_form.current

    def _set_page_handler(self, this: object, event: PageEvent, handler: object):
        page = _wizard_page(this)
        if isinstance(handler, Function):
            page.handlers[event] = handler
        else:
            page.handlers.pop(event, None)

    @external()
    def TWizardPage__SetOnActivate(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnActivate, handler)

    @external()
    def TWizardPage__SetOnShouldSkipPage(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnShouldSkipPage, handler)

    @external()
    def TWizardPage__SetOnNextButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnNextButtonClick, handler)

    @external()
    def TWizardPage__SetOnBackButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnBackButtonClick, handler)

    @external()
    def TWizardPage__SetOnCancelButtonClick(self, this: object, handler: object):
        self._set_page_handler(this, PageEvent.OnCancelButtonClick, handler)

    @external()
    def Unassigned(self) -> Variant:
        return variants.EMPTY

    @external()
    def Null(self) -> Variant:
        return variants.NULL

    @external()
    def VarType(self, v: Variant) -> int:
        return v.type

    @external()
    def VarIsNull(self, v: Variant) -> bool:
        return v.type == VarType.Null

    @external()
    def VarIsEmpty(self, v: Variant) -> bool:
        return v.type == VarType.Empty

    @external()
    def VarIsClear(self, v: Variant) -> bool:
        return v.type == VarType.Empty or v.type in (VarType.Dispatch, VarType.Unknown) and v.value is None

    @external()
    def Set8087CW(self, cw: int):
        self.runtime.control_word = cw & 0xFFFF

    @external()
    def Get8087CW(self) -> int:
        return self.runtime.control_word | x87.RESERVED_BIT

    @external()
    def GetDateTimeString(
        self,
        fmt: str,
        date_separator: str,
        time_separator: str,
    ) -> str:

        now = self.config.start_time
        now = now + timedelta(
            milliseconds=(self.config.milliseconds_per_instruction * self.clock))
        now = now + timedelta(seconds=self.seconds_slept)

        date_separator = date_separator.lstrip('\0')
        time_separator = time_separator.lstrip('\0')

        def dt(m: re.Match[str]):
            spec = m[1]
            ampm = m[2]
            if ampm:
                am, _, pm = ampm.partition('/')
                spec = spec.upper()
                suffix = now.strftime('%p').lower()
                suffix = {'am': am, 'pm': pm}[suffix]
            else:
                suffix = ''
            if spec == 'dddddd' or spec == 'ddddd':
                return now.date().isoformat()
            if spec == 't':
                return now.time().isoformat('minutes')
            if spec == 'tt':
                return now.time().isoformat('seconds')
            if spec == 'd':
                return str(now.day)
            if spec == 'm':
                return str(now.month)
            if spec == 'h':
                return str(now.hour)
            if spec == 'n':
                return str(now.minute)
            if spec == 's':
                return str(now.second)
            if spec == 'H':
                return now.strftime('%I').lstrip('0') + suffix
            if spec == '/':
                return date_separator or spec
            if spec == ':':
                return time_separator or spec
            return now.strftime({
                'dddd'  : '%A',
                'ddd'   : '%a',
                'dd'    : '%d',
                'mmmm'  : '%B',
                'mmm'   : '%b',
                'mm'    : '%m',
                'yyyy'  : '%Y',
                'yy'    : '%y',
                'hh'    : '%H',
                'HH'    : '%I' + suffix,
                'nn'    : '%M',
                'ss'    : '%S',
            }.get(spec, m[0]))

        split = re.split(F'({formats.string!s})', fmt)
        for k in range(0, len(split), 2):
            split[k] = re.sub('(d+|m+|y+|h+|n+|s+|t+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
        for k in range(1, len(split), 2):
            split[k] = split[k][1:-1]
        return ''.join(split)

    @external()
    def Chr(self, b: int) -> str:
        return chr(b)

    @external()
    def Ord(self, c: str) -> int:
        return ord(c)

    @external()
    def Copy(self, string: str, index: int, count: int) -> str:
        index -= 1
        return string[index:index + count]

    @external()
    def Length(self, string: str) -> int:
        return len(string)

    @external(alias='AnsiLowercase')
    def Lowercase(self, string: str) -> str:
        return string.lower()

    @external(alias='AnsiUppercase')
    def Uppercase(self, string: str) -> str:
        return string.upper()

    @external(alias='Replicate')
    def StringOfChar(self, c: str, count: int) -> str:
        self._reserve_text(count, wide=False)
        return c * count

    @external()
    def Delete(self, string: Variable[str], index: int, count: int):
        index -= 1
        old = string.get()
        string.set(old[:index] + old[index + count:])

    @external()
    def Insert(self, string: str, dest: Variable[str], index: int):
        index -= 1
        old = dest.get()
        dest.set(old[:index] + string + old[index:])

    @external()
    def StringChange(self, string: Variable[str], old: str, new: str) -> int:
        return self.StringChangeEx(string, old, new, False)

    @external()
    def StringChangeEx(self, string: Variable[str], old: str, new: str, _: bool) -> int:
        haystack = string.get()
        count = haystack.count(old)
        self._reserve_text(len(haystack) + count * (len(new) - len(old)))
        string.set(haystack.replace(old, new))
        return count

    @external()
    def Pos(self, sub: str, string: str) -> int:
        if not sub:
            return 0
        return string.find(sub) + 1

    @external()
    def AddQuotes(self, string: str) -> str:
        if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
            string = F'"{string}"'
        return string

    @external()
    def RemoveQuotes(self, string: str) -> str:
        if string and string[0] == '"' and string[~0] == '"':
            string = string[1:-1]
        return string

    @external()
    def CompareText(self, a: str, b: str) -> int:
        return self.CompareStr(a.casefold(), b.casefold())

    @external()
    def CompareStr(self, a: str, b: str) -> int:
        if a > b:
            return +1
        if a < b:
            return -1
        return 0

    @external()
    def SameText(self, a: str, b: str) -> bool:
        return a.casefold() == b.casefold()

    @external()
    def SameStr(self, a: str, b: str) -> bool:
        return a == b

    @external()
    def IsWildcard(self, pattern: str) -> bool:
        return '*' in pattern or '?' in pattern

    @external()
    def WildcardMatch(self, text: str, pattern: str) -> bool:
        return fnmatch.fnmatch(text, pattern)

    @external()
    def Trim(self, string: str) -> str:
        return strings.trim(string)

    @external()
    def TrimLeft(self, string: str) -> str:
        return strings.trim_left(string)

    @external()
    def TrimRight(self, string: str) -> str:
        return strings.trim_right(string)

    @external()
    def StringJoin(self, sep: str, values: list[str]) -> str:
        self._reserve_text(sum(map(len, values)) + len(sep) * max(len(values) - 1, 0))
        return sep.join(values)

    @external()
    def StringSplitEx(self, string: str, separators: list[str], quote: str, how: TSplitType) -> list[str]:
        def spans():
            found = _split_spans(string, separators, quote)
            if how == TSplitType.stExcludeEmpty:
                return (span for span in found if span[0] < span[1])
            if how == TSplitType.stExcludeLastEmpty:
                return _without_empty_last(found)
            return found
        self._reserve_variables(_StringType, sum(1 for _ in spans()))
        return [string[start:end] for start, end in spans()]

    @external()
    def StringSplit(self, string: str, separators: list[str], how: TSplitType) -> list[str]:
        return self.StringSplitEx(string, separators, '', how)

    @external()
    def StrToInt(self, s: str) -> int:
        value, error = numbers.val_integer(s, 32)
        return -1 if error else value

    @external()
    def StrToIntDef(self, s: str, d: int) -> int:
        value, error = numbers.val_integer(s, 32)
        return d if error else value

    @external()
    def StrToInt64(self, s: str) -> int:
        return numbers.str_to_int64(s)

    @external()
    def StrToInt64Def(self, s: str, d: int) -> int:
        return numbers.str_to_int64_default(s, d)

    @external()
    def StrToFloat(self, s: str) -> x87.Real:
        runtime = self.runtime
        value, error = numbers.val_float(s, runtime.library)
        if error or value is None:
            raise IFPSException(_INVALID_FLOAT)
        return runtime.fpu.store(value, x87.Format.Extended)

    @external()
    def FloatToStr(self, e: x87.Real) -> str:
        if self.dialect is Dialect.Inno6:
            return numbers.str_fixed(e, 12, self.runtime.library)
        return numbers.float_to_str(e, self.runtime)

    @external()
    def StrToUInt64(self, s: str) -> int:
        return numbers.str_to_uint64(s)

    @external()
    def StrToUInt64Def(self, s: str, d: int) -> int:
        return numbers.str_to_uint64_default(s, d)

    @external(alias='UInt64ToStr')
    def UIntToStr(self, i: int) -> str:
        return str(i & 0xFFFFFFFFFFFFFFFF)

    @external(alias='Int64ToStr')
    def IntToStr(self, i: int) -> str:
        return str(i)

    @external()
    def PadL(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.rjust(n)

    @external()
    def PadR(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.ljust(n)

    @external()
    def PadZ(self, s: str, n: int) -> str:
        self._reserve_text(n)
        return s.rjust(n, '0')

    @external()
    def StrToVersion(self, s: str, v: Variable[int]) -> bool:
        try:
            packed = bytes(map(int, s.split('.')))
        except Exception:
            return False
        if len(packed) != 4:
            return False
        v.set(int.from_bytes(packed, 'little'))
        return True

    @external()
    def CharLength(self, string: str, index: int) -> int:
        return 1

    @external()
    def AddBackslash(self, string: str) -> str:
        return self.setup.path_func.add_backslash(string)

    @external()
    def AddPeriod(self, string: str) -> str:
        if string and string[~0] != '.':
            string = F'{string}.'
        return string

    @external()
    def RemoveBackslashUnlessRoot(self, string: str) -> str:
        return self.setup.path_func.remove_backslash_unless_root(string)

    @external()
    def RemoveBackslash(self, string: str) -> str:
        return self.setup.path_func.remove_backslash(string)

    @external()
    def ChangeFileExt(self, name: str, ext: str) -> str:
        return self.setup.path_func.change_ext(name, ext)

    @external()
    def ExtractFileExt(self, name: str) -> str:
        return self.setup.path_func.extract_ext(name)

    @external()
    def ExtractFileDir(self, name: str) -> str:
        return self.setup.path_func.extract_dir(name)

    @external()
    def ExtractFilePath(self, name: str) -> str:
        return self.setup.path_func.extract_path(name)

    @external()
    def ExtractFileName(self, name: str) -> str:
        return self.setup.path_func.extract_name(name)

    @external()
    def ExtractFileDrive(self, name: str) -> str:
        return self.setup.path_func.extract_drive(name)

    @external()
    def ExtractRelativePath(self, base: str, dst: str) -> str:
        return self.setup.path_func.extract_relative_path(base, dst)

    @external()
    def ExpandFileName(self, name: str) -> str:
        try:
            return self.setup.path_func.expand(name, self.current_folder.path)
        except PathFuncError as error:
            raise DelphiException(str(error)) from error

    @external()
    def ExpandUNCFileName(self, name: str) -> str:
        return expand_file_name(name, self.current_folder.path)

    @external()
    def GetCurrentDir(self) -> str:
        return self.current_folder.path

    @external()
    def SetLength(self, target: Variable, size: int):
        target = target.deref()
        if target.container:
            self.SetArrayLength(target, size)
            return
        size = max(size, 0)
        self._reserve_text(size, wide=target.type.code not in (TC.AnsiString, TC.PChar))
        old = target.get()
        old = old.ljust(size, '\0')
        target.set(old[:size])

    @external(alias='OemToCharBuff')
    def CharToOemBuff(self, string: Variable[str]):
        # TODO
        ...

    @external()
    def Utf8Encode(self, string: str) -> str:
        return string.encode('utf8').decode('latin1')

    @external()
    def Utf8Decode(self, string: str) -> str:
        return string.encode('latin1').decode('utf8')

    @external()
    def GetMD5OfString(self, string: str) -> str:
        return hashlib.md5(string.encode('latin1')).hexdigest()

    @external()
    def GetMD5OfUnicodeString(self, string: str) -> str:
        return hashlib.md5(string.encode('utf8')).hexdigest()

    @external()
    def GetSHA1OfString(self, string: str) -> str:
        return hashlib.sha1(string.encode('latin1')).hexdigest()

    @external()
    def GetSHA1OfUnicodeString(self, string: str) -> str:
        return hashlib.sha1(string.encode('utf8')).hexdigest()

    @external()
    def GetSHA256OfString(self, string: str) -> str:
        return hashlib.sha256(string.encode('latin1')).hexdigest()

    @external()
    def GetSHA256OfUnicodeString(self, string: str) -> str:
        return hashlib.sha256(string.encode('utf8')).hexdigest()

    @external()
    def SysErrorMessage(self, code: int) -> str:
        return F'[description for error {code & 0xFFFFFFFF:08X}]'

    @external()
    def MinimizePathName(self, path: str, font: object, max_len: int) -> str:
        return path

    @external()
    def CheckForMutexes(self, mutexes: str) -> bool:
        return any(m in self.mutexes for m in mutexes.split(','))

    @external()
    def CreateMutex(self, name: str):
        if self.config.log_mutexes:
            yield NewMutex(name)
        self.mutexes.add(name)

    @external()
    def GetWinDir(self) -> str:
        return self.expand_constant('{win}')

    @external()
    def GetSystemDir(self) -> str:
        return self.expand_constant('{sys}')

    @external()
    def GetWindowsVersion(self) -> int:
        version = int.from_bytes(
            struct.pack('>BBH', *self.config.windows_os_version), 'big')
        return version

    @external()
    def GetWindowsVersionEx(self, tv: Variable[int | bool]):
        tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
        tv[3], tv[4]        = self.config.windows_sp_version # noqa
        tv[5], tv[6], tv[7] = True, 0, 0

    @external()
    def GetWindowsVersionString(self) -> str:
        return '{}.{:02d}.{:04d}'.format(*self.config.windows_os_version)

    @external()
    def CreateOleObject(self, name: str) -> OleObject:
        return OleObject(name)

    @external()
    def GetActiveOleObject(self, name: str) -> OleObject:
        return OleObject(name)

    @external()
    def IDispatchInvoke(self, ole: OleObject | None, prop_set: bool, name: str, value: Any) -> Variant:
        if ole is None:
            raise IFPSException(_NIL_INVOKE)
        return variants.EMPTY

    @external()
    def FindWindowByClassName(self, name: str) -> int:
        return 0

    @external()
    def FindWindowByWindowName(self, name: str) -> int:
        return 0

    @external()
    def WizardSilent(self) -> bool:
        return self.config.wizard_silent

    @external()
    def WizardNoIcons(self) -> bool:
        return self._form().icons_off

    @external()
    def WizardSetupType(self, descriptions: bool) -> str:
        form = self._form()
        if (setup_type := form.setup_type) is None:
            return ''
        return form.type_description(setup_type) if descriptions else setup_type.Name

    def _field_expander(self, caller: _ScriptRun | None) -> Expander:
        """
        Expands the fields of the entries in the lists of the wizard for a support function that the
        script calls: a code constant runs inside the run of the caller, as in `ExpandConstant`.
        """
        def expand(entry: object, field: EntryField, text: str) -> Generator[Any, Any, str]:
            return self.constants.expand(text, self.code_runner(caller))
        return expand

    @external(caller=True)
    def WizardSelectedComponents(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        expand = self._field_expander(caller)
        return comma_text((yield from self._form().selected_components(descriptions, False, expand)))

    @external(caller=True)
    def WizardSelectedTasks(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
        expand = self._field_expander(caller)
        return comma_text((yield from self._form().selected_tasks(descriptions, False, False, expand)))

    @external(alias=['IsComponentSelected'])
    def WizardIsComponentSelected(self, components: str) -> bool:
        return _selected(self._form().selection().components, components)

    @external(alias=['IsTaskSelected'])
    def WizardIsTaskSelected(self, tasks: str) -> bool:
        return _selected(self._form().selection().tasks, tasks)

    @external()
    def WizardSelectComponents(self, components: str):
        self._form().select_components(comma_strings(components.replace('/', '\\')))

    @external()
    def WizardSelectTasks(self, tasks: str):
        self._form().select_tasks(comma_strings(tasks.replace('/', '\\')))

    @external()
    def TWizardForm__GetComponentsList(self, this: object) -> object:
        return self.wizard_form.components

    @external()
    def TWizardForm__GetTasksList(self, this: object) -> object:
        return self.wizard_form.tasks

    @external()
    def TWizardForm__GetRunList(self, this: object) -> object:
        return self.wizard_form.run_list

    @external()
    def TWizardForm__GetTypesCombo(self, this: object) -> object:
        return self.wizard_form.types_combo

    @external()
    def TWizardForm__GetNoIconsCheck(self, this: object) -> object:
        return self.wizard_form.no_icons_check

    @external()
    def TNewCheckListBox__GetChecked(self, this: object, index: int) -> bool:
        if isinstance(this, CheckList):
            return this.checked(index)
        return True

    @external()
    def TNewCheckListBox__SetChecked(self, this: object, index: int, checked: bool):
        if isinstance(this, CheckList):
            this.set_checked(index, checked)

    @external()
    def TNewCheckListBox__GetState(self, this: object, index: int) -> int:
        if isinstance(this, CheckList):
            return this.state(index)
        return 1

    @external()
    def TNewCheckListBox__GetItemEnabled(self, this: object, index: int) -> bool:
        if isinstance(this, CheckList):
            return this.enabled(index)
        return True

    @external()
    def TNewCheckListBox__CheckItem(self, this: object, index: int, operation: int) -> bool:
        if isinstance(this, CheckList):
            return this.check(index, CheckOperation(operation))
        return True

    @external()
    def CurrentFilename(self) -> str:
        if (file := self.current_file) is None or not file.destination:
            raise _outside_file_entry('CurrentFilename')
        return file.destination

    @external()
    def CurrentSourceFilename(self) -> str:
        if (file := self.current_file) is None or not file.source:
            raise _outside_file_entry('CurrentSourceFilename', ' with flag "external"')
        return file.source

    def expand(self, text: str, custom: Mapping[str, str] | None = None) -> Generator[Any, Any, str]:
        """
        Expands the constants in a text of the setup where Setup expands it, with the custom
        constants that Setup passes for it; each code constant runs in a new run of the script.
        """
        return self.constants.expand(text, self.code_runner(None), custom)

    def registry_key_exists(self, key: str) -> bool:
        """
        Whether a key of the emulated registry exists.
        """
        return self.constants.registry('', None, key)

    @property
    def temporary_directory(self) -> str:
        """
        The temporary directory of Setup, the value of `{tmp}`.
        """
        return self.constants.temporary_directory

    def check_time(self):
        """
        Ends the emulation when its time is up.
        """
        if (watch := self._stopwatch) is not None and watch.expired():
            raise EmulatorTimeout

    def _setup_codes(self) -> dict[ArgumentKind, TC]:
        """
        The type codes of the variables that Setup creates for the values that it passes to the
        functions of the script and for the results that it takes from them.
        """
        return {
            ArgumentKind.String  : TC.UnicodeString if self.ifps.unicode else TC.AnsiString,
            ArgumentKind.Integer : TC.S32,
            ArgumentKind.Boolean : TC.U08,
            ArgumentKind.Step    : TC.U08,
            ArgumentKind.Page    : TC.Class,
            ArgumentKind.Object  : TC.Class,
        }

    def _fits(self, function: Function, call: SetupCall) -> bool:
        """
        Whether the declaration of a function of the script takes the call that Setup makes: the
        same number of parameters, each passed the same way and of the type that Setup passes, and
        a result of the type that Setup takes. A parameter or a result whose type the script does
        not record is taken to fit, as Pascal Script records none for a support function that Inno
        Setup does not document, such as `UsingWinNT`.
        """
        if (decl := function.decl) is None or len(decl.parameters) != len(call.parameters):
            return False
        codes = self._setup_codes()
        for parameter, (kind, by_value) in zip(decl.parameters, call.parameters):
            if parameter.const != by_value:
                return False
            if parameter.type is not None and parameter.type.code != codes[kind]:
                return False
        if call.result is None:
            return decl.void
        if decl.void:
            return False
        return (result := decl.return_type) is None or result.code == codes[call.result]

    def call_from_setup(
        self,
        function: Function,
        arguments: Sequence,
        call: SetupCall,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, Any]:
        """
        Calls a function of the script as Setup does: Pascal Script runs a function whose
        declaration does not take the call on a stack that does not fit it, and the emulation ends
        there. Setup creates the variables of the values that it passes and of the result that it
        takes.
        """
        if not self._fits(function, call):
            raise EmulatorUndefined(F'Setup calls {function.name} as {call}, which its declaration does not take.')
        codes = self._setup_codes()
        types = [TPrimitive(codes[kind]) for kind, _ in call.parameters]
        result = None if call.result is None else TPrimitive(codes[call.result])
        run = _ScriptRun(self.dialect, caller)
        return (yield from self._execute(run, function, arguments, types, result))

    def call_by_name(
        self,
        name: str,
        arguments: list,
        call: SetupCall,
        caller: _ScriptRun | None = None,
    ) -> Generator[Any, Any, Any]:
        """
        Calls a function of the script by name as Setup's `CodeRunner` finds it: a function of the
        script or a support function that the script imports.
        """
        if (function := self.symbols.get(name)) is None:
            raise DelphiException('Script error: Could not call proc')
        return (yield from self.call_from_setup(function, arguments, call, caller))

    @external()
    def Low(self, var: Variable) -> int:
        return self._bound(var.deref(), False)

    @external()
    def High(self, var: Variable) -> int:
        return self._bound(var.deref(), True)

    def _bound(self, var: Variable, high: bool) -> int:
        t = var.type
        code = t.code
        if code is TC.Array:
            return len(var) - 1 if high else 0
        if isinstance(t, TStaticArray):
            start = t.offset or 0
            return start + t.size - 1 if high else start
        if code is TC.AnsiString:
            return len(var.get()) if high else 1
        if (valid := code.integer_range) is not None:
            return valid.stop - 1 if high else valid.start
        raise IFPSException(kind=PSError.CouldNotCallProc)

    @external()
    def SizeOf(self, var: Variable) -> int:
        return pascal.size_of(var.type, self.runtime)

    del external

Subclasses

Class variables

var external_symbols

The type of the None singleton.

Instance variables

var globals

The global variables of the script.

Expand source code Browse git
@property
def globals(self) -> list[Variable]:
    """
    The global variables of the script.
    """
    if (script_globals := self._globals) is None:
        for v in self.ifps.globals:
            self._reserve_variables(v.type)
        script_globals = self._globals = [Variable(v.type, v.spec) for v in self.ifps.globals]
    return script_globals
var temporary_directory

The temporary directory of Setup, the value of {tmp}.

Expand source code Browse git
@property
def temporary_directory(self) -> str:
    """
    The temporary directory of Setup, the value of `{tmp}`.
    """
    return self.constants.temporary_directory

Methods

def reset(self, walked=False)

Reset the emulator timing, the state of the Delphi run-time library, and mutexes. The script starts over with new global variables that hold their default values, in a new Setup process with a new wizard form, which Setup walks through when walked is set.

Expand source code Browse git
def reset(self, walked: bool = False):
    """
    Reset the emulator timing, the state of the Delphi run-time library, and mutexes. The
    script starts over with new global variables that hold their default values, in a new Setup
    process with a new wizard form, which Setup walks through when `walked` is set.
    """
    self.seconds_slept = 0.0
    self.clock = 0
    self.runtime = self.setup.runtime(numbers.FormatSettings.of_locale(self.config.lcid))
    self.machine = pascal.Machine(self.dialect, self.runtime)
    self.mutexes.clear()
    self.wizard_form = wizard = WizardForm(
        StandIns.of(self.config),
        walked=walked,
        entries_known=self.setup_info is not None,
        uninstallable=self.setup.uninstallable.answer is not False,
    )
    self.constants = SetupConstants(self.setup, self.config, wizard, self.machine_folders)
    self.current_folder = CurrentFolder(self.launch_folder)
    self.installed = InstalledPaths(self.current_folder)
    self.current_file: CurrentFile | None = None
    self._globals = None
    self._win32api_last_error = 0
    self._win32api_sorted_process_ids = sorted(self.config.processes)
    return self
def unimplemented(self, function, arguments, result)

The base IFPS emulator raises refinery.lib.inno.emulator.NeedSymbol when an external symbol is unimplemented. Child classes can override this function to handle the missing symbol differently; it receives the arguments of the call and the variable for its result, if the symbol is a function.

Expand source code Browse git
def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
    """
    The base IFPS emulator raises `refinery.lib.inno.emulator.NeedSymbol` when an external
    symbol is unimplemented. Child classes can override this function to handle the missing
    symbol differently; it receives the arguments of the call and the variable for its result,
    if the symbol is a function.
    """
    raise NeedSymbol(function.name)
def emulate_function(self, function, *args)

Emulate a function call to the given function, passing the given arguments. The method returns the return value of the emulated function call if it is not a procedure.

Expand source code Browse git
@EventCall.Wrap
def emulate_function(self, function: Function, *args):
    """
    Emulate a function call to the given function, passing the given arguments. The method
    returns the return value of the emulated function call if it is not a procedure.
    """
    return (yield from self._timed(self._execute(_ScriptRun(self.dialect), function, args)))
def TInputDirWizardPage__GetValues(self, this, k)

An emulated handler for the external symbol TInputDirWizardPage::GetValues.

Expand source code Browse git
@external()
def TInputDirWizardPage__GetValues(self, this: object, k: int) -> str:
    return F'$InputDir{k}'
def TInputFileWizardPage__GetValues(self, this, k)

An emulated handler for the external symbol TInputFileWizardPage::GetValues.

Expand source code Browse git
@external()
def TInputFileWizardPage__GetValues(self, this: object, k: int) -> str:
    return F'$InputFile{k}'
def TWizardForm__GetPasswordEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetPasswordEdit.

Expand source code Browse git
@external()
def TWizardForm__GetPasswordEdit(self, this: object) -> object:
    return self.wizard_form.password_box
def TWizardForm__GetDirEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetDirEdit.

Expand source code Browse git
@external()
def TWizardForm__GetDirEdit(self, this: object) -> object:
    return self.wizard_form.dir_edit
def TWizardForm__GetGroupEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetGroupEdit.

Expand source code Browse git
@external()
def TWizardForm__GetGroupEdit(self, this: object) -> object:
    return self.wizard_form.group_edit
def TWizardForm__GetUserInfoNameEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetUserInfoNameEdit.

Expand source code Browse git
@external()
def TWizardForm__GetUserInfoNameEdit(self, this: object) -> object:
    return self.wizard_form.user_info_name_edit
def TWizardForm__GetUserInfoOrgEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetUserInfoOrgEdit.

Expand source code Browse git
@external()
def TWizardForm__GetUserInfoOrgEdit(self, this: object) -> object:
    return self.wizard_form.user_info_org_edit
def TWizardForm__GetUserInfoSerialEdit(self, this)

An emulated handler for the external symbol TWizardForm::GetUserInfoSerialEdit.

Expand source code Browse git
@external()
def TWizardForm__GetUserInfoSerialEdit(self, this: object) -> object:
    return self.wizard_form.user_info_serial_edit
def TWizardForm__GetPrevAppDir(self, this)

An emulated handler for the external symbol TWizardForm::GetPrevAppDir.

Expand source code Browse git
@external()
def TWizardForm__GetPrevAppDir(self, this: object) -> str:
    return ''
def text_changed(self, box)

Runs the handler of a text box whose text changed, as the box's OnChange runs: a function of the script, or Setup's own handler. An error reaches the code that changed the text.

Expand source code Browse git
def text_changed(self, box: TextBox) -> Generator[Any, Any, None]:
    """
    Runs the handler of a text box whose text changed, as the box's `OnChange` runs: a function
    of the script, or Setup's own handler. An error reaches the code that changed the text.
    """
    handler = box.on_change
    if isinstance(handler, Function):
        yield from self.call_from_setup(handler, (box,), _NOTIFY_CALL)
    elif handler is SetupHandler.UserInfoEditChange:
        yield from user_info_edit_change(self, box)
def TEdit__GetText(self, this)

An emulated handler for the external symbol TEdit::GetText.

Expand source code Browse git
@external()
def TEdit__GetText(self, this: object) -> str:
    return self._box_text(this)
def TCustomEdit__GetText(self, this)

An emulated handler for the external symbol TCustomEdit::GetText.

Expand source code Browse git
@external()
def TCustomEdit__GetText(self, this: object) -> str:
    return self._box_text(this)
def TPasswordEdit__GetText(self, this)

An emulated handler for the external symbol TPasswordEdit::GetText.

Expand source code Browse git
@external()
def TPasswordEdit__GetText(self, this: object) -> str:
    return self._box_text(this)
def TEdit__SetText(self, this, value)

An emulated handler for the external symbol TEdit::SetText.

Expand source code Browse git
@external()
def TEdit__SetText(self, this: object, value: str):
    yield from self._set_box_text(this, value)
def TCustomEdit__SetText(self, this, value)

An emulated handler for the external symbol TCustomEdit::SetText.

Expand source code Browse git
@external()
def TCustomEdit__SetText(self, this: object, value: str):
    yield from self._set_box_text(this, value)
def TPasswordEdit__SetText(self, this, value)

An emulated handler for the external symbol TPasswordEdit::SetText.

Expand source code Browse git
@external()
def TPasswordEdit__SetText(self, this: object, value: str):
    yield from self._set_box_text(this, value)
    if value:
        yield NewPassword(value)
def TCustomEdit__Clear(self, this)

An emulated handler for the external symbol TCustomEdit::Clear.

Expand source code Browse git
@external()
def TCustomEdit__Clear(self, this: object):
    if isinstance(this, TextBox):
        this.clear()
        yield from self.text_changed(this)
def TCustomEdit__ClearSelection(self, this)

An emulated handler for the external symbol TCustomEdit::ClearSelection.

Expand source code Browse git
@external()
def TCustomEdit__ClearSelection(self, this: object):
    if isinstance(this, TextBox) and this.clear_selection():
        yield from self.text_changed(this)
def TCustomEdit__GetSelText(self, this)

An emulated handler for the external symbol TCustomEdit::GetSelText.

Expand source code Browse git
@external()
def TCustomEdit__GetSelText(self, this: object) -> str:
    return this.selected_text if isinstance(this, TextBox) else ''
def TCustomEdit__SetSelText(self, this, value)

An emulated handler for the external symbol TCustomEdit::SetSelText.

Expand source code Browse git
@external()
def TCustomEdit__SetSelText(self, this: object, value: str):
    yield from self._replace_box_selection(this, value)
def TCustomEdit__SelectAll(self, this)

An emulated handler for the external symbol TCustomEdit::SelectAll.

Expand source code Browse git
@external()
def TCustomEdit__SelectAll(self, this: object):
    if isinstance(this, TextBox):
        this.select(0, -1)
def TCustomEdit__GetSelStart(self, this)

An emulated handler for the external symbol TCustomEdit::GetSelStart.

Expand source code Browse git
@external()
def TCustomEdit__GetSelStart(self, this: object) -> int:
    return this.selection_start if isinstance(this, TextBox) else 0
def TCustomEdit__SetSelStart(self, this, value)

An emulated handler for the external symbol TCustomEdit::SetSelStart.

Expand source code Browse git
@external()
def TCustomEdit__SetSelStart(self, this: object, value: int):
    if isinstance(this, TextBox):
        this.select(value, value)
def TCustomEdit__GetSelLength(self, this)

An emulated handler for the external symbol TCustomEdit::GetSelLength.

Expand source code Browse git
@external()
def TCustomEdit__GetSelLength(self, this: object) -> int:
    return this.selection_length if isinstance(this, TextBox) else 0
def TCustomEdit__SetSelLength(self, this, value)

An emulated handler for the external symbol TCustomEdit::SetSelLength.

Expand source code Browse git
@external()
def TCustomEdit__SetSelLength(self, this: object, value: int):
    if isinstance(this, TextBox):
        start = this.selection_start
        this.select(start, start + value)
def TEdit__SetCharCase(self, this, value)

An emulated handler for the external symbol TEdit::SetCharCase.

Expand source code Browse git
@external()
def TEdit__SetCharCase(self, this: object, value: int):
    if isinstance(this, TextBox) and value in _EDIT_CHAR_CASES:
        this.set_case(TEditCharCase(value))
def TEdit__SetOnChange(self, this, handler)

An emulated handler for the external symbol TEdit::SetOnChange.

Expand source code Browse git
@external()
def TEdit__SetOnChange(self, this: object, handler: object):
    self._set_box_handler(this, handler)
def TPasswordEdit__SetOnChange(self, this, handler)

An emulated handler for the external symbol TPasswordEdit::SetOnChange.

Expand source code Browse git
@external()
def TPasswordEdit__SetOnChange(self, this: object, handler: object):
    self._set_box_handler(this, handler)
def shell32__ShellExecute(self, hwnd, operation, file, args, cwd, show)

An emulated handler for the external symbol shell32::ShellExecute.

Expand source code Browse git
@external(alias=['ShellExecuteA', 'ShellExecuteW'])
def shell32__ShellExecute(self, hwnd: int, operation: str, file: str, args: str, cwd: str, show: int) -> int:
    if file == self.config.executable:
        for arg in shlex.split(args):
            if arg.upper().startswith('/PASSWORD='):
                yield NewPassword(arg[10:])
    return 0x40
def kernel32__GetLastError(self)

An emulated handler for the external symbol kernel32::GetLastError.

Expand source code Browse git
@external()
def kernel32__GetLastError(self) -> int:
    return self._win32api_last_error
def kernel32__SetLastError(self, code)

An emulated handler for the external symbol kernel32::SetLastError.

Expand source code Browse git
@external()
def kernel32__SetLastError(self, code: int):
    self._win32api_last_error = code
def kernel32__CreateToolhelp32Snapshot(self, flags, process_id)

An emulated handler for the external symbol kernel32::CreateToolhelp32Snapshot.

Expand source code Browse git
@external()
def kernel32__CreateToolhelp32Snapshot(self, flags: int, process_id: int) -> int:
    if not self.config.allow_process_listing:
        return -1
    return random.randrange(0x10, 0x100000)
def kernel32__Process32First(self, handle, p_record)

An emulated handler for the external symbol kernel32::Process32First.

Expand source code Browse git
@external(alias=['Process32FirstW'])
def kernel32__Process32First(self, handle: int, p_record: Variable[list]) -> bool:
    if not (pids := self._win32api_sorted_process_ids):
        return False
    record = p_record.get()
    pid = pids[0]
    self._PROCESSENTRY32(record, pid)
    p_record.set(record)
    return True
def kernel32__LoadLibrary(self, name)

An emulated handler for the external symbol kernel32::LoadLibrary.

Expand source code Browse git
@external(alias=[
    'LoadLibraryW',
    'LoadLibraryA',
])
def kernel32__LoadLibrary(self, name: str) -> int:
    name = name.lower()
    base, dot, ext = name.rpartition('.')
    if dot and ext.lower() == 'dll':
        name = base
    return self.winapi_module_to_addr.get(name, 0)
def kernel32__GetProcAddress(self, module, name)

An emulated handler for the external symbol kernel32::GetProcAddress.

Expand source code Browse git
@external(alias=['GetProcAddressA'])
def kernel32__GetProcAddress(self, module: int, name: str) -> int:
    if module not in self.winapi_addr_to_module:
        return 0
    return self.winapi_symbol_to_addr.get(name, 0)
def kernel32__Process32Next(self, handle, p_record)

An emulated handler for the external symbol kernel32::Process32Next.

Expand source code Browse git
@external(alias=['Process32NextW'])
def kernel32__Process32Next(self, handle: int, p_record: Variable[list]) -> bool:
    record = p_record.get()
    pid = record[2]
    pids = self._win32api_sorted_process_ids
    k = bisect.bisect_right(pids, pid)
    if k < len(pids):
        pid = pids[k]
        self._PROCESSENTRY32(record, pid)
        p_record.set(record)
        return True
    else:
        self.kernel32__SetLastError(ERROR_NO_MORE_FILES)
        return False
def kernel32__GetTickCount(self)

An emulated handler for the external symbol kernel32::GetTickCount.

Expand source code Browse git
@external()
def kernel32__GetTickCount(self) -> int:
    tick = self.clock
    tick *= self.config.milliseconds_per_instruction
    tick += self.seconds_slept * 1000
    return int(tick)
def user32__GetSystemMetrics(self, index)

An emulated handler for the external symbol user32::GetSystemMetrics.

Expand source code Browse git
@external()
def user32__GetSystemMetrics(self, index: int) -> int:
    if index == 80:
        return 1
    if index == 43:
        return 2
    return 0
def IsArm32Compatible(self)

An emulated handler for the external symbol IsArm32Compatible.

Expand source code Browse git
@external()
def IsArm32Compatible(self) -> bool:
    return architecture('arm32compatible', self.config.x64)
def IsArm64(self)

An emulated handler for the external symbol IsArm64.

Expand source code Browse git
@external()
def IsArm64(self) -> bool:
    return architecture('arm64', self.config.x64)
def IsX64(self)

An emulated handler for the external symbol IsX64.

Expand source code Browse git
@external()
def IsX64(self) -> bool:
    return architecture('x64', self.config.x64)
def IsX64OS(self)

An emulated handler for the external symbol IsX64OS.

Expand source code Browse git
@external()
def IsX64OS(self) -> bool:
    return architecture('x64os', self.config.x64)
def IsX64Compatible(self)

An emulated handler for the external symbol IsX64Compatible.

Expand source code Browse git
@external()
def IsX64Compatible(self) -> bool:
    return architecture('x64compatible', self.config.x64)
def IsX86(self)

An emulated handler for the external symbol IsX86.

Expand source code Browse git
@external()
def IsX86(self) -> bool:
    return architecture('x86', self.config.x64)
def IsX86OS(self)

An emulated handler for the external symbol IsX86OS.

Expand source code Browse git
@external()
def IsX86OS(self) -> bool:
    return architecture('x86os', self.config.x64)
def IsX86Compatible(self)

An emulated handler for the external symbol IsX86Compatible.

Expand source code Browse git
@external()
def IsX86Compatible(self) -> bool:
    return architecture('x86compatible', self.config.x64)
def Terminated(self)

An emulated handler for the external symbol Terminated.

Expand source code Browse git
@external(alias=[
    'Debugging',
    'IsUninstaller',
])
def Terminated(self) -> bool:
    return False
def IsAdmin(self)

An emulated handler for the external symbol IsAdmin.

Expand source code Browse git
@external()
def IsAdmin(self) -> bool:
    return self.config.admin
def kernel32__Sleep(self, ms)

An emulated handler for the external symbol Sleep.

Expand source code Browse git
@external(name='Sleep')
@external()
def kernel32__Sleep(self, ms: int):
    seconds = ms / 1000.0
    self.seconds_slept += seconds
def Random(self, top)

An emulated handler for the external symbol Random.

Expand source code Browse git
@external()
def Random(self, top: int) -> int:
    return random.randrange(0, top)
def WStrGet(self, string, index)

An emulated handler for the external symbol WStrGet.

Expand source code Browse git
@external(alias='StrGet')
def WStrGet(self, string: Variable[str], index: int) -> str:
    text = string.get()
    if not 1 <= index <= len(text):
        raise IFPSException(
            _OUT_OF_STRING_RANGE,
            PSError.CustomError,
            failed_call=self.dialect is Dialect.Inno6,
        )
    return text[index - 1]
def WStrSet(self, char, index, dst)

An emulated handler for the external symbol WStrSet.

Expand source code Browse git
@external(alias='StrSet')
def WStrSet(self, char: str, index: int, dst: Variable[str]):
    old = dst.get()
    if not 1 <= index <= len(old):
        raise IFPSException(_OUT_OF_STRING_RANGE, PSError.CustomError)
    dst.set(old[:index - 1] + char + old[index:])
def GetEnv(self, name)

An emulated handler for the external symbol GetEnv.

Expand source code Browse git
@external()
def GetEnv(self, name: str) -> str:
    return self.config.environment.get(name, F'%{name}%')
def Beep(self)

An emulated handler for the external symbol Beep.

Expand source code Browse git
@external()
def Beep(self):
    pass
def Abort(self)

An emulated handler for the external symbol Abort.

Expand source code Browse git
@external()
def Abort(self):
    if self.config.throw_abort:
        raise AbortEmulation
    raise IFPSException(SysConst.SOperationAborted)
def DirExists(self, path)

An emulated handler for the external symbol DirExists.

Expand source code Browse git
@external()
def DirExists(self, path: str) -> bool:
    return self.installed.directory_exists(path)
def FileOrDirExists(self, path)

An emulated handler for the external symbol FileOrDirExists.

Expand source code Browse git
@external()
def FileOrDirExists(self, path: str) -> bool:
    return self.installed.exists(path)
def ForceDirectories(self, path)

An emulated handler for the external symbol ForceDirectories.

Expand source code Browse git
@external()
def ForceDirectories(self, path: str) -> bool:
    return True
def LoadStringFromFile(self, path, out)

An emulated handler for the external symbol LoadStringFromFile.

Expand source code Browse git
@external(alias='LoadStringFromLockedFile')
def LoadStringFromFile(self, path: str, out: Variable[str]) -> bool:
    return True
def LoadStringsFromFile(self, path, out)

An emulated handler for the external symbol LoadStringsFromFile.

Expand source code Browse git
@external(alias='LoadStringsFromLockedFile')
def LoadStringsFromFile(self, path: str, out: Variable[str]) -> bool:
    return True
def code_runner(self, caller)

Runs the function of a code constant inside the run of the caller, or in a new run of the script without one.

Expand source code Browse git
def code_runner(self, caller: _ScriptRun | None) -> constants.CodeRunner:
    """
    Runs the function of a code constant inside the run of the caller, or in a new run of the
    script without one.
    """
    def run_code(function: str, argument: str) -> Generator[Any, Any, str]:
        value = yield from self.call_by_name(function, [argument], CODE_CONSTANT, caller)
        return str(value)
    return run_code
def ExpandConstant(self, string, caller=None)

An emulated handler for the external symbol ExpandConstant.

Expand source code Browse git
@external(caller=True)
def ExpandConstant(self, string: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
    return self.constants.expand(string, self.code_runner(caller))
def ExpandConstantEx(self, string, custom_var, custom_val, caller=None)

An emulated handler for the external symbol ExpandConstantEx.

Expand source code Browse git
@external(caller=True)
def ExpandConstantEx(
    self,
    string: str,
    custom_var: str,
    custom_val: str,
    caller: _ScriptRun | None = None,
) -> Generator[Any, Any, str]:
    return self.constants.expand(string, self.code_runner(caller), {custom_var: custom_val})
def expand_constant(self, string, custom_var=None, custom_val='')

Expands the constants in the given string as Setup expands them, with one optional custom constant. A code constant runs a function of the script; its calls are not reported.

Expand source code Browse git
def expand_constant(self, string: str, custom_var: str | None = None, custom_val: str = '') -> str:
    """
    Expands the constants in the given string as Setup expands them, with one optional custom
    constant. A code constant runs a function of the script; its calls are not reported.
    """
    custom = None if custom_var is None else {custom_var: custom_val}
    expansion = EventCall(self._timed(self.constants.expand(string, self.code_runner(None), custom)))
    value = expansion.value
    assert value is not None
    return value
def DeleteFile(self, path)

An emulated handler for the external symbol DeleteFile.

Expand source code Browse git
@external()
def DeleteFile(self, path: str) -> bool:
    self.installed.remove(path)
    return True
def FileExists(self, file_name)

An emulated handler for the external symbol FileExists.

Expand source code Browse git
@external()
def FileExists(self, file_name: str) -> bool:
    return self.installed.file_exists(file_name)
def Log(self, log)

An emulated handler for the external symbol Log.

Expand source code Browse git
@external()
def Log(self, log: str):
    ...
def Inc(self, p)

An emulated handler for the external symbol Inc.

Expand source code Browse git
@external()
def Inc(self, p: Variable[int]):
    if not p.deref().type.code.integer:
        raise IFPSException(kind=PSError.CouldNotCallProc)
    p.set(p.get() + 1)
def Dec(self, p)

An emulated handler for the external symbol Dec.

Expand source code Browse git
@external()
def Dec(self, p: Variable[int]):
    if not p.deref().type.code.integer:
        raise IFPSException(kind=PSError.CouldNotCallProc)
    p.set(p.get() - 1)
def FindFirst(self, file_name, frec)

An emulated handler for the external symbol FindFirst.

Expand source code Browse git
@external()
def FindFirst(self, file_name: str, frec: Variable) -> bool:
    return False
def Round(self, x)

An emulated handler for the external symbol Round.

Expand source code Browse git
@external()
def Round(self, x: x87.Real) -> int:
    if self.setup.release.since((6, 6, 0)):
        return self.runtime.fpu.integer(x, x87.Rounding.Nearest)
    return self.runtime.fpu.integer(x)
def Trunc(self, x)

An emulated handler for the external symbol Trunc.

Expand source code Browse git
@external()
def Trunc(self, x: x87.Real) -> int:
    return self.runtime.fpu.integer(x, x87.Rounding.TowardZero)
def Int(self, x)

An emulated handler for the external symbol Int.

Expand source code Browse git
@external()
def Int(self, x: x87.Real) -> x87.Real:
    return self.runtime.fpu.round_integral(x, x87.Rounding.TowardZero)
def Abs(self, x)

An emulated handler for the external symbol Abs.

Expand source code Browse git
@external()
def Abs(self, x: x87.Real) -> x87.Real:
    return self.runtime.fpu.absolute(x)
def Pi(self)

An emulated handler for the external symbol Pi.

Expand source code Browse git
@external()
def Pi(self) -> x87.Real:
    return self.runtime.fpu.pi()
def Sqrt(self, x)

An emulated handler for the external symbol Sqrt.

Expand source code Browse git
@external()
def Sqrt(self, x: x87.Real) -> x87.Real:
    return self.runtime.fpu.square_root(x)
def Sin(self, x)

An emulated handler for the external symbol Sin.

Expand source code Browse git
@external()
def Sin(self, x: x87.Real) -> x87.Real:
    return self.runtime.fpu.sine(x)
def Cos(self, x)

An emulated handler for the external symbol Cos.

Expand source code Browse git
@external()
def Cos(self, x: x87.Real) -> x87.Real:
    return self.runtime.fpu.cosine(x)
def GetSpaceOnDisk(self, path, in_megabytes, avail, space)

An emulated handler for the external symbol GetSpaceOnDisk.

Expand source code Browse git
@external()
def GetSpaceOnDisk(
    self,
    path: str,
    in_megabytes: bool,
    avail: Variable[int],
    space: Variable[int],
) -> bool:
    _a = 3_000_000
    _t = 5_000_000
    if not in_megabytes:
        _a *= 1000
        _t *= 1000
    avail.set(_a)
    space.set(_t)
    return True
def GetSpaceOnDisk64(self, path, avail, space)

An emulated handler for the external symbol GetSpaceOnDisk64.

Expand source code Browse git
@external()
def GetSpaceOnDisk64(
    self,
    path: str,
    avail: Variable[int],
    space: Variable[int],
) -> bool:
    avail.set(3_000_000_000)
    space.set(5_000_000_000)
    return True
def Exec(self, exe, cmd, cwd, show, wait, out)

An emulated handler for the external symbol Exec.

Expand source code Browse git
@external()
def Exec(
    self,
    exe: str,
    cmd: str,
    cwd: str,
    show: int,
    wait: int,
    out: Variable[int],
) -> bool:
    out.set(0)
    return True
def GetCmdTail(self)

An emulated handler for the external symbol GetCmdTail.

Expand source code Browse git
@external()
def GetCmdTail(self) -> str:
    return ''
def ParamCount(self)

An emulated handler for the external symbol ParamCount.

Expand source code Browse git
@external()
def ParamCount(self) -> int:
    return 0
def ParamStr(self, index)

An emulated handler for the external symbol ParamStr.

Expand source code Browse git
@external()
def ParamStr(self, index: int) -> str:
    return ''
def ActiveLanguage(self)

An emulated handler for the external symbol ActiveLanguage.

Expand source code Browse git
@external()
def ActiveLanguage(self) -> str:
    return self.setup.language_name
def CustomMessage(self, msg_name)

An emulated handler for the external symbol CustomMessage.

Expand source code Browse git
@external()
def CustomMessage(self, msg_name: str) -> str:
    if (message := self.setup.custom_message(msg_name)) is None:
        raise InternalError(F'Unknown custom message name "{msg_name}"')
    return message
def FmtMessage(self, fmt, args)

An emulated handler for the external symbol FmtMessage.

Expand source code Browse git
@external()
def FmtMessage(self, fmt: str, args: list[str]) -> str:
    return constants.fmt_message(fmt, args)
def Format(self, fmt, args)

An emulated handler for the external symbol Format.

Expand source code Browse git
@external()
def Format(self, fmt: str, args: Variable[list]) -> str:
    elements = args.deref()
    arguments = [_open_array_element(elements.at(k)) for k in range(len(elements))]
    return formatting.format(fmt, arguments, self.runtime, self._reserve_text)
def SetupMessage(self, id)

An emulated handler for the external symbol SetupMessage.

Expand source code Browse git
@external()
def SetupMessage(self, id: int) -> str:
    return self.setup.message(id)
def IsWin64(self)

An emulated handler for the external symbol IsWin64.

Expand source code Browse git
@external()
def IsWin64(self) -> bool:
    return self.config.x64
def Is64BitInstallMode(self)

An emulated handler for the external symbol Is64BitInstallMode.

Expand source code Browse git
@external()
def Is64BitInstallMode(self) -> bool:
    return self.setup.install_mode.decided()
def RaiseException(self, msg)

An emulated handler for the external symbol RaiseException.

Expand source code Browse git
@external()
def RaiseException(self, msg: str):
    raise IFPSException(msg)
def GetExceptionMessage(self, caller=None)

An emulated handler for the external symbol GetExceptionMessage.

Expand source code Browse git
@external(caller=True)
def GetExceptionMessage(self, caller: _ScriptRun | None = None) -> str:
    if caller is None or (error := caller.current_exception) is None:
        return '(There is no current exception)'
    return error.message
def ProcessorArchitecture(self)

An emulated handler for the external symbol ProcessorArchitecture.

Expand source code Browse git
@external()
def ProcessorArchitecture(self) -> int:
    return processor(self.config.x64).value
def GetUserNameString(self)

An emulated handler for the external symbol GetUserNameString.

Expand source code Browse git
@external()
def GetUserNameString(self) -> str:
    return self.config.user_name
def GetComputerNameString(self)

An emulated handler for the external symbol GetComputerNameString.

Expand source code Browse git
@external()
def GetComputerNameString(self) -> str:
    return self.config.host_name
def GetUILanguage(self)

An emulated handler for the external symbol GetUILanguage.

Expand source code Browse git
@external()
def GetUILanguage(self) -> int:
    return self.config.lcid
def GetArrayLength(self, array)

An emulated handler for the external symbol GetArrayLength.

Expand source code Browse git
@external()
def GetArrayLength(self, array: Variable) -> int:
    array = array.deref()
    return len(array)
def SetArrayLength(self, array, n)

An emulated handler for the external symbol SetArrayLength.

Expand source code Browse git
@external()
def SetArrayLength(self, array: Variable, n: int):
    array = array.deref()
    if isinstance(t := array.type, TArray):
        self._reserve_variables(t.type, n)
    array.resize(n)
def WizardForm(self)

An emulated handler for the external symbol WizardForm.

Expand source code Browse git
@external()
def WizardForm(self) -> object:
    return self._form()
def MainForm(self)

An emulated handler for the external symbol MainForm.

Expand source code Browse git
@external()
def MainForm(self) -> object:
    """
    The function of Inno Setup 5 that gives Setup's main form; before Setup created the form,
    it raises an internal error.
    """
    if (main := self.wizard_form.main) is None:
        raise InternalError('An attempt was made to access MainForm before it has been created')
    return main
def CreateCustomPage(self, after, caption, description)

An emulated handler for the external symbol CreateCustomPage.

Expand source code Browse git
@external()
def CreateCustomPage(self, after: int, caption: str, description: str) -> object:
    return self._form().add_page(after)
def CreateInputQueryPage(self, after, caption, description, subcaption)

An emulated handler for the external symbol CreateInputQueryPage.

Expand source code Browse git
@external()
def CreateInputQueryPage(self, after: int, caption: str, description: str, subcaption: str) -> object:
    return self._form().add_page(after)
def CreateInputOptionPage(self, after, caption, description, subcaption, exclusive, listbox)

An emulated handler for the external symbol CreateInputOptionPage.

Expand source code Browse git
@external()
def CreateInputOptionPage(
    self,
    after: int,
    caption: str,
    description: str,
    subcaption: str,
    exclusive: bool,
    listbox: bool,
) -> object:
    return self._form().add_page(after)
def CreateInputDirPage(self, after, caption, description, subcaption, append, folder)

An emulated handler for the external symbol CreateInputDirPage.

Expand source code Browse git
@external()
def CreateInputDirPage(
    self,
    after: int,
    caption: str,
    description: str,
    subcaption: str,
    append: bool,
    folder: str,
) -> object:
    return self._form().add_page(after)
def CreateInputFilePage(self, after, caption, description, subcaption)

An emulated handler for the external symbol CreateInputFilePage.

Expand source code Browse git
@external()
def CreateInputFilePage(self, after: int, caption: str, description: str, subcaption: str) -> object:
    return self._form().add_page(after)
def CreateOutputMsgPage(self, after, caption, description, message)

An emulated handler for the external symbol CreateOutputMsgPage.

Expand source code Browse git
@external()
def CreateOutputMsgPage(self, after: int, caption: str, description: str, message: str) -> object:
    return self._form().add_page(after)
def CreateOutputMsgMemoPage(self, after, caption, description, subcaption, message)

An emulated handler for the external symbol CreateOutputMsgMemoPage.

Expand source code Browse git
@external()
def CreateOutputMsgMemoPage(
    self,
    after: int,
    caption: str,
    description: str,
    subcaption: str,
    message: str,
) -> object:
    return self._form().add_page(after)
def CreateOutputProgressPage(self, caption, description)

An emulated handler for the external symbol CreateOutputProgressPage.

Expand source code Browse git
@external(alias='CreateOutputMarqueeProgressPage')
def CreateOutputProgressPage(self, caption: str, description: str) -> object:
    return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)
def CreateDownloadPage(self, caption, description, progress)

An emulated handler for the external symbol CreateDownloadPage.

Expand source code Browse git
@external(alias='CreateExtractionPage')
def CreateDownloadPage(self, caption: str, description: str, progress: object) -> object:
    return self._form().add_page(-1, PageStyle.AlwaysSkip | PageStyle.NoButtons)
def TWizardPage__GetID(self, this)

An emulated handler for the external symbol TWizardPage::GetID.

Expand source code Browse git
@external()
def TWizardPage__GetID(self, this: object) -> int:
    return _wizard_page(this).id
def Class__SetNil(self, value)

An emulated handler for the external symbol Class::SetNil.

Expand source code Browse git
@external()
def Class__SetNil(self, value: object) -> object:
    """
    Pascal Script's assignment of nil to a variable of a class.
    """
    return None
def Class__CastToType(self, source)

An emulated handler for the external symbol Class::CastToType.

Expand source code Browse git
@external()
def Class__CastToType(self, source: object) -> object:
    """
    A cast of an object to a class; the emulator's objects do not record the classes that the
    script knows, and the cast keeps the object.
    """
    return source
def GetWizardForm(self)

An emulated handler for the external symbol GetWizardForm.

Expand source code Browse git
@external()
def GetWizardForm(self) -> object:
    return self._form()
def PageFromID(self, id)

An emulated handler for the external symbol PageFromID.

Expand source code Browse git
@external()
def PageFromID(self, id: int) -> object:
    return self._form().page(id)
def PageIndexFromID(self, id)

An emulated handler for the external symbol PageIndexFromID.

Expand source code Browse git
@external()
def PageIndexFromID(self, id: int) -> int:
    return self._form().page_index(id)
def WizardDirValue(self)

An emulated handler for the external symbol WizardDirValue.

Expand source code Browse git
@external()
def WizardDirValue(self) -> str:
    return self.setup.path_func.remove_backslash_unless_root(self._form().dir_edit.text)
def WizardGroupValue(self)

An emulated handler for the external symbol WizardGroupValue.

Expand source code Browse git
@external()
def WizardGroupValue(self) -> str:
    return self.setup.path_func.remove_backslash_unless_root(self._form().group_edit.text)
def IsAdminInstallMode(self)

An emulated handler for the external symbol IsAdminInstallMode.

Expand source code Browse git
@external()
def IsAdminInstallMode(self) -> bool:
    return self.setup.admin_install_mode
def GetPreviousData(self, name, default, caller=None)

An emulated handler for the external symbol GetPreviousData.

Expand source code Browse git
@external(caller=True)
def GetPreviousData(self, name: str, default: str, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
    app_id = '' if (info := self.setup_info) is None else info.Header.text(TextDirective.AppId) or ''
    yield from self.constants.expand(app_id, self.code_runner(caller))
    return default
def TWizardForm__GetCurPageID(self, this)

An emulated handler for the external symbol TWizardForm::GetCurPageID.

Expand source code Browse git
@external()
def TWizardForm__GetCurPageID(self, this: object) -> int:
    return self.wizard_form.current
def TWizardPage__SetOnActivate(self, this, handler)

An emulated handler for the external symbol TWizardPage::SetOnActivate.

Expand source code Browse git
@external()
def TWizardPage__SetOnActivate(self, this: object, handler: object):
    self._set_page_handler(this, PageEvent.OnActivate, handler)
def TWizardPage__SetOnShouldSkipPage(self, this, handler)

An emulated handler for the external symbol TWizardPage::SetOnShouldSkipPage.

Expand source code Browse git
@external()
def TWizardPage__SetOnShouldSkipPage(self, this: object, handler: object):
    self._set_page_handler(this, PageEvent.OnShouldSkipPage, handler)
def TWizardPage__SetOnNextButtonClick(self, this, handler)

An emulated handler for the external symbol TWizardPage::SetOnNextButtonClick.

Expand source code Browse git
@external()
def TWizardPage__SetOnNextButtonClick(self, this: object, handler: object):
    self._set_page_handler(this, PageEvent.OnNextButtonClick, handler)
def TWizardPage__SetOnBackButtonClick(self, this, handler)

An emulated handler for the external symbol TWizardPage::SetOnBackButtonClick.

Expand source code Browse git
@external()
def TWizardPage__SetOnBackButtonClick(self, this: object, handler: object):
    self._set_page_handler(this, PageEvent.OnBackButtonClick, handler)
def TWizardPage__SetOnCancelButtonClick(self, this, handler)

An emulated handler for the external symbol TWizardPage::SetOnCancelButtonClick.

Expand source code Browse git
@external()
def TWizardPage__SetOnCancelButtonClick(self, this: object, handler: object):
    self._set_page_handler(this, PageEvent.OnCancelButtonClick, handler)
def Unassigned(self)

An emulated handler for the external symbol Unassigned.

Expand source code Browse git
@external()
def Unassigned(self) -> Variant:
    return variants.EMPTY
def Null(self)

An emulated handler for the external symbol Null.

Expand source code Browse git
@external()
def Null(self) -> Variant:
    return variants.NULL
def VarType(self, v)

An emulated handler for the external symbol VarType.

Expand source code Browse git
@external()
def VarType(self, v: Variant) -> int:
    return v.type
def VarIsNull(self, v)

An emulated handler for the external symbol VarIsNull.

Expand source code Browse git
@external()
def VarIsNull(self, v: Variant) -> bool:
    return v.type == VarType.Null
def VarIsEmpty(self, v)

An emulated handler for the external symbol VarIsEmpty.

Expand source code Browse git
@external()
def VarIsEmpty(self, v: Variant) -> bool:
    return v.type == VarType.Empty
def VarIsClear(self, v)

An emulated handler for the external symbol VarIsClear.

Expand source code Browse git
@external()
def VarIsClear(self, v: Variant) -> bool:
    return v.type == VarType.Empty or v.type in (VarType.Dispatch, VarType.Unknown) and v.value is None
def Set8087CW(self, cw)

An emulated handler for the external symbol Set8087CW.

Expand source code Browse git
@external()
def Set8087CW(self, cw: int):
    self.runtime.control_word = cw & 0xFFFF
def Get8087CW(self)

An emulated handler for the external symbol Get8087CW.

Expand source code Browse git
@external()
def Get8087CW(self) -> int:
    return self.runtime.control_word | x87.RESERVED_BIT
def GetDateTimeString(self, fmt, date_separator, time_separator)

An emulated handler for the external symbol GetDateTimeString.

Expand source code Browse git
@external()
def GetDateTimeString(
    self,
    fmt: str,
    date_separator: str,
    time_separator: str,
) -> str:

    now = self.config.start_time
    now = now + timedelta(
        milliseconds=(self.config.milliseconds_per_instruction * self.clock))
    now = now + timedelta(seconds=self.seconds_slept)

    date_separator = date_separator.lstrip('\0')
    time_separator = time_separator.lstrip('\0')

    def dt(m: re.Match[str]):
        spec = m[1]
        ampm = m[2]
        if ampm:
            am, _, pm = ampm.partition('/')
            spec = spec.upper()
            suffix = now.strftime('%p').lower()
            suffix = {'am': am, 'pm': pm}[suffix]
        else:
            suffix = ''
        if spec == 'dddddd' or spec == 'ddddd':
            return now.date().isoformat()
        if spec == 't':
            return now.time().isoformat('minutes')
        if spec == 'tt':
            return now.time().isoformat('seconds')
        if spec == 'd':
            return str(now.day)
        if spec == 'm':
            return str(now.month)
        if spec == 'h':
            return str(now.hour)
        if spec == 'n':
            return str(now.minute)
        if spec == 's':
            return str(now.second)
        if spec == 'H':
            return now.strftime('%I').lstrip('0') + suffix
        if spec == '/':
            return date_separator or spec
        if spec == ':':
            return time_separator or spec
        return now.strftime({
            'dddd'  : '%A',
            'ddd'   : '%a',
            'dd'    : '%d',
            'mmmm'  : '%B',
            'mmm'   : '%b',
            'mm'    : '%m',
            'yyyy'  : '%Y',
            'yy'    : '%y',
            'hh'    : '%H',
            'HH'    : '%I' + suffix,
            'nn'    : '%M',
            'ss'    : '%S',
        }.get(spec, m[0]))

    split = re.split(F'({formats.string!s})', fmt)
    for k in range(0, len(split), 2):
        split[k] = re.sub('(d+|m+|y+|h+|n+|s+|t+)((?:[aA][mM]?/[pP][mM]?)?)', dt, split[k])
    for k in range(1, len(split), 2):
        split[k] = split[k][1:-1]
    return ''.join(split)
def Chr(self, b)

An emulated handler for the external symbol Chr.

Expand source code Browse git
@external()
def Chr(self, b: int) -> str:
    return chr(b)
def Ord(self, c)

An emulated handler for the external symbol Ord.

Expand source code Browse git
@external()
def Ord(self, c: str) -> int:
    return ord(c)
def Copy(self, string, index, count)

An emulated handler for the external symbol Copy.

Expand source code Browse git
@external()
def Copy(self, string: str, index: int, count: int) -> str:
    index -= 1
    return string[index:index + count]
def Length(self, string)

An emulated handler for the external symbol Length.

Expand source code Browse git
@external()
def Length(self, string: str) -> int:
    return len(string)
def Lowercase(self, string)

An emulated handler for the external symbol Lowercase.

Expand source code Browse git
@external(alias='AnsiLowercase')
def Lowercase(self, string: str) -> str:
    return string.lower()
def Uppercase(self, string)

An emulated handler for the external symbol Uppercase.

Expand source code Browse git
@external(alias='AnsiUppercase')
def Uppercase(self, string: str) -> str:
    return string.upper()
def StringOfChar(self, c, count)

An emulated handler for the external symbol StringOfChar.

Expand source code Browse git
@external(alias='Replicate')
def StringOfChar(self, c: str, count: int) -> str:
    self._reserve_text(count, wide=False)
    return c * count
def Delete(self, string, index, count)

An emulated handler for the external symbol Delete.

Expand source code Browse git
@external()
def Delete(self, string: Variable[str], index: int, count: int):
    index -= 1
    old = string.get()
    string.set(old[:index] + old[index + count:])
def Insert(self, string, dest, index)

An emulated handler for the external symbol Insert.

Expand source code Browse git
@external()
def Insert(self, string: str, dest: Variable[str], index: int):
    index -= 1
    old = dest.get()
    dest.set(old[:index] + string + old[index:])
def StringChange(self, string, old, new)

An emulated handler for the external symbol StringChange.

Expand source code Browse git
@external()
def StringChange(self, string: Variable[str], old: str, new: str) -> int:
    return self.StringChangeEx(string, old, new, False)
def StringChangeEx(self, string, old, new, _)

An emulated handler for the external symbol StringChangeEx.

Expand source code Browse git
@external()
def StringChangeEx(self, string: Variable[str], old: str, new: str, _: bool) -> int:
    haystack = string.get()
    count = haystack.count(old)
    self._reserve_text(len(haystack) + count * (len(new) - len(old)))
    string.set(haystack.replace(old, new))
    return count
def Pos(self, sub, string)

An emulated handler for the external symbol Pos.

Expand source code Browse git
@external()
def Pos(self, sub: str, string: str) -> int:
    if not sub:
        return 0
    return string.find(sub) + 1
def AddQuotes(self, string)

An emulated handler for the external symbol AddQuotes.

Expand source code Browse git
@external()
def AddQuotes(self, string: str) -> str:
    if string and (string[0] != '"' or string[~0] != '"') and ' ' in string:
        string = F'"{string}"'
    return string
def RemoveQuotes(self, string)

An emulated handler for the external symbol RemoveQuotes.

Expand source code Browse git
@external()
def RemoveQuotes(self, string: str) -> str:
    if string and string[0] == '"' and string[~0] == '"':
        string = string[1:-1]
    return string
def CompareText(self, a, b)

An emulated handler for the external symbol CompareText.

Expand source code Browse git
@external()
def CompareText(self, a: str, b: str) -> int:
    return self.CompareStr(a.casefold(), b.casefold())
def CompareStr(self, a, b)

An emulated handler for the external symbol CompareStr.

Expand source code Browse git
@external()
def CompareStr(self, a: str, b: str) -> int:
    if a > b:
        return +1
    if a < b:
        return -1
    return 0
def SameText(self, a, b)

An emulated handler for the external symbol SameText.

Expand source code Browse git
@external()
def SameText(self, a: str, b: str) -> bool:
    return a.casefold() == b.casefold()
def SameStr(self, a, b)

An emulated handler for the external symbol SameStr.

Expand source code Browse git
@external()
def SameStr(self, a: str, b: str) -> bool:
    return a == b
def IsWildcard(self, pattern)

An emulated handler for the external symbol IsWildcard.

Expand source code Browse git
@external()
def IsWildcard(self, pattern: str) -> bool:
    return '*' in pattern or '?' in pattern
def WildcardMatch(self, text, pattern)

An emulated handler for the external symbol WildcardMatch.

Expand source code Browse git
@external()
def WildcardMatch(self, text: str, pattern: str) -> bool:
    return fnmatch.fnmatch(text, pattern)
def Trim(self, string)

An emulated handler for the external symbol Trim.

Expand source code Browse git
@external()
def Trim(self, string: str) -> str:
    return strings.trim(string)
def TrimLeft(self, string)

An emulated handler for the external symbol TrimLeft.

Expand source code Browse git
@external()
def TrimLeft(self, string: str) -> str:
    return strings.trim_left(string)
def TrimRight(self, string)

An emulated handler for the external symbol TrimRight.

Expand source code Browse git
@external()
def TrimRight(self, string: str) -> str:
    return strings.trim_right(string)
def StringJoin(self, sep, values)

An emulated handler for the external symbol StringJoin.

Expand source code Browse git
@external()
def StringJoin(self, sep: str, values: list[str]) -> str:
    self._reserve_text(sum(map(len, values)) + len(sep) * max(len(values) - 1, 0))
    return sep.join(values)
def StringSplitEx(self, string, separators, quote, how)

An emulated handler for the external symbol StringSplitEx.

Expand source code Browse git
@external()
def StringSplitEx(self, string: str, separators: list[str], quote: str, how: TSplitType) -> list[str]:
    def spans():
        found = _split_spans(string, separators, quote)
        if how == TSplitType.stExcludeEmpty:
            return (span for span in found if span[0] < span[1])
        if how == TSplitType.stExcludeLastEmpty:
            return _without_empty_last(found)
        return found
    self._reserve_variables(_StringType, sum(1 for _ in spans()))
    return [string[start:end] for start, end in spans()]
def StringSplit(self, string, separators, how)

An emulated handler for the external symbol StringSplit.

Expand source code Browse git
@external()
def StringSplit(self, string: str, separators: list[str], how: TSplitType) -> list[str]:
    return self.StringSplitEx(string, separators, '', how)
def StrToInt(self, s)

An emulated handler for the external symbol StrToInt.

Expand source code Browse git
@external()
def StrToInt(self, s: str) -> int:
    value, error = numbers.val_integer(s, 32)
    return -1 if error else value
def StrToIntDef(self, s, d)

An emulated handler for the external symbol StrToIntDef.

Expand source code Browse git
@external()
def StrToIntDef(self, s: str, d: int) -> int:
    value, error = numbers.val_integer(s, 32)
    return d if error else value
def StrToInt64(self, s)

An emulated handler for the external symbol StrToInt64.

Expand source code Browse git
@external()
def StrToInt64(self, s: str) -> int:
    return numbers.str_to_int64(s)
def StrToInt64Def(self, s, d)

An emulated handler for the external symbol StrToInt64Def.

Expand source code Browse git
@external()
def StrToInt64Def(self, s: str, d: int) -> int:
    return numbers.str_to_int64_default(s, d)
def StrToFloat(self, s)

An emulated handler for the external symbol StrToFloat.

Expand source code Browse git
@external()
def StrToFloat(self, s: str) -> x87.Real:
    runtime = self.runtime
    value, error = numbers.val_float(s, runtime.library)
    if error or value is None:
        raise IFPSException(_INVALID_FLOAT)
    return runtime.fpu.store(value, x87.Format.Extended)
def FloatToStr(self, e)

An emulated handler for the external symbol FloatToStr.

Expand source code Browse git
@external()
def FloatToStr(self, e: x87.Real) -> str:
    if self.dialect is Dialect.Inno6:
        return numbers.str_fixed(e, 12, self.runtime.library)
    return numbers.float_to_str(e, self.runtime)
def StrToUInt64(self, s)

An emulated handler for the external symbol StrToUInt64.

Expand source code Browse git
@external()
def StrToUInt64(self, s: str) -> int:
    return numbers.str_to_uint64(s)
def StrToUInt64Def(self, s, d)

An emulated handler for the external symbol StrToUInt64Def.

Expand source code Browse git
@external()
def StrToUInt64Def(self, s: str, d: int) -> int:
    return numbers.str_to_uint64_default(s, d)
def UIntToStr(self, i)

An emulated handler for the external symbol UIntToStr.

Expand source code Browse git
@external(alias='UInt64ToStr')
def UIntToStr(self, i: int) -> str:
    return str(i & 0xFFFFFFFFFFFFFFFF)
def IntToStr(self, i)

An emulated handler for the external symbol IntToStr.

Expand source code Browse git
@external(alias='Int64ToStr')
def IntToStr(self, i: int) -> str:
    return str(i)
def PadL(self, s, n)

An emulated handler for the external symbol PadL.

Expand source code Browse git
@external()
def PadL(self, s: str, n: int) -> str:
    self._reserve_text(n)
    return s.rjust(n)
def PadR(self, s, n)

An emulated handler for the external symbol PadR.

Expand source code Browse git
@external()
def PadR(self, s: str, n: int) -> str:
    self._reserve_text(n)
    return s.ljust(n)
def PadZ(self, s, n)

An emulated handler for the external symbol PadZ.

Expand source code Browse git
@external()
def PadZ(self, s: str, n: int) -> str:
    self._reserve_text(n)
    return s.rjust(n, '0')
def StrToVersion(self, s, v)

An emulated handler for the external symbol StrToVersion.

Expand source code Browse git
@external()
def StrToVersion(self, s: str, v: Variable[int]) -> bool:
    try:
        packed = bytes(map(int, s.split('.')))
    except Exception:
        return False
    if len(packed) != 4:
        return False
    v.set(int.from_bytes(packed, 'little'))
    return True
def CharLength(self, string, index)

An emulated handler for the external symbol CharLength.

Expand source code Browse git
@external()
def CharLength(self, string: str, index: int) -> int:
    return 1
def AddBackslash(self, string)

An emulated handler for the external symbol AddBackslash.

Expand source code Browse git
@external()
def AddBackslash(self, string: str) -> str:
    return self.setup.path_func.add_backslash(string)
def AddPeriod(self, string)

An emulated handler for the external symbol AddPeriod.

Expand source code Browse git
@external()
def AddPeriod(self, string: str) -> str:
    if string and string[~0] != '.':
        string = F'{string}.'
    return string
def RemoveBackslashUnlessRoot(self, string)

An emulated handler for the external symbol RemoveBackslashUnlessRoot.

Expand source code Browse git
@external()
def RemoveBackslashUnlessRoot(self, string: str) -> str:
    return self.setup.path_func.remove_backslash_unless_root(string)
def RemoveBackslash(self, string)

An emulated handler for the external symbol RemoveBackslash.

Expand source code Browse git
@external()
def RemoveBackslash(self, string: str) -> str:
    return self.setup.path_func.remove_backslash(string)
def ChangeFileExt(self, name, ext)

An emulated handler for the external symbol ChangeFileExt.

Expand source code Browse git
@external()
def ChangeFileExt(self, name: str, ext: str) -> str:
    return self.setup.path_func.change_ext(name, ext)
def ExtractFileExt(self, name)

An emulated handler for the external symbol ExtractFileExt.

Expand source code Browse git
@external()
def ExtractFileExt(self, name: str) -> str:
    return self.setup.path_func.extract_ext(name)
def ExtractFileDir(self, name)

An emulated handler for the external symbol ExtractFileDir.

Expand source code Browse git
@external()
def ExtractFileDir(self, name: str) -> str:
    return self.setup.path_func.extract_dir(name)
def ExtractFilePath(self, name)

An emulated handler for the external symbol ExtractFilePath.

Expand source code Browse git
@external()
def ExtractFilePath(self, name: str) -> str:
    return self.setup.path_func.extract_path(name)
def ExtractFileName(self, name)

An emulated handler for the external symbol ExtractFileName.

Expand source code Browse git
@external()
def ExtractFileName(self, name: str) -> str:
    return self.setup.path_func.extract_name(name)
def ExtractFileDrive(self, name)

An emulated handler for the external symbol ExtractFileDrive.

Expand source code Browse git
@external()
def ExtractFileDrive(self, name: str) -> str:
    return self.setup.path_func.extract_drive(name)
def ExtractRelativePath(self, base, dst)

An emulated handler for the external symbol ExtractRelativePath.

Expand source code Browse git
@external()
def ExtractRelativePath(self, base: str, dst: str) -> str:
    return self.setup.path_func.extract_relative_path(base, dst)
def ExpandFileName(self, name)

An emulated handler for the external symbol ExpandFileName.

Expand source code Browse git
@external()
def ExpandFileName(self, name: str) -> str:
    try:
        return self.setup.path_func.expand(name, self.current_folder.path)
    except PathFuncError as error:
        raise DelphiException(str(error)) from error
def ExpandUNCFileName(self, name)

An emulated handler for the external symbol ExpandUNCFileName.

Expand source code Browse git
@external()
def ExpandUNCFileName(self, name: str) -> str:
    return expand_file_name(name, self.current_folder.path)
def GetCurrentDir(self)

An emulated handler for the external symbol GetCurrentDir.

Expand source code Browse git
@external()
def GetCurrentDir(self) -> str:
    return self.current_folder.path
def SetLength(self, target, size)

An emulated handler for the external symbol SetLength.

Expand source code Browse git
@external()
def SetLength(self, target: Variable, size: int):
    target = target.deref()
    if target.container:
        self.SetArrayLength(target, size)
        return
    size = max(size, 0)
    self._reserve_text(size, wide=target.type.code not in (TC.AnsiString, TC.PChar))
    old = target.get()
    old = old.ljust(size, '\0')
    target.set(old[:size])
def CharToOemBuff(self, string)

An emulated handler for the external symbol CharToOemBuff.

Expand source code Browse git
@external(alias='OemToCharBuff')
def CharToOemBuff(self, string: Variable[str]):
    # TODO
    ...
def Utf8Encode(self, string)

An emulated handler for the external symbol Utf8Encode.

Expand source code Browse git
@external()
def Utf8Encode(self, string: str) -> str:
    return string.encode('utf8').decode('latin1')
def Utf8Decode(self, string)

An emulated handler for the external symbol Utf8Decode.

Expand source code Browse git
@external()
def Utf8Decode(self, string: str) -> str:
    return string.encode('latin1').decode('utf8')
def GetMD5OfString(self, string)

An emulated handler for the external symbol GetMD5OfString.

Expand source code Browse git
@external()
def GetMD5OfString(self, string: str) -> str:
    return hashlib.md5(string.encode('latin1')).hexdigest()
def GetMD5OfUnicodeString(self, string)

An emulated handler for the external symbol GetMD5OfUnicodeString.

Expand source code Browse git
@external()
def GetMD5OfUnicodeString(self, string: str) -> str:
    return hashlib.md5(string.encode('utf8')).hexdigest()
def GetSHA1OfString(self, string)

An emulated handler for the external symbol GetSHA1OfString.

Expand source code Browse git
@external()
def GetSHA1OfString(self, string: str) -> str:
    return hashlib.sha1(string.encode('latin1')).hexdigest()
def GetSHA1OfUnicodeString(self, string)

An emulated handler for the external symbol GetSHA1OfUnicodeString.

Expand source code Browse git
@external()
def GetSHA1OfUnicodeString(self, string: str) -> str:
    return hashlib.sha1(string.encode('utf8')).hexdigest()
def GetSHA256OfString(self, string)

An emulated handler for the external symbol GetSHA256OfString.

Expand source code Browse git
@external()
def GetSHA256OfString(self, string: str) -> str:
    return hashlib.sha256(string.encode('latin1')).hexdigest()
def GetSHA256OfUnicodeString(self, string)

An emulated handler for the external symbol GetSHA256OfUnicodeString.

Expand source code Browse git
@external()
def GetSHA256OfUnicodeString(self, string: str) -> str:
    return hashlib.sha256(string.encode('utf8')).hexdigest()
def SysErrorMessage(self, code)

An emulated handler for the external symbol SysErrorMessage.

Expand source code Browse git
@external()
def SysErrorMessage(self, code: int) -> str:
    return F'[description for error {code & 0xFFFFFFFF:08X}]'
def MinimizePathName(self, path, font, max_len)

An emulated handler for the external symbol MinimizePathName.

Expand source code Browse git
@external()
def MinimizePathName(self, path: str, font: object, max_len: int) -> str:
    return path
def CheckForMutexes(self, mutexes)

An emulated handler for the external symbol CheckForMutexes.

Expand source code Browse git
@external()
def CheckForMutexes(self, mutexes: str) -> bool:
    return any(m in self.mutexes for m in mutexes.split(','))
def CreateMutex(self, name)

An emulated handler for the external symbol CreateMutex.

Expand source code Browse git
@external()
def CreateMutex(self, name: str):
    if self.config.log_mutexes:
        yield NewMutex(name)
    self.mutexes.add(name)
def GetWinDir(self)

An emulated handler for the external symbol GetWinDir.

Expand source code Browse git
@external()
def GetWinDir(self) -> str:
    return self.expand_constant('{win}')
def GetSystemDir(self)

An emulated handler for the external symbol GetSystemDir.

Expand source code Browse git
@external()
def GetSystemDir(self) -> str:
    return self.expand_constant('{sys}')
def GetWindowsVersion(self)

An emulated handler for the external symbol GetWindowsVersion.

Expand source code Browse git
@external()
def GetWindowsVersion(self) -> int:
    version = int.from_bytes(
        struct.pack('>BBH', *self.config.windows_os_version), 'big')
    return version
def GetWindowsVersionEx(self, tv)

An emulated handler for the external symbol GetWindowsVersionEx.

Expand source code Browse git
@external()
def GetWindowsVersionEx(self, tv: Variable[int | bool]):
    tv[0], tv[1], tv[2] = self.config.windows_os_version # noqa
    tv[3], tv[4]        = self.config.windows_sp_version # noqa
    tv[5], tv[6], tv[7] = True, 0, 0
def GetWindowsVersionString(self)

An emulated handler for the external symbol GetWindowsVersionString.

Expand source code Browse git
@external()
def GetWindowsVersionString(self) -> str:
    return '{}.{:02d}.{:04d}'.format(*self.config.windows_os_version)
def CreateOleObject(self, name)

An emulated handler for the external symbol CreateOleObject.

Expand source code Browse git
@external()
def CreateOleObject(self, name: str) -> OleObject:
    return OleObject(name)
def GetActiveOleObject(self, name)

An emulated handler for the external symbol GetActiveOleObject.

Expand source code Browse git
@external()
def GetActiveOleObject(self, name: str) -> OleObject:
    return OleObject(name)
def IDispatchInvoke(self, ole, prop_set, name, value)

An emulated handler for the external symbol IDispatchInvoke.

Expand source code Browse git
@external()
def IDispatchInvoke(self, ole: OleObject | None, prop_set: bool, name: str, value: Any) -> Variant:
    if ole is None:
        raise IFPSException(_NIL_INVOKE)
    return variants.EMPTY
def FindWindowByClassName(self, name)

An emulated handler for the external symbol FindWindowByClassName.

Expand source code Browse git
@external()
def FindWindowByClassName(self, name: str) -> int:
    return 0
def FindWindowByWindowName(self, name)

An emulated handler for the external symbol FindWindowByWindowName.

Expand source code Browse git
@external()
def FindWindowByWindowName(self, name: str) -> int:
    return 0
def WizardSilent(self)

An emulated handler for the external symbol WizardSilent.

Expand source code Browse git
@external()
def WizardSilent(self) -> bool:
    return self.config.wizard_silent
def WizardNoIcons(self)

An emulated handler for the external symbol WizardNoIcons.

Expand source code Browse git
@external()
def WizardNoIcons(self) -> bool:
    return self._form().icons_off
def WizardSetupType(self, descriptions)

An emulated handler for the external symbol WizardSetupType.

Expand source code Browse git
@external()
def WizardSetupType(self, descriptions: bool) -> str:
    form = self._form()
    if (setup_type := form.setup_type) is None:
        return ''
    return form.type_description(setup_type) if descriptions else setup_type.Name
def WizardSelectedComponents(self, descriptions, caller=None)

An emulated handler for the external symbol WizardSelectedComponents.

Expand source code Browse git
@external(caller=True)
def WizardSelectedComponents(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
    expand = self._field_expander(caller)
    return comma_text((yield from self._form().selected_components(descriptions, False, expand)))
def WizardSelectedTasks(self, descriptions, caller=None)

An emulated handler for the external symbol WizardSelectedTasks.

Expand source code Browse git
@external(caller=True)
def WizardSelectedTasks(self, descriptions: bool, caller: _ScriptRun | None = None) -> Generator[Any, Any, str]:
    expand = self._field_expander(caller)
    return comma_text((yield from self._form().selected_tasks(descriptions, False, False, expand)))
def WizardIsComponentSelected(self, components)

An emulated handler for the external symbol WizardIsComponentSelected.

Expand source code Browse git
@external(alias=['IsComponentSelected'])
def WizardIsComponentSelected(self, components: str) -> bool:
    return _selected(self._form().selection().components, components)
def WizardIsTaskSelected(self, tasks)

An emulated handler for the external symbol WizardIsTaskSelected.

Expand source code Browse git
@external(alias=['IsTaskSelected'])
def WizardIsTaskSelected(self, tasks: str) -> bool:
    return _selected(self._form().selection().tasks, tasks)
def WizardSelectComponents(self, components)

An emulated handler for the external symbol WizardSelectComponents.

Expand source code Browse git
@external()
def WizardSelectComponents(self, components: str):
    self._form().select_components(comma_strings(components.replace('/', '\\')))
def WizardSelectTasks(self, tasks)

An emulated handler for the external symbol WizardSelectTasks.

Expand source code Browse git
@external()
def WizardSelectTasks(self, tasks: str):
    self._form().select_tasks(comma_strings(tasks.replace('/', '\\')))
def TWizardForm__GetComponentsList(self, this)

An emulated handler for the external symbol TWizardForm::GetComponentsList.

Expand source code Browse git
@external()
def TWizardForm__GetComponentsList(self, this: object) -> object:
    return self.wizard_form.components
def TWizardForm__GetTasksList(self, this)

An emulated handler for the external symbol TWizardForm::GetTasksList.

Expand source code Browse git
@external()
def TWizardForm__GetTasksList(self, this: object) -> object:
    return self.wizard_form.tasks
def TWizardForm__GetRunList(self, this)

An emulated handler for the external symbol TWizardForm::GetRunList.

Expand source code Browse git
@external()
def TWizardForm__GetRunList(self, this: object) -> object:
    return self.wizard_form.run_list
def TWizardForm__GetTypesCombo(self, this)

An emulated handler for the external symbol TWizardForm::GetTypesCombo.

Expand source code Browse git
@external()
def TWizardForm__GetTypesCombo(self, this: object) -> object:
    return self.wizard_form.types_combo
def TWizardForm__GetNoIconsCheck(self, this)

An emulated handler for the external symbol TWizardForm::GetNoIconsCheck.

Expand source code Browse git
@external()
def TWizardForm__GetNoIconsCheck(self, this: object) -> object:
    return self.wizard_form.no_icons_check
def TNewCheckListBox__GetChecked(self, this, index)

An emulated handler for the external symbol TNewCheckListBox::GetChecked.

Expand source code Browse git
@external()
def TNewCheckListBox__GetChecked(self, this: object, index: int) -> bool:
    if isinstance(this, CheckList):
        return this.checked(index)
    return True
def TNewCheckListBox__SetChecked(self, this, index, checked)

An emulated handler for the external symbol TNewCheckListBox::SetChecked.

Expand source code Browse git
@external()
def TNewCheckListBox__SetChecked(self, this: object, index: int, checked: bool):
    if isinstance(this, CheckList):
        this.set_checked(index, checked)
def TNewCheckListBox__GetState(self, this, index)

An emulated handler for the external symbol TNewCheckListBox::GetState.

Expand source code Browse git
@external()
def TNewCheckListBox__GetState(self, this: object, index: int) -> int:
    if isinstance(this, CheckList):
        return this.state(index)
    return 1
def TNewCheckListBox__GetItemEnabled(self, this, index)

An emulated handler for the external symbol TNewCheckListBox::GetItemEnabled.

Expand source code Browse git
@external()
def TNewCheckListBox__GetItemEnabled(self, this: object, index: int) -> bool:
    if isinstance(this, CheckList):
        return this.enabled(index)
    return True
def TNewCheckListBox__CheckItem(self, this, index, operation)

An emulated handler for the external symbol TNewCheckListBox::CheckItem.

Expand source code Browse git
@external()
def TNewCheckListBox__CheckItem(self, this: object, index: int, operation: int) -> bool:
    if isinstance(this, CheckList):
        return this.check(index, CheckOperation(operation))
    return True
def CurrentFilename(self)

An emulated handler for the external symbol CurrentFilename.

Expand source code Browse git
@external()
def CurrentFilename(self) -> str:
    if (file := self.current_file) is None or not file.destination:
        raise _outside_file_entry('CurrentFilename')
    return file.destination
def CurrentSourceFilename(self)

An emulated handler for the external symbol CurrentSourceFilename.

Expand source code Browse git
@external()
def CurrentSourceFilename(self) -> str:
    if (file := self.current_file) is None or not file.source:
        raise _outside_file_entry('CurrentSourceFilename', ' with flag "external"')
    return file.source
def expand(self, text, custom=None)

Expands the constants in a text of the setup where Setup expands it, with the custom constants that Setup passes for it; each code constant runs in a new run of the script.

Expand source code Browse git
def expand(self, text: str, custom: Mapping[str, str] | None = None) -> Generator[Any, Any, str]:
    """
    Expands the constants in a text of the setup where Setup expands it, with the custom
    constants that Setup passes for it; each code constant runs in a new run of the script.
    """
    return self.constants.expand(text, self.code_runner(None), custom)
def registry_key_exists(self, key)

Whether a key of the emulated registry exists.

Expand source code Browse git
def registry_key_exists(self, key: str) -> bool:
    """
    Whether a key of the emulated registry exists.
    """
    return self.constants.registry('', None, key)
def check_time(self)

Ends the emulation when its time is up.

Expand source code Browse git
def check_time(self):
    """
    Ends the emulation when its time is up.
    """
    if (watch := self._stopwatch) is not None and watch.expired():
        raise EmulatorTimeout
def call_from_setup(self, function, arguments, call, caller=None)

Calls a function of the script as Setup does: Pascal Script runs a function whose declaration does not take the call on a stack that does not fit it, and the emulation ends there. Setup creates the variables of the values that it passes and of the result that it takes.

Expand source code Browse git
def call_from_setup(
    self,
    function: Function,
    arguments: Sequence,
    call: SetupCall,
    caller: _ScriptRun | None = None,
) -> Generator[Any, Any, Any]:
    """
    Calls a function of the script as Setup does: Pascal Script runs a function whose
    declaration does not take the call on a stack that does not fit it, and the emulation ends
    there. Setup creates the variables of the values that it passes and of the result that it
    takes.
    """
    if not self._fits(function, call):
        raise EmulatorUndefined(F'Setup calls {function.name} as {call}, which its declaration does not take.')
    codes = self._setup_codes()
    types = [TPrimitive(codes[kind]) for kind, _ in call.parameters]
    result = None if call.result is None else TPrimitive(codes[call.result])
    run = _ScriptRun(self.dialect, caller)
    return (yield from self._execute(run, function, arguments, types, result))
def call_by_name(self, name, arguments, call, caller=None)

Calls a function of the script by name as Setup's CodeRunner finds it: a function of the script or a support function that the script imports.

Expand source code Browse git
def call_by_name(
    self,
    name: str,
    arguments: list,
    call: SetupCall,
    caller: _ScriptRun | None = None,
) -> Generator[Any, Any, Any]:
    """
    Calls a function of the script by name as Setup's `CodeRunner` finds it: a function of the
    script or a support function that the script imports.
    """
    if (function := self.symbols.get(name)) is None:
        raise DelphiException('Script error: Could not call proc')
    return (yield from self.call_from_setup(function, arguments, call, caller))
def Low(self, var)

An emulated handler for the external symbol Low.

Expand source code Browse git
@external()
def Low(self, var: Variable) -> int:
    return self._bound(var.deref(), False)
def High(self, var)

An emulated handler for the external symbol High.

Expand source code Browse git
@external()
def High(self, var: Variable) -> int:
    return self._bound(var.deref(), True)
def SizeOf(self, var)

An emulated handler for the external symbol SizeOf.

Expand source code Browse git
@external()
def SizeOf(self, var: Variable) -> int:
    return pascal.size_of(var.type, self.runtime)
class InnoSetupEmulator (archive, options=None, **more)

A specialized IFPSEmulator that can emulate the InnoSetup installation with a focus on continuing execution as much as possible.

Expand source code Browse git
class InnoSetupEmulator(IFPSEmulator):
    """
    A specialized `IFPSEmulator` that can emulate the InnoSetup installation with a focus on
    continuing execution as much as possible.
    """

    def __init__(
        self,
        archive: InnoArchive | IFPSFile,
        options: IFPSEmulatorConfig | None = None,
        **more,
    ):
        super().__init__(archive, options, **more)
        cfg = self.config
        self.entries: SetupEntries | None = None
        if (info := self.setup_info) is not None:
            self.entries = SetupEntries.of(info, cfg.windows_os_version, cfg.windows_sp_version)

    def emulate_installation(
        self,
        password: str = '',
        max_seconds: float | None = None,
        password_test: Callable[[str], bool] | None = None,
    ):
        """
        Emulates the calls into the script that Setup makes during an installation in which the user
        clicks Next on every page until Setup finishes; a silent installation when the configuration
        says so. Each installation starts the script over. The given password is typed into the box
        on the password page. An event or entry that ends with a script error is reported as a
        `refinery.lib.inno.emulation.NewScriptError` and the installation goes on, as described
        for `refinery.lib.inno.installation.Installation`. Any other exception ends the emulation.
        A call of an emulated function that fails is reported as a
        `refinery.lib.inno.emulation.NewEmulationError`, also when the script handles its error.
        The time limit counts for the whole installation; the given number of seconds replaces the
        one of the configuration. Setup tests a password against its own before it asks the
        script; the caller can give that test as `password_test`, without it the test fails.
        """
        self.reset(walked=True)
        self.wizard_form.needs_restart = self.setup.always_restart
        installation = Installation(self, self.entries, password, password_test)
        return (yield from self._timed(installation.run(), max_seconds))

    def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
        """
        Any unimplemented function is essentially skipped. Any arguments passed by reference and
        all return values that are of type integer are set to `1` in an attempt to indicate success
        wherever possible. When the function is a member of a control of the wizard that holds
        choices of the installation, called on that control, the emulator no longer knows those
        choices, unless the member belongs to every control, such as its position, or only changes
        how the control looks.
        """
        if arguments and _changes_choices(function):
            self.wizard_form.touched(arguments[0].deref().data)
        if result is not None and not result.container and _holds_integer(result.type):
            result.set(1)
        for ptr in arguments:
            if not ptr.pointer:
                continue
            var = ptr.deref()
            if not var.container and _holds_integer(var.type):
                var.set(1)

Ancestors

Methods

def emulate_installation(self, password='', max_seconds=None, password_test=None)

Emulates the calls into the script that Setup makes during an installation in which the user clicks Next on every page until Setup finishes; a silent installation when the configuration says so. Each installation starts the script over. The given password is typed into the box on the password page. An event or entry that ends with a script error is reported as a NewScriptError and the installation goes on, as described for Installation. Any other exception ends the emulation. A call of an emulated function that fails is reported as a NewEmulationError, also when the script handles its error. The time limit counts for the whole installation; the given number of seconds replaces the one of the configuration. Setup tests a password against its own before it asks the script; the caller can give that test as password_test, without it the test fails.

Expand source code Browse git
def emulate_installation(
    self,
    password: str = '',
    max_seconds: float | None = None,
    password_test: Callable[[str], bool] | None = None,
):
    """
    Emulates the calls into the script that Setup makes during an installation in which the user
    clicks Next on every page until Setup finishes; a silent installation when the configuration
    says so. Each installation starts the script over. The given password is typed into the box
    on the password page. An event or entry that ends with a script error is reported as a
    `refinery.lib.inno.emulation.NewScriptError` and the installation goes on, as described
    for `refinery.lib.inno.installation.Installation`. Any other exception ends the emulation.
    A call of an emulated function that fails is reported as a
    `refinery.lib.inno.emulation.NewEmulationError`, also when the script handles its error.
    The time limit counts for the whole installation; the given number of seconds replaces the
    one of the configuration. Setup tests a password against its own before it asks the
    script; the caller can give that test as `password_test`, without it the test fails.
    """
    self.reset(walked=True)
    self.wizard_form.needs_restart = self.setup.always_restart
    installation = Installation(self, self.entries, password, password_test)
    return (yield from self._timed(installation.run(), max_seconds))
def unimplemented(self, function, arguments, result)

Any unimplemented function is essentially skipped. Any arguments passed by reference and all return values that are of type integer are set to 1 in an attempt to indicate success wherever possible. When the function is a member of a control of the wizard that holds choices of the installation, called on that control, the emulator no longer knows those choices, unless the member belongs to every control, such as its position, or only changes how the control looks.

Expand source code Browse git
def unimplemented(self, function: Function, arguments: list[Variable], result: Variable | None):
    """
    Any unimplemented function is essentially skipped. Any arguments passed by reference and
    all return values that are of type integer are set to `1` in an attempt to indicate success
    wherever possible. When the function is a member of a control of the wizard that holds
    choices of the installation, called on that control, the emulator no longer knows those
    choices, unless the member belongs to every control, such as its position, or only changes
    how the control looks.
    """
    if arguments and _changes_choices(function):
        self.wizard_form.touched(arguments[0].deref().data)
    if result is not None and not result.container and _holds_integer(result.type):
        result.set(1)
    for ptr in arguments:
        if not ptr.pointer:
            continue
        var = ptr.deref()
        if not var.container and _holds_integer(var.type):
            var.set(1)

Inherited members