Module refinery.lib.inno.ifps

The code is based on the logic implemented in IFPSTools: https://github.com/Wack0/IFPSTools

Expand source code Browse git
"""
The code is based on the logic implemented in IFPSTools:
 https://github.com/Wack0/IFPSTools
"""
from __future__ import annotations

import abc
import enum
import io
import itertools
import math

from collections import OrderedDict
from dataclasses import dataclass, field
from fractions import Fraction
from typing import (
    Callable,
    Generator,
    NamedTuple,
    TypeVar,
)
from uuid import UUID

from refinery.lib.inno import CaseInsensitiveDict
from refinery.lib.inno.delphi import numbers, variants, x87
from refinery.lib.inno.delphi.runtime import Platform
from refinery.lib.inno.symbols import IFPSAPI, IFPSClasses, IFPSEvents, IFPSSignature
from refinery.lib.structures import Struct, StructReader

_E = TypeVar('_E', bound=type[enum.Enum])

_TAB = '\x20\x20'


def extended(_data: bytes) -> x87.Real:
    """
    Parses the 10 bytes of an x87 extended-precision float into its exact value.
    """
    if len(_data) != 10:
        raise ValueError
    data = int.from_bytes(_data, 'little')
    negative = bool(data >> 79)
    exponent = (data >> 64) & 0x7FFF
    mantissa = data & 0xFFFFFFFFFFFFFFFF
    if exponent == 0x7FFF:
        if mantissa & 0x7FFFFFFFFFFFFFFF == 0:
            return -math.inf if negative else math.inf
        return math.nan
    if mantissa == 0:
        return -0.0 if negative else 0.0
    return x87.real(-mantissa if negative else mantissa, max(exponent, 1) - 16383 - 63)


def represent(cls: _E) -> _E:
    """
    A decorator for various IFPS integer enumeration classes to change the default string
    representation.
    """
    cls.__repr__ = lambda self: F'{self.__class__.__name__}.{self.name}'
    cls. __str__ = lambda self: self.name
    return cls


@represent
class Op(enum.IntEnum):
    """
    An enumeration of all known IFPS opcodes.
    """
    Assign       = 0x00  # noqa
    Calculate    = 0x01  # noqa
    Push         = 0x02  # noqa
    PushVar      = 0x03  # noqa
    Pop          = 0x04  # noqa
    Call         = 0x05  # noqa
    Jump         = 0x06  # noqa
    JumpTrue     = 0x07  # noqa
    JumpFalse    = 0x08  # noqa
    Ret          = 0x09  # noqa
    StackType    = 0x0A  # noqa
    PushType     = 0x0B  # noqa
    Compare      = 0x0C  # noqa
    CallVar      = 0x0D  # noqa
    SetPtr       = 0x0E  # noqa
    BooleanNot   = 0x0F  # noqa
    Neg          = 0x10  # noqa
    SetFlag      = 0x11  # noqa
    JumpFlag     = 0x12  # noqa
    PushEH       = 0x13  # noqa
    PopEH        = 0x14  # noqa
    IntegerNot   = 0x15  # noqa
    SetPtrToCopy = 0x16  # noqa
    Inc          = 0x17  # noqa
    Dec          = 0x18  # noqa
    JumpPop1     = 0x19  # noqa
    JumpPop2     = 0x1A  # noqa
    Nop          = 0xFF  # noqa
    _INVALID     = 0xDD  # noqa

    @classmethod
    def FromInt(cls, code: int):
        try:
            return cls(code)
        except ValueError:
            return cls._INVALID


class Dialect(enum.Enum):
    """
    The versions of Pascal Script whose rules for values differ, named by the Inno Setup that ships
    them: the same rules from 5.5.3 to 6.7, and new ones from 7.0 on.
    """
    Inno6 = 6
    Inno7 = 7


class AOp(enum.IntEnum):
    """
    An enumeration of all known IFPS arithmetic opcodes.
    """
    Add = 0
    Sub = 1
    Mul = 2
    Div = 3
    Mod = 4
    Shl = 5
    Shr = 6
    And = 7
    BOr = 8
    Xor = 9
    As = 10

    def __str__(self):
        if self is AOp.As:
            return 'as'
        glyph = ('+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^')[self]
        return F'{glyph}='


class COp(enum.IntEnum):
    """
    An enumeration of all known IFPS comparison opcodes.
    """
    GE = 0
    LE = 1
    GT = 2
    LT = 3
    NE = 4
    EQ = 5
    IN = 6
    IS = 7

    def __str__(self):
        return ('>=', '<=', '>', '<', '!=', '==', 'in', 'is')[self]


@represent
class TC(enum.IntEnum):
    """
    An enumeration of all known IFPS type codes.
    """
    ReturnAddress       = 0x00  # noqa
    U08                 = 0x01  # noqa
    S08                 = 0x02  # noqa
    U16                 = 0x03  # noqa
    S16                 = 0x04  # noqa
    U32                 = 0x05  # noqa
    S32                 = 0x06  # noqa
    Single              = 0x07  # noqa
    Double              = 0x08  # noqa
    Extended            = 0x09  # noqa
    AnsiString          = 0x0A  # noqa
    Record              = 0x0B  # noqa
    Array               = 0x0C  # noqa
    Pointer             = 0x0D  # noqa
    PChar               = 0x0E  # noqa
    ResourcePointer     = 0x0F  # noqa
    Variant             = 0x10  # noqa
    S64                 = 0x11  # noqa
    Char                = 0x12  # noqa
    WideString          = 0x13  # noqa
    WideChar            = 0x14  # noqa
    ProcPtr             = 0x15  # noqa
    StaticArray         = 0x16  # noqa
    Set                 = 0x17  # noqa
    Currency            = 0x18  # noqa
    Class               = 0x19  # noqa
    Interface           = 0x1A  # noqa
    NotificationVariant = 0x1B  # noqa
    UnicodeString       = 0x1C  # noqa
    U64                 = 0x1D  # noqa
    Enum                = 0x81  # noqa
    Type                = 0x82  # noqa
    ExtClass            = 0x83  # noqa

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the code represents a primitive type.
        """
        return self not in _TC_NON_PRIMITIVE

    @property
    def container(self) -> bool:
        """
        Indicates whether the code represents a container type.
        """
        return self in _TC_CONTAINER

    @property
    def width(self):
        """
        For primitive types, this gives the size of an immediate of this type in bytes.
        """
        return _TC_WIDTH.get(self, 0)

    @property
    def integer_range(self) -> range | None:
        """
        For integer types, this gives the range of values that an immediate of this type can hold.
        """
        return _TC_INTEGER_RANGE.get(self)

    @property
    def integer(self) -> bool:
        """
        Indicates whether the code represents an integer type.
        """
        return self in _TC_INTEGER_RANGE

    @property
    def signed(self) -> bool:
        """
        Indicates whether the code represents a signed integer type.
        """
        return self in _TC_SIGNED

    @property
    def character(self) -> bool:
        """
        Indicates whether the code represents a character type.
        """
        return self in _TC_CHARACTER


_TC_NON_PRIMITIVE = frozenset((
    TC.Class,
    TC.ProcPtr,
    TC.Interface,
    TC.Set,
    TC.StaticArray,
    TC.Array,
    TC.Record,
))

_TC_CONTAINER = frozenset((
    TC.StaticArray,
    TC.Array,
    TC.Record,
))

_TC_CHARACTER = frozenset((
    TC.Char,
    TC.WideChar,
))

_TC_WIDTH = {
    TC.Variant       : 0x10,
    TC.Char          : 0x01,
    TC.S08           : 0x01,
    TC.U08           : 0x01,
    TC.WideChar      : 0x02,
    TC.S16           : 0x02,
    TC.U16           : 0x02,
    TC.WideString    : 0x04,
    TC.UnicodeString : 0x04,
    TC.Interface     : 0x04,
    TC.Class         : 0x04,
    TC.PChar         : 0x04,
    TC.AnsiString    : 0x04,
    TC.Single        : 0x04,
    TC.S32           : 0x04,
    TC.U32           : 0x04,
    TC.ProcPtr       : 0x0C,
    TC.Currency      : 0x08,
    TC.Pointer       : 0x0C,
    TC.Double        : 0x08,
    TC.S64           : 0x08,
    TC.U64           : 0x08,
    TC.Extended      : 0x0A,
    TC.ReturnAddress : 0x1C,
}


def _integer_range(code: TC, signed: bool) -> range:
    size = 1 << (code.width * 8)
    if signed:
        return range(-(size >> 1), size >> 1)
    return range(size)


_TC_INTEGER_RANGE = {
    TC.U08 : _integer_range(TC.U08, False),
    TC.U16 : _integer_range(TC.U16, False),
    TC.U32 : _integer_range(TC.U32, False),
    TC.U64 : _integer_range(TC.U64, False),
    TC.S08 : _integer_range(TC.S08, True),
    TC.S16 : _integer_range(TC.S16, True),
    TC.S32 : _integer_range(TC.S32, True),
    TC.S64 : _integer_range(TC.S64, True),
}

_TC_SIGNED = frozenset(code for code, valid in _TC_INTEGER_RANGE.items() if valid.start < 0)


@dataclass
class IFPSTypeBase(abc.ABC):
    """
    The base class for any IFPS type.
    """
    code: TC
    symbol: str | None = field(default=None, kw_only=True)
    attributes: list[FunctionAttribute] | None = field(default=None, kw_only=True)

    def simple(self, nested: bool = False) -> bool:
        """
        Indicate whether the type requires more than one line to pretty print.
        """
        return True

    def display(self, indent=0):
        """
        Compute a display string that can be used to represent the type in disassembly.
        """
        return indent * _TAB + self.code.name

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the type is primitive.
        """
        return self.code.primitive

    @property
    def container(self) -> bool:
        """
        Indicates whether the type is a container.
        """
        return self.code.container

    @property
    def boolean(self) -> bool:
        """
        Indicates whether the type is the Boolean type of Pascal Script: a byte that the runtime
        recognizes by its exported name and converts into a truth value for a Variant.
        """
        return self.code is TC.U08 and (self.symbol or '').upper() == 'BOOLEAN'

    def __str__(self):
        if self.symbol is not None:
            return self.symbol
        return self.display(0)


@dataclass
class TPrimitive(IFPSTypeBase):
    """
    A primitive IFPS type.
    """


@dataclass
class TProcPtr(IFPSTypeBase):
    """
    The procedure pointer IFPS type.
    """
    void: bool
    args: tuple[DeclSpecParam, ...]

    def display(self, indent=0):
        name = super().display(indent)
        args = []
        for k, spec in enumerate(self.args, 1):
            arg = F'Arg{k}'
            if not spec.const:
                arg = F'*{arg}'
            if spec.type is not None:
                arg = F'{spec.type!s} {arg}'
            args.append(arg)
        args = ', '.join(args)
        return F'{name}({args})'


@dataclass
class TInterface(IFPSTypeBase):
    """
    An IFPS type representing a COM interface.
    """
    uuid: UUID

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.uuid!s})'


@dataclass
class TClass(IFPSTypeBase):
    """
    An IFPS type representing an IFPS class.
    """
    name: str


@dataclass
class TSet(IFPSTypeBase):
    """
    An IFPS type representing a bit vector.
    """
    size: int

    @property
    def size_in_bytes(self):
        q, r = divmod(self.size, 8)
        return q + (r and 1 or 0)

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.size})'


@dataclass
class TArray(IFPSTypeBase):
    """
    An IFPS type representing a dynamic array.
    """
    type: IFPSType

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'array of {display}'

    def simple(self, nested: bool = False) -> bool:
        return self.type.simple(nested)


@dataclass
class TStaticArray(IFPSTypeBase):
    """
    An IFPS type representing a static array (i.e. a tuple).
    """
    type: IFPSType
    size: int
    offset: int | None = None

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'{display}[{self.size}]'

    def simple(self, nested: bool = False) -> bool:
        return self.type.simple(nested)


@dataclass
class TRecord(IFPSTypeBase):
    """
    An IFPS type representing a structure.
    """
    members: tuple[IFPSType, ...]

    @property
    def size(self):
        return len(self.members)

    def simple(self, nested: bool = False) -> bool:
        if nested:
            return False
        if len(self.members) > 10:
            return False
        return all(m.simple(True) for m in self.members)

    def display(self, indent=0):
        output = io.StringIO()
        output.write(indent * _TAB)
        output.write('struct {')
        if self.simple():
            output.write(', '.join(str(m) for m in self.members))
        else:
            for k, member in enumerate(self.members):
                if k > 0:
                    output.write(',')
                output.write('\n')
                output.write(member.display(indent + 1))
            if self.members:
                output.write(F'\n{_TAB * indent}')
        output.write('}')
        return output.getvalue()


IFPSType = (
    TRecord
    | TStaticArray
    | TArray
    | TSet
    | TProcPtr
    | TClass
    | TInterface
    | TPrimitive
)
"""
Represents any of the possible IFPS data types:

- `refinery.lib.inno.ifps.TRecord`
- `refinery.lib.inno.ifps.TStaticArray`
- `refinery.lib.inno.ifps.TArray`
- `refinery.lib.inno.ifps.TSet`
- `refinery.lib.inno.ifps.TProcPtr`
- `refinery.lib.inno.ifps.TClass`
- `refinery.lib.inno.ifps.TInterface`
- `refinery.lib.inno.ifps.TPrimitive`
"""


class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: str | int | float | x87.Extended | Fraction | bytes | Function

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        if isinstance(v, x87.Extended):
            return numbers.shortest(v)
        if isinstance(v, Fraction):
            return variants.currency_text(v, numbers.FormatSettings())
        return repr(v)


class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name


@dataclass
class DeclSpecParam:
    """
    A function parameter specification.
    """
    const: bool
    """
    True if this parameter is passed by value, not by reference.
    """
    type: IFPSType | None = None
    """
    The type of this parameter.
    """
    name: str | None = None
    """
    The name of this parameter.
    """


class CallType(str, enum.Enum):
    """
    This enumeration classifies the different call types.
    """
    Symbol = 'symbol'
    Procedure = 'procedure'
    Function = 'function'

    def __str__(self):
        return self.value


@dataclass
class DeclSpec:
    """
    This class captures the declaration info of a function symbol.
    """
    void: bool
    parameters: list[DeclSpecParam] = field(default_factory=list)
    name: str = ''
    calling_convention: str | None = None
    return_type: IFPSType | None = None
    module: str | None = None
    classname: str | None = None
    method: bool = False
    """
    True for a method of a class, which Pascal Script calls with its object before the parameters
    that the declaration lists.
    """
    delay_load: bool = False
    vtable_index: int | None = None
    load_with_altered_search_path: bool = False

    @property
    def argc(self):
        return len(self.parameters)

    def represent(self, name: str, ref: bool = False, rel: bool = False):
        def pparam(k: int, p: DeclSpecParam):
            name = p.name or F'{VariableType.Argument!s}{k}'
            if p.type is not None:
                name = F'{name}: {p.type!s}'
            if not p.const:
                name = F'*{name}'
            return name
        if self.name and name in self.name:
            name = self.name
        spec = name
        if self.vtable_index is not None:
            spec = F'{self.name}[{self.vtable_index}]'
        if not rel and self.classname:
            spec = F'{self.classname}.{spec}'
        if not rel and self.module:
            spec = F'{self.module}::{spec}'
        if not ref:
            if self.delay_load:
                spec = F'__delay_load {spec}'
            if self.calling_convention:
                spec = F'__{self.calling_convention} {spec}'
            spec = F'{self.type} {spec}'
            args = self.parameters
            args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
            spec = F'{spec}({args})'
            if self.return_type:
                spec = F'{spec}: {self.return_type!s}'
        return spec

    @property
    def type(self):
        if self.void:
            return CallType.Procedure
        return CallType.Function

    def __repr__(self):
        return self.represent(self.name or '(*)')

    @classmethod
    def ParseF(cls, reader: StructReader[bytes], load_flags: int, max_argc: int):
        """
        Parses the declaration of an import. Pascal Script writes `load_flags` flag bytes into the
        declaration of a DLL import. The parameter count of a property is a number that no other
        data backs; a count above `max_argc` is refused.
        """
        def ascii():
            return reader.read_c_string('latin1')

        def boolean():
            return bool(reader.u8())

        def cc():
            return {
                0: 'register',
                1: 'pascal',
                2: 'cdecl',
                3: 'stdcall',
            }.get(reader.u8(), cls.calling_convention)

        def read_parameters():
            nonlocal void
            void = not boolean()
            parameters.extend(DeclSpecParam(not b) for b in reader.read())

        void = True
        name = None
        properties = {}
        parameters = []

        if reader.readif(b'dll:'):
            reader.readif(B'files:')
            if (module := ascii()).lower().endswith('.dll'):
                module = module[:-4]
            properties.update(module=module)
            name = ascii()
            properties.update(calling_convention=cc())
            if load_flags > 0:
                properties.update(delay_load=boolean())
            if load_flags > 1:
                properties.update(load_with_altered_search_path=boolean())
            read_parameters()
        elif reader.readif(b'class:'):
            if reader.remaining_bytes == 1:
                spec = reader.peek(1)
                void = False
                parameters.append(DeclSpecParam(False))
                name = {
                    b'+': 'CastToType',
                    B'-': 'SetNil'
                }.get(spec)
                properties.update(classname='Class', calling_convention='pascal')
            else:
                properties.update(classname=reader.read_terminated_array(b'|').decode('latin1'))
                name = reader.read_terminated_array(b'|').decode('latin1')
                if write := name.endswith('@'):
                    name = name[:-1]
                if write or reader.peek(1) != bytes(1):
                    void = write
                    if (argc := reader.u32() + write) > max_argc:
                        raise ValueError(F'The property {name} takes {argc} arguments, more than any call can pass.')
                    parameters.extend(DeclSpecParam(True) for _ in range(argc))
                    name = F'Set{name}' if write else F'Get{name}'
                else:
                    properties.update(calling_convention=cc(), method=True)
                    read_parameters()
        elif reader.readif(b'intf:.'):
            name = 'CoInterface'
            properties.update(vtable_index=reader.u32())
            properties.update(calling_convention=cc())
            read_parameters()
        else:
            read_parameters()
        return cls(void, parameters, name=name, **properties)

    @classmethod
    def ParseE(cls, data: bytes, ipfs: IFPSFile):
        decl = data.split(B'\x20')
        try:
            return_type = int(decl.pop(0))
        except Exception:
            void = True
        else:
            void = return_type < 0
        if not void:
            return_type = ipfs.types[return_type]
        else:
            return_type = None
        parameters = []
        for param in decl:
            try:
                i = int(param[1:])
            except Exception:
                tv = None
            else:
                tv = ipfs.types[i]
            parameters.append(
                DeclSpecParam(param[:1] == B'@', tv))
        return cls(void, parameters, return_type=return_type)


def _trace_stack(bbs: dict[int, BasicBlock]) -> None:
    """
    Traces the stack depth from the entry block through every block that is reachable from it, in
    depth-first order. A block that is reached with two different depths has no known depth.
    """
    visited: set[int] = set()
    errored: set[int] = set()
    pending: list[tuple[int, int | None]] = [(0, 0)]
    while pending:
        offset, stack = pending.pop()
        if offset in errored:
            continue
        bb = bbs[offset]
        if bb.stack is not None and stack != bb.stack:
            stack = None
        if stack is None:
            errored.add(offset)
        elif offset in visited:
            continue
        else:
            visited.add(offset)
        bb.stack = stack
        if stack is not None:
            for insn in bb.body:
                insn.stack = stack
                stack += insn.stack_delta
        pending.extend((target, stack) for target in reversed(bb.targets))


@dataclass
class Function:
    """
    Represents a function in the IFPS runtime.
    """
    symbol: str = ''
    decl: DeclSpec | None = None
    body: list[Instruction] | None = None
    attributes: list[FunctionAttribute] | None = None
    _bbs: dict[int, BasicBlock] | None = None
    _ins: dict[int, Instruction] | None = None

    @property
    def name(self):
        symbol = self.symbol
        if (decl := self.decl) and (name := decl.name) and (symbol in name):
            symbol = name
        return symbol

    @property
    def code(self):
        if code := self._ins:
            return code
        self._ins = code = {i.offset: i for i in self.body}
        return code

    def reference(self, rel: bool = False) -> str:
        if self.decl is None:
            return self.symbol
        return self.decl.represent(self.symbol, ref=True, rel=rel)

    def __repr__(self):
        if self.decl is None:
            return F'symbol {self.symbol}'
        return self.decl.represent(self.symbol)

    def __str__(self):
        return self.reference()

    @property
    def type(self):
        if self.decl is None:
            return CallType.Symbol
        return self.decl.type

    def get_basic_blocks(self) -> dict[int, BasicBlock]:
        if (bbs := self._bbs) is not None:
            return bbs
        if self.body is None:
            bbs = self._bbs = {}
            return bbs

        bbs: dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
        self._bbs = bbs
        jump = False

        for insn in self.body:
            try:
                bb = bbs[insn.offset]
            except KeyError:
                if jump or insn.jumptarget:
                    nb = bbs[insn.offset] = BasicBlock(insn.offset)
                    if not jump:
                        nb.sources[bb.offset] = bb
                        bb.targets[nb.offset] = nb
                    bb = nb
            bb.body.append(insn)
            if not insn.branches:
                jump = False
                continue
            targets = [insn.operands[0]]
            sequence = insn.offset + insn.size
            jump = insn.jumps
            if not jump and insn.opcode != Op.Ret:
                targets.append(sequence)
            for t in targets:
                if not (bt := bbs.get(t)):
                    bt = bbs[t] = BasicBlock(t)
                bb.targets[t] = bt
                bt.sources[bb.offset] = bb

        for offset, bb in list(bbs.items()):
            if bb.body:
                continue
            del bbs[offset]
            for source in bb.sources.values():
                source.targets.pop(offset, None)

        _trace_stack(bbs)

        for insn in self.body:
            if (stack := insn.stack) is None:
                continue
            for k, op in enumerate(insn.operands):
                if not isinstance(op, Operand):
                    continue
                if not (v := op.variable) or v.type != VariableType.Local:
                    continue
                if v.index <= stack:
                    continue
                raise IndexError(
                    F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                    F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

        return bbs


class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: VariableSpec | str | None
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable, or the
    name of a global variable that the script exports. If this property is `None`, the variable is
    unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy` opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec | str | None):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'


@represent
class OperandType(enum.IntEnum):
    """
    Classifies the type of an `refinery.lib.inno.ifps.Operand`.
    """
    Variable = 0
    Value = 1
    IndexedByInt = 2
    IndexedByVar = 3


@represent
class EHType(enum.IntEnum):
    """
    This enumeration lists the possible types of code region covered by an exception handler.
    """
    Try = 0
    Finally = 1
    Catch = 2
    SecondFinally = 3


@represent
class NewEH(enum.IntEnum):
    """
    This enumeration gives names to the 4 arguments of the opcode responsible for registering a new
    exception handler. The first argument specifies the location of a finally, the second argument
    specifies the location of a catch handler, and so on.
    """
    Finally = 0
    CatchAt = 1
    SecondFinally = 2
    End = 3


class VariableType(str, enum.Enum):
    Global = 'GlobalVar'
    Local = 'LocalVar'
    Argument = 'Argument'

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.value


class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'


class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')


_Op_Maxlen = max(len(op.name) for op in Op)
_Op_StackD = {
    Op.Push     : +1,
    Op.PushVar  : +1,
    Op.PushType : +1,
    Op.Pop      : -1,
    Op.JumpPop1 : -1,
    Op.JumpPop2 : -2,
}


@dataclass
class Instruction:
    offset: int
    opcode: Op
    encoded: bytes = B''
    stack: int | None = None
    operands: list[str | bool | int | float | Operand | IFPSType | Function | None] = field(default_factory=list)
    operator: AOp | COp | None = None
    jumptarget: bool = False

    def callee(self) -> Function:
        """
        The function that this instruction calls; it must be a `refinery.lib.inno.ifps.Op.Call`.
        """
        if not isinstance(target := self.operands[0], Function):
            raise TypeError(F'The instruction at offset 0x{self.offset:X} calls no function.')
        return target

    def op(self, index: int):
        arg = self.operands[index]
        if not isinstance(arg, Operand):
            raise TypeError
        return arg

    @property
    def size(self):
        return len(self.encoded)

    @property
    def branches(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpFalse,
            Op.JumpTrue,
            Op.JumpFlag,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def jumps(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def stack_delta(self):
        return _Op_StackD.get(self.opcode, 0)

    def oprep(self, labels: dict[int, str] | None = None):
        if self.branches:
            dst = self.operands[0]
            if not labels or not (label := labels.get(dst)):
                label = F'0x{dst:X}'
            var = [str(op) for op in self.operands[1:]]
            return ', '.join((label, *var))
        elif self.opcode is Op.PushEH:
            ops = []
            for op, name in reversed(list(zip(self.operands, NewEH))):
                if op is None:
                    continue
                ops.append(F'{name}:0x{op:X}')
            return '\x20'.join(ops)
        elif self.opcode is Op.PopEH:
            return F'End{EHType(self.operands[0])}'
        elif self.opcode is Op.SetFlag:
            rep, negated = self.operands
            return F'!{rep}' if negated else str(rep)
        elif self.opcode is Op.Compare:
            dst, a, b = self.operands
            return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
        elif self.opcode is Op.Calculate:
            dst, src = self.operands
            return F'{dst!s} {self.operator!s} {src!s}'
        elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
            dst, src = self.operands
            return F'{dst!s} := {src!s}'
        else:
            return ', '.join(str(op) for op in self.operands)

    def pretty(self, labels: dict[int, str] | None = None):
        return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()

    def __repr__(self):
        return F'{self.opcode.name}({self.oprep()})'

    def __str__(self):
        return self.pretty()


@dataclass
class BasicBlock:
    offset: int
    stack: int | None = None
    body: list[Instruction] = field(default_factory=list)
    sources: dict[int, BasicBlock] = field(default_factory=dict)
    targets: dict[int, BasicBlock] = field(default_factory=dict)

    @property
    def stack_delta(self):
        return sum(insn.stack_delta for insn in self.body)

    @property
    def size(self):
        return sum(insn.size for insn in self.body)


class FTag(enum.IntFlag):
    External = 0b0001
    Exported = 0b0010
    HasAttrs = 0b0100

    def check(self, v):
        return bool(self & v)


class _BuiltinParameter(NamedTuple):
    name: str
    const: bool


_BUILTIN_PARAMETERS: dict[str, tuple[_BuiltinParameter, ...]] = {
    'getarraylength' : (_BuiltinParameter('Arr', True),),
    'setarraylength' : (_BuiltinParameter('arr', False), _BuiltinParameter('count', True)),
    'length'         : (_BuiltinParameter('S', True),),
    'setlength'      : (_BuiltinParameter('s', False), _BuiltinParameter('NewLength', True)),
    'low'            : (_BuiltinParameter('X', True),),
    'high'           : (_BuiltinParameter('X', True),),
    'dec'            : (_BuiltinParameter('x', False),),
    'inc'            : (_BuiltinParameter('x', False),),
    'include'        : (_BuiltinParameter('s', False), _BuiltinParameter('m', True)),
    'exclude'        : (_BuiltinParameter('s', False), _BuiltinParameter('m', True)),
    'sizeof'         : (_BuiltinParameter('Data', True),),
}
"""
The built-in functions of Pascal Script that get their parameters after their declaration was
compiled, so that their compiled declaration has none; with the parameters they get.
"""


_ARGUMENT_SIZE = 6
"""
The fewest bytes of code that an argument of a call takes: Pascal Script pushes each argument with
an instruction of five bytes or more and pops it with one of one byte.
"""


class _CallSite(NamedTuple):
    """
    What a call of a function shows about its declaration. Pascal Script passes the variable for
    the result right before the call, and frees the argument slots and the result right after it,
    one `Pop` each; the expression around the call may free more.
    """
    result: bool
    pops: int

    def contradicts(self, decl: DeclSpec) -> bool:
        return self.result == decl.void or self.pops < decl.argc + self.result

    def slack(self, decl: DeclSpec) -> int:
        return self.pops - decl.argc - self.result


def _shapes_agree(decl: DeclSpec, signature: IFPSSignature) -> bool:
    return (
        decl.void == signature.void
        and decl.argc == signature.argc
        and all(old.const == new.const for old, new in zip(decl.parameters, signature.parameters))
    )


class IFPSFile(Struct):
    MinVer = 12
    MaxVer = 23

    Magic = B'IFPS'

    def __init__(
        self,
        reader: StructReader[memoryview],
        codec: str = 'latin1',
        unicode: bool | None = None,
        load_flags: int | None = None,
    ):
        """
        The setup that compiled the script decides whether `String` is a Unicode string and how many
        load flags a DLL import has. When the caller does not know them, they are read from the
        script: it is Unicode unless its only string type is `AnsiString`, and the load flags are
        decided by the calls of the DLL imports. The script's standard types tell which Pascal
        Script compiled it and for which platform.
        """
        self.codec = codec
        self.load_flags = load_flags
        self.types: list[IFPSType] = []
        self.functions: list[Function] = []
        self.globals: list[VariableBase] = []
        self._strings: dict[str, None] = {}
        self.reader = reader
        if reader.remaining_bytes < 28:
            raise ValueError('Less than 28 bytes in file, not enough data to parse.')
        magic = reader.read(4)
        if magic != self.Magic:
            raise ValueError(F'Invalid magic sequence: {magic.hex()}')
        self.version = reader.u32()
        self.count_types = reader.u32()
        self.count_functions = reader.u32()
        self.count_variables = reader.u32()
        self.entry = reader.u32()
        self.import_size = reader.u32()

        if self.version not in range(self.MinVer, self.MaxVer + 1):
            raise NotImplementedError(
                F'This IFPS file has version {self.version}, which is not in the supported range '
                F'[{self.MinVer},{self.MaxVer}].')

        self._known_type_names = {
            TC.U08        : {'Byte', 'Boolean'},
            TC.S08        : {'ShortInt'},
            TC.U16        : {'Word'},
            TC.S16        : {'SmallInt'},
            TC.S32        : {'Integer', 'LongInt'},
            TC.U32        : {'LongWord', 'Cardinal', 'HWND', 'TSetupProcessorArchitecture'},
            TC.Char       : {'AnsiChar'},
            TC.PChar      : {'PAnsiChar'},
            TC.AnsiString : {'AnyString'},
            TC.S64        : {'Int64', 'NativeInt'},
            TC.U64        : {'UInt64', 'NativeUInt'},
        }

        self._load_types()
        if unicode is None:
            codes = {t.code for t in self.types}
            unicode = TC.UnicodeString in codes or TC.AnsiString not in codes
        self.unicode = unicode
        self.dialect = self._dialect()
        self.platform = self._platform()
        self._name_types()
        self._load_functions()
        self._load_variables()

        del self._known_type_names
        self.strings: list[str] = list(self._strings)
        del self._strings

    def _dialect(self) -> Dialect:
        """
        The Pascal Script that compiled the script. The compiler of Inno Setup 7 declares the type
        `UInt64` in every script, and Pascal Script has no such type before Inno Setup 7.
        """
        if any(t.code is TC.U64 for t in self.types):
            return Dialect.Inno7
        return Dialect.Inno6

    def _platform(self) -> Platform:
        """
        The platform of the Setup that the script was compiled for. The compiler declares its
        standard types first, in a fixed order; from Inno Setup 7 on, `NativeUInt` follows the first
        `Single` and is 64 bits wide in a script for a 64-bit Setup. Earlier versions only build
        32-bit Setups and have no 64-bit unsigned type. The standard types decide, so the types
        read so far decide while the type table is read.
        """
        types = self.types
        for k, t in enumerate(types):
            if t.code is TC.Single:
                if k + 1 < len(types) and types[k + 1].code is TC.U64:
                    return Platform.Win64
                break
        return Platform.Win32

    def _read_extended(self, reader: StructReader) -> x87.Real:
        """
        An Extended constant, which is a Double in a script for a 64-bit Setup.
        """
        if self._platform() is Platform.Win64:
            return reader.f64()
        return extended(reader.read(10))

    def _name_types(self, missing_types: set[str] | None = None):
        tbn: dict[str, IFPSType] = CaseInsensitiveDict()
        self.types_by_name = tbn
        for t in self.types:
            name = str(t)
            code = t.code
            known = self._known_type_names.get(code)
            if known:
                known.discard(name)
            tbn[name] = t

        if missing_types:
            def add_type(name: str, type: IFPSType):
                tbn[name] = type
                self.types.append(type)
                return type

            def make_string(name: str = 'String'):
                try:
                    return tbn[name]
                except KeyError:
                    pass
                code = TC.UnicodeString if self.unicode else TC.AnsiString
                for type in tbn.values():
                    if type.code is code:
                        break
                else:
                    return add_type(name, TPrimitive(code, symbol=name))
                tbn[name] = type
                return type

            for name in tbn:
                missing_types.discard(name)

            if 'TGUID' in missing_types:
                missing_types |= {'LongWord', 'Word', 'Byte'}

            for code in TC:
                name = code.name
                if name not in missing_types:
                    continue
                missing_types.discard(name)
                add_type(name, TPrimitive(code, symbol=name))

            for code, names in self._known_type_names.items():
                for name in names:
                    if name not in missing_types:
                        continue
                    missing_types.discard(name)
                    add_type(name, TPrimitive(code, symbol=name))

            for name in [
                'TVarType',
                'TInputQueryWizardPage',
                'TInputOptionWizardPage',
                'TInputDirWizardPage',
                'TInputFileWizardPage',
                'TOutputMsgWizardPage',
                'TOutputMsgMemoWizardPage',
                'TOutputProgressWizardPage',
                'TOutputMarqueeProgressWizardPage',
                'TDownloadWizardPage',
                'ExtractionWizardPage',
                'TWizardPage',
                'TSetupForm',
                'TComponent',
                'TNewNotebookPage',
            ]:
                if name not in missing_types:
                    continue
                add_type(name, TClass(TC.Class, name, symbol=name))

            if (name := 'String') in missing_types:
                make_string(name)

            if (name := 'TArrayOfString') in missing_types:
                add_type(name, TArray(TC.Array, make_string()))

            if (name := 'IUnknown') in missing_types:
                add_type(name, TInterface(TC.Interface, UUID('{00000000-0000-0000-C000-000000000046}')))

            if (name := 'TGUID') in missing_types:
                add_type(name, TRecord(TC.Record, (
                    tbn['LongWord'],
                    tbn['Word'],
                    tbn['Word'],
                    TStaticArray(TC.StaticArray, tbn['Byte'], 8)
                ), symbol=name))

    def _load_types(self):
        def _normalize(n: str):
            return IFPSClasses.Types.get(n.casefold(), n)
        reader = self.reader
        types = self.types
        for k in range(self.count_types):
            typecode = reader.u8()
            exported = bool(typecode & 0x80)
            typecode = typecode & 0x7F
            try:
                code = TC(typecode)
            except ValueError as V:
                raise ValueError(F'Unknown type code value 0x{typecode:02X}.') from V
            if code in (TC.Class, TC.ExtClass):
                t = TClass(code, _normalize(reader.read_length_prefixed_ascii()))
            elif code is TC.ProcPtr:
                spec = reader.read_length_prefixed()
                void = bool(spec[0])
                args = tuple(DeclSpecParam(not b) for b in spec[1:])
                t = TProcPtr(code, void, args)
            elif code is TC.Interface:
                guid = UUID(bytes=bytes(reader.read(0x10)))
                t = TInterface(code, guid)
            elif code is TC.Set:
                t = TSet(code, reader.u32())
            elif code is TC.StaticArray:
                type = types[reader.u32()]
                size = reader.u32()
                offset = None if self.version <= 22 else reader.u32()
                t = TStaticArray(code, type, size, offset)
            elif code is TC.Array:
                t = TArray(code, types[reader.u32()])
            elif code is TC.Record:
                length = reader.u32()
                members = tuple(types[reader.u32()] for _ in range(length))
                t = TRecord(code, members, symbol=F'RECORD{k}')
            else:
                t = TPrimitive(code, symbol=code.name)
            if exported:
                t.symbol = _normalize(reader.read_length_prefixed_ascii())
                if self.version <= 21:
                    t.name = _normalize(reader.read_length_prefixed_ascii())
            types.append(t)
            if self.version >= 21:
                t.attributes = list(self._read_attributes())

    def _read_value(self, reader: StructReader | None = None) -> Value:
        if reader is None:
            reader = self.reader
        type = self.types[reader.u32()]
        size = type.code.width
        processor: Callable[[], int | float | str | bytes] | None = {
            TC.U08           : reader.u8,
            TC.S08           : reader.i8,
            TC.U16           : reader.u16,
            TC.S16           : reader.i16,
            TC.U32           : reader.u32,
            TC.S32           : reader.i32,
            TC.S64           : reader.i64,
            TC.U64           : reader.u64,
            TC.Single        : reader.f32,
            TC.Double        : reader.f64,
            TC.Extended      : lambda: self._read_extended(reader),
            TC.AnsiString    : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.PChar         : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.WideString    : reader.read_length_prefixed_utf16,
            TC.UnicodeString : reader.read_length_prefixed_utf16,
            TC.Char          : lambda: chr(reader.u8()),
            TC.WideChar      : lambda: chr(reader.u16()),
            TC.ProcPtr       : lambda: self.functions[reader.u32()],
            TC.Set           : lambda: int.from_bytes(reader.read(type.size_in_bytes), 'little'),
            TC.Currency      : lambda: Fraction(reader.i64(), 10_000),
        }.get(type.code, None)
        if processor is not None:
            data = processor()
        elif size > 0:
            data = bytes(reader.read(size))
        else:
            raise ValueError(F'Unable to read attribute of type {type!s}.')
        if isinstance(data, str):
            self._strings[data] = None
        return Value(type, data)

    def _read_attributes(self) -> Generator[FunctionAttribute]:
        reader = self.reader
        count = reader.u32()
        for _ in range(count):
            name = reader.read_length_prefixed_ascii()
            fields = tuple(self._read_value() for _ in range(reader.u32()))
            yield FunctionAttribute(name, fields)

    def _load_functions(self):
        reader = self.reader
        width = len(F'{self.count_functions:X}')
        dll_imports: list[tuple[Function, bytes]] = []
        max_argc = len(reader) // _ARGUMENT_SIZE

        for k in range(self.count_functions):
            decl = None
            body = None
            name = F'F{k:0{width}X}'
            tags = reader.u8()
            attributes = None
            exported = FTag.Exported.check(tags)
            spec = None
            if FTag.External.check(tags):
                name = reader.read_length_prefixed_ascii(8)
                if exported:
                    spec = bytes(reader.read_length_prefixed())
                    if not spec.startswith(B'dll:'):
                        decl = DeclSpec.ParseF(StructReader(spec), 0, max_argc)
            else:
                offset = reader.u32()
                length = reader.u32()
                if exported:
                    name = reader.read_length_prefixed_ascii()
                    decl = DeclSpec.ParseE(bytes(reader.read_length_prefixed()), self)
                with reader.detour(offset):
                    body = reader.read(length)
            if FTag.HasAttrs.check(tags):
                attributes = list(self._read_attributes())
            fn = Function(name, decl=decl, body=body, attributes=attributes)
            self.functions.append(fn)
            if decl is None and spec is not None:
                dll_imports.append((fn, spec))

        for function in self.functions:
            if body := function.body:
                void = decl.void if (decl := function.decl) else False
                function.body = list(self._parse_bytecode(body, void))

        calls: dict[int, list[_CallSite]] = {}

        for function in self.functions:
            body = function.body or ()
            for k, instruction in enumerate(body):
                if instruction.opcode is not Op.Call:
                    continue
                callee = instruction.operands[0] = self.functions[instruction.operands[0]]
                pops = 0
                for after in itertools.islice(body, k + 1, None):
                    if after.opcode is not Op.Pop:
                        break
                    pops += 1
                result = k > 0 and body[k - 1].opcode is Op.PushVar
                calls.setdefault(id(callee), []).append(_CallSite(result, pops))

        self._load_dll_imports(dll_imports, calls, max_argc)

        missing_types = set()
        signatures: list[tuple[Function, IFPSSignature]] = []

        for function in self.functions:
            decl = function.decl
            if (
                decl is not None
                and not decl.parameters
                and not decl.module
                and not decl.classname
                and function.body is None
                and (builtin := _BUILTIN_PARAMETERS.get(function.name.casefold()))
            ):
                decl.parameters = [DeclSpecParam(p.const, name=p.name) for p in builtin]
            if (signature := self._signature(function, missing_types)) is None:
                continue
            if decl is None:
                decl = function.decl = DeclSpec(
                    signature.void or any(not call.result for call in calls.get(id(function), ())),
                    [DeclSpecParam(p.const) for p in signature.parameters],
                )
            function.symbol = decl.name = signature.name
            if not _shapes_agree(decl, signature):
                continue
            signatures.append((function, signature))
            for old, new in zip(decl.parameters, signature.parameters):
                if (t := old.type) is not None:
                    t.symbol = new.type
                elif t := self.types_by_name.get(new.type):
                    t.symbol = new.type
                else:
                    missing_types.add(new.type)
            if sr := signature.return_type:
                if (dr := decl.return_type) or (dr := self.types_by_name.get(sr)):
                    dr.symbol = sr
                else:
                    missing_types.add(sr)

        self.type_name_conflicts = self._name_types(missing_types)

        for function, signature in signatures:
            decl = function.decl
            assert decl is not None
            for old, new in zip(decl.parameters, signature.parameters):
                if old.type is None:
                    old.type = self.types_by_name.get(new.type)
                old.name = new.name or old.name
            if (rt := signature.return_type) and (decl.return_type is None):
                decl.return_type = self.types_by_name.get(rt)

    def _load_dll_imports(
        self,
        imports: list[tuple[Function, bytes]],
        calls: dict[int, list[_CallSite]],
        max_argc: int,
    ):
        """
        Parses the declarations of the DLL imports. Depending on its revision, Pascal Script wrote
        zero, one or two load flags into them, independent of the version of the script. Unless the
        caller gave the count, it is the one that the calls of the imports contradict least, then
        the one that leaves the fewest extra pops after the calls, then the larger one. A count for
        which a declaration cannot be parsed is out.
        """
        def parse(load_flags: int) -> list[DeclSpec]:
            return [DeclSpec.ParseF(StructReader(spec), load_flags, max_argc) for _, spec in imports]

        def fit(load_flags: int) -> tuple[int, int, int]:
            contradictions = slack = 0
            for (function, _), decl in zip(imports, candidates[load_flags]):
                for call in calls.get(id(function), ()):
                    if call.contradicts(decl):
                        contradictions += 1
                    else:
                        slack += call.slack(decl)
            return contradictions, slack, -load_flags

        if (load_flags := self.load_flags) is None:
            candidates: dict[int, list[DeclSpec]] = {}
            for count in (0, 1, 2):
                try:
                    candidates[count] = parse(count)
                except EOFError:
                    continue
            if not candidates:
                raise ValueError('The declarations of the DLL imports do not parse with any count of load flags.')
            load_flags = min(candidates, key=fit)
            decls = candidates[load_flags]
        else:
            decls = parse(load_flags)
        for (function, _), decl in zip(imports, decls):
            function.decl = decl

    def _signature(self, function: Function, missing_types: set[str]) -> IFPSSignature | None:
        """
        The declaration that Inno Setup documents for the given function: an event for a function
        of the script, a member of a class for a method or a property, else one of the functions
        that Inno Setup and Pascal Script provide. Imports from a DLL have none.
        """
        decl = function.decl
        if function.body is not None:
            return IFPSEvents.get(function.name)
        if decl is not None and decl.module is not None:
            return None
        if decl is not None and decl.classname:
            if (info := IFPSClasses.Classes.get(decl.classname)) is None:
                return None
            if info.name not in self.types_by_name:
                missing_types.add(info.name)
            decl.classname = info.name
            return info.members.get(decl.name)
        return IFPSAPI.get(function.name)

    def _load_variables(self):
        reader = self.reader
        for index in range(self.count_variables):
            code = reader.u32()
            spec = VariableSpec(index, VariableType.Global)
            if reader.u8() & 1:
                spec = reader.read_length_prefixed_ascii()
            self.globals.append(VariableBase(self.types[code], spec))

    def _read_variable_spec(self, index: int, void: bool) -> VariableSpec:
        if index < 0x40000000:
            return VariableSpec(index, VariableType.Global)
        index -= 0x60000000
        if index >= 0:
            return VariableSpec(index, VariableType.Local)
        index = -index if void else ~index
        return VariableSpec(index, VariableType.Argument)

    def _read_operand(self, reader: StructReader, void: bool) -> Operand:
        ot = OperandType(reader.u8())
        kw = {}
        if ot is OperandType.Variable:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
        if ot is OperandType.Value:
            kw.update(value=self._read_value(reader))
        if ot >= OperandType.IndexedByInt:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
            index = reader.u32()
            if ot is OperandType.IndexedByVar:
                index = self._read_variable_spec(index, void)
            kw.update(index=index)
        return Operand(ot, **kw)

    def _parse_bytecode(self, data: memoryview, void: bool) -> Generator[Instruction]:
        disassembly: dict[int, Instruction] = OrderedDict()
        reader = StructReader(data)

        argcount = {
            Op.Assign: 2,
            Op.CallVar: 1,
            Op.Dec: 1,
            Op.Inc: 1,
            Op.BooleanNot: 1,
            Op.Neg: 1,
            Op.IntegerNot: 1,
            Op.SetPtrToCopy: 2,
            Op.SetPtr: 2,
        }

        while not reader.eof:
            def arg(k=1):
                for _ in range(k):
                    args.append(self._read_operand(reader, void))
            addr = reader.tell()
            cval = reader.u8()
            code = Op.FromInt(cval)
            insn = Instruction(addr, code)
            args = insn.operands
            disassembly[insn.offset] = insn
            aryness = argcount.get(code)
            if aryness is not None:
                arg(aryness)
            elif code in (Op.Ret, Op.Nop, Op.Pop):
                pass
            elif code is Op.Calculate:
                insn.operator = AOp(reader.u8())
                arg(2)
            elif code in (Op.Push, Op.PushVar):
                arg()
            elif code in (Op.Jump, Op.JumpFlag):
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.Call:
                args.append(reader.u32())
            elif code in (Op.JumpTrue, Op.JumpFalse):
                target = reader.i32()
                val = self._read_operand(reader, void)
                args.append(reader.tell() + target)
                args.append(val)
            elif code is Op.JumpPop1:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.JumpPop2:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.StackType:
                args.append(self._read_variable_spec(reader.u32(), void))
                args.append(reader.u32())
            elif code is Op.PushType:
                args.append(self.types[reader.u32()])
            elif code is Op.Compare:
                insn.operator = COp(reader.u8())
                arg(3)
            elif code is Op.SetFlag:
                arg()
                args.append(bool(reader.u8()))
            elif code is Op.PushEH:
                args.extend(reader.i32() for _ in range(4))
                for k, a in enumerate(args):
                    args[k] = a + reader.tell() if a >= 0 else None
            elif code is Op.PopEH:
                args.append(reader.u8())
            elif code is Op._INVALID:
                raise ValueError(F'Unsupported opcode: 0x{cval:02X}')
            else:
                raise ValueError(F'Unhandled opcode: {code.name}')
            size = reader.tell() - addr
            reader.seekrel(-size)
            insn.encoded = bytes(reader.read(size))

        for k, instruction in enumerate(disassembly.values()):
            if not instruction.branches:
                continue
            target = instruction.operands[0]
            try:
                disassembly[target].jumptarget = True
            except KeyError as K:
                raise RuntimeError(
                    F'The jump target of instruction {k} at 0x{instruction.offset:X} is invalid; '
                    F'the invalid instruction is a {instruction.opcode.name} to 0x{target:X}.'
                ) from K

        yield from disassembly.values()

    def __str__(self):
        return self.disassembly()

    def disassembly(self, print_bytes: bool = False, print_bytes_count: int = 12) -> str:
        def sortkey(f: Function):
            d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
            return (*d, f.name)

        for function in self.functions:
            function.get_basic_blocks()

        classes: dict[str, dict[str, Function]] = {}
        external: list[Function] = []
        internal: list[Function] = []

        for t in self.types:
            if isinstance(t, TClass):
                classes[t.name] = {}

        for function in self.functions:
            if (decl := function.decl) and (name := decl.classname):
                try:
                    members = classes[name]
                except KeyError:
                    members = classes[name] = {}
                members[decl.name] = function
                continue
            dl = internal if function.body else external
            dl.append(function)

        external.sort(key=sortkey)

        output = io.StringIO()
        _omax = max((
            max(insn.offset for insn in fn.body)
            for fn in self.functions if fn.body
        ), default=0)
        _smax = max((
            max((insn.stack for insn in fn.body if insn.stack is not None), default=1)
            for fn in self.functions if fn.body
        ), default=0)
        _omax = max(len(self.types), len(self.globals), _omax)
        _omax = len(F'{_omax:X}')
        _smax = len(F'{_smax:d}')

        if classes:
            for name, members in classes.items():
                if not members:
                    output.write(F'external class {name};\n')
            output.write('\n')
            for name, members in classes.items():
                if not members:
                    continue
                output.write(F'external class {name}')
                if members:
                    for spec in members.values():
                        output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                    output.write('\nend')
                output.write(';\n\n')

        if self.types:
            typedefs = []
            for type in self.types:
                if type.code != TC.Record and type.symbol in (type.code.name, None):
                    continue
                if isinstance(type, TClass):
                    continue
                typedefs.append((type.symbol, type.display()))
            typedefs.sort()
            for symbol, display in typedefs:
                output.write(F'typedef {symbol} = {display}\n')
            output.write('\n')

        if self.globals:
            for variable in self.globals:
                output.write(F'global {variable!s}\n')
            output.write('\n')

        if external:
            for function in external:
                output.write(F'external {function!r}\n')
            output.write('\n')

        if internal:
            def create_prefix(instruction: Instruction):
                stack = instruction.stack
                stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
                return F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}'

            for function in internal:
                labels = [insn.offset for insn in function.body if insn.jumptarget]
                labelw = max(len(str(len(labels))), 2)
                labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}

                output.write(F'{function!r}\nbegin\n')
                labelc = 0

                for instruction in function.body:
                    prefix = create_prefix(instruction)
                    if instruction.jumptarget:
                        output.write(F'{labeld[labels[labelc]]}:\n')
                        labelc += 1
                    if print_bytes:
                        hexbytes = instruction.encoded.hex(' ').split()
                    else:
                        hexbytes = ['']
                    hexbytes_iter = iter(hexbytes)
                    instruction_written = False
                    prefix_length = len(prefix)
                    ic = instruction.pretty(labeld)
                    while line := list(itertools.islice(hexbytes_iter, 0, print_bytes_count)):
                        output.write(prefix)
                        if print_bytes:
                            prefix = prefix_length * '\x20'
                            output.write('\x20'.join(line).ljust(3 * print_bytes_count - 1))
                        if not instruction_written:
                            output.write(ic)
                            instruction_written = True
                        output.write('\n')

                output.write('end;\n\n')

        return output.getvalue().strip()

Global variables

var IFPSType

Represents any of the possible IFPS data types:

Functions

def extended(_data)

Parses the 10 bytes of an x87 extended-precision float into its exact value.

Expand source code Browse git
def extended(_data: bytes) -> x87.Real:
    """
    Parses the 10 bytes of an x87 extended-precision float into its exact value.
    """
    if len(_data) != 10:
        raise ValueError
    data = int.from_bytes(_data, 'little')
    negative = bool(data >> 79)
    exponent = (data >> 64) & 0x7FFF
    mantissa = data & 0xFFFFFFFFFFFFFFFF
    if exponent == 0x7FFF:
        if mantissa & 0x7FFFFFFFFFFFFFFF == 0:
            return -math.inf if negative else math.inf
        return math.nan
    if mantissa == 0:
        return -0.0 if negative else 0.0
    return x87.real(-mantissa if negative else mantissa, max(exponent, 1) - 16383 - 63)
def represent(cls)

A decorator for various IFPS integer enumeration classes to change the default string representation.

Expand source code Browse git
def represent(cls: _E) -> _E:
    """
    A decorator for various IFPS integer enumeration classes to change the default string
    representation.
    """
    cls.__repr__ = lambda self: F'{self.__class__.__name__}.{self.name}'
    cls. __str__ = lambda self: self.name
    return cls

Classes

class Op (*args, **kwds)

An enumeration of all known IFPS opcodes.

Expand source code Browse git
@represent
class Op(enum.IntEnum):
    """
    An enumeration of all known IFPS opcodes.
    """
    Assign       = 0x00  # noqa
    Calculate    = 0x01  # noqa
    Push         = 0x02  # noqa
    PushVar      = 0x03  # noqa
    Pop          = 0x04  # noqa
    Call         = 0x05  # noqa
    Jump         = 0x06  # noqa
    JumpTrue     = 0x07  # noqa
    JumpFalse    = 0x08  # noqa
    Ret          = 0x09  # noqa
    StackType    = 0x0A  # noqa
    PushType     = 0x0B  # noqa
    Compare      = 0x0C  # noqa
    CallVar      = 0x0D  # noqa
    SetPtr       = 0x0E  # noqa
    BooleanNot   = 0x0F  # noqa
    Neg          = 0x10  # noqa
    SetFlag      = 0x11  # noqa
    JumpFlag     = 0x12  # noqa
    PushEH       = 0x13  # noqa
    PopEH        = 0x14  # noqa
    IntegerNot   = 0x15  # noqa
    SetPtrToCopy = 0x16  # noqa
    Inc          = 0x17  # noqa
    Dec          = 0x18  # noqa
    JumpPop1     = 0x19  # noqa
    JumpPop2     = 0x1A  # noqa
    Nop          = 0xFF  # noqa
    _INVALID     = 0xDD  # noqa

    @classmethod
    def FromInt(cls, code: int):
        try:
            return cls(code)
        except ValueError:
            return cls._INVALID

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var Assign

The type of the None singleton.

var Calculate

The type of the None singleton.

var Push

The type of the None singleton.

var PushVar

The type of the None singleton.

var Pop

The type of the None singleton.

var Call

The type of the None singleton.

var Jump

The type of the None singleton.

var JumpTrue

The type of the None singleton.

var JumpFalse

The type of the None singleton.

var Ret

The type of the None singleton.

var StackType

The type of the None singleton.

var PushType

The type of the None singleton.

var Compare

The type of the None singleton.

var CallVar

The type of the None singleton.

var SetPtr

The type of the None singleton.

var BooleanNot

The type of the None singleton.

var Neg

The type of the None singleton.

var SetFlag

The type of the None singleton.

var JumpFlag

The type of the None singleton.

var PushEH

The type of the None singleton.

var PopEH

The type of the None singleton.

var IntegerNot

The type of the None singleton.

var SetPtrToCopy

The type of the None singleton.

var Inc

The type of the None singleton.

var Dec

The type of the None singleton.

var JumpPop1

The type of the None singleton.

var JumpPop2

The type of the None singleton.

var Nop

The type of the None singleton.

Static methods

def FromInt(code)
class Dialect (*args, **kwds)

The versions of Pascal Script whose rules for values differ, named by the Inno Setup that ships them: the same rules from 5.5.3 to 6.7, and new ones from 7.0 on.

Expand source code Browse git
class Dialect(enum.Enum):
    """
    The versions of Pascal Script whose rules for values differ, named by the Inno Setup that ships
    them: the same rules from 5.5.3 to 6.7, and new ones from 7.0 on.
    """
    Inno6 = 6
    Inno7 = 7

Ancestors

  • enum.Enum

Class variables

var Inno6

The type of the None singleton.

var Inno7

The type of the None singleton.

class AOp (*args, **kwds)

An enumeration of all known IFPS arithmetic opcodes.

Expand source code Browse git
class AOp(enum.IntEnum):
    """
    An enumeration of all known IFPS arithmetic opcodes.
    """
    Add = 0
    Sub = 1
    Mul = 2
    Div = 3
    Mod = 4
    Shl = 5
    Shr = 6
    And = 7
    BOr = 8
    Xor = 9
    As = 10

    def __str__(self):
        if self is AOp.As:
            return 'as'
        glyph = ('+', '-', '*', '/', '%', '<<', '>>', '&', '|', '^')[self]
        return F'{glyph}='

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var Add

The type of the None singleton.

var Sub

The type of the None singleton.

var Mul

The type of the None singleton.

var Div

The type of the None singleton.

var Mod

The type of the None singleton.

var Shl

The type of the None singleton.

var Shr

The type of the None singleton.

var And

The type of the None singleton.

var BOr

The type of the None singleton.

var Xor

The type of the None singleton.

var As

The type of the None singleton.

class COp (*args, **kwds)

An enumeration of all known IFPS comparison opcodes.

Expand source code Browse git
class COp(enum.IntEnum):
    """
    An enumeration of all known IFPS comparison opcodes.
    """
    GE = 0
    LE = 1
    GT = 2
    LT = 3
    NE = 4
    EQ = 5
    IN = 6
    IS = 7

    def __str__(self):
        return ('>=', '<=', '>', '<', '!=', '==', 'in', 'is')[self]

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var GE

The type of the None singleton.

var LE

The type of the None singleton.

var GT

The type of the None singleton.

var LT

The type of the None singleton.

var NE

The type of the None singleton.

var EQ

The type of the None singleton.

var IN

The type of the None singleton.

var IS

The type of the None singleton.

class TC (*args, **kwds)

An enumeration of all known IFPS type codes.

Expand source code Browse git
@represent
class TC(enum.IntEnum):
    """
    An enumeration of all known IFPS type codes.
    """
    ReturnAddress       = 0x00  # noqa
    U08                 = 0x01  # noqa
    S08                 = 0x02  # noqa
    U16                 = 0x03  # noqa
    S16                 = 0x04  # noqa
    U32                 = 0x05  # noqa
    S32                 = 0x06  # noqa
    Single              = 0x07  # noqa
    Double              = 0x08  # noqa
    Extended            = 0x09  # noqa
    AnsiString          = 0x0A  # noqa
    Record              = 0x0B  # noqa
    Array               = 0x0C  # noqa
    Pointer             = 0x0D  # noqa
    PChar               = 0x0E  # noqa
    ResourcePointer     = 0x0F  # noqa
    Variant             = 0x10  # noqa
    S64                 = 0x11  # noqa
    Char                = 0x12  # noqa
    WideString          = 0x13  # noqa
    WideChar            = 0x14  # noqa
    ProcPtr             = 0x15  # noqa
    StaticArray         = 0x16  # noqa
    Set                 = 0x17  # noqa
    Currency            = 0x18  # noqa
    Class               = 0x19  # noqa
    Interface           = 0x1A  # noqa
    NotificationVariant = 0x1B  # noqa
    UnicodeString       = 0x1C  # noqa
    U64                 = 0x1D  # noqa
    Enum                = 0x81  # noqa
    Type                = 0x82  # noqa
    ExtClass            = 0x83  # noqa

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the code represents a primitive type.
        """
        return self not in _TC_NON_PRIMITIVE

    @property
    def container(self) -> bool:
        """
        Indicates whether the code represents a container type.
        """
        return self in _TC_CONTAINER

    @property
    def width(self):
        """
        For primitive types, this gives the size of an immediate of this type in bytes.
        """
        return _TC_WIDTH.get(self, 0)

    @property
    def integer_range(self) -> range | None:
        """
        For integer types, this gives the range of values that an immediate of this type can hold.
        """
        return _TC_INTEGER_RANGE.get(self)

    @property
    def integer(self) -> bool:
        """
        Indicates whether the code represents an integer type.
        """
        return self in _TC_INTEGER_RANGE

    @property
    def signed(self) -> bool:
        """
        Indicates whether the code represents a signed integer type.
        """
        return self in _TC_SIGNED

    @property
    def character(self) -> bool:
        """
        Indicates whether the code represents a character type.
        """
        return self in _TC_CHARACTER

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var ReturnAddress

The type of the None singleton.

var U08

The type of the None singleton.

var S08

The type of the None singleton.

var U16

The type of the None singleton.

var S16

The type of the None singleton.

var U32

The type of the None singleton.

var S32

The type of the None singleton.

var Single

The type of the None singleton.

var Double

The type of the None singleton.

var Extended

The type of the None singleton.

var AnsiString

The type of the None singleton.

var Record

The type of the None singleton.

var Array

The type of the None singleton.

var Pointer

The type of the None singleton.

var PChar

The type of the None singleton.

var ResourcePointer

The type of the None singleton.

var Variant

The type of the None singleton.

var S64

The type of the None singleton.

var Char

The type of the None singleton.

var WideString

The type of the None singleton.

var WideChar

The type of the None singleton.

var ProcPtr

The type of the None singleton.

var StaticArray

The type of the None singleton.

var Set

The type of the None singleton.

var Currency

The type of the None singleton.

var Class

The type of the None singleton.

var Interface

The type of the None singleton.

var NotificationVariant

The type of the None singleton.

var UnicodeString

The type of the None singleton.

var U64

The type of the None singleton.

var Enum

The type of the None singleton.

var Type

The type of the None singleton.

var ExtClass

The type of the None singleton.

Instance variables

var primitive

Indicates whether the code represents a primitive type.

Expand source code Browse git
@property
def primitive(self) -> bool:
    """
    Indicates whether the code represents a primitive type.
    """
    return self not in _TC_NON_PRIMITIVE
var container

Indicates whether the code represents a container type.

Expand source code Browse git
@property
def container(self) -> bool:
    """
    Indicates whether the code represents a container type.
    """
    return self in _TC_CONTAINER
var width

For primitive types, this gives the size of an immediate of this type in bytes.

Expand source code Browse git
@property
def width(self):
    """
    For primitive types, this gives the size of an immediate of this type in bytes.
    """
    return _TC_WIDTH.get(self, 0)
var integer_range

For integer types, this gives the range of values that an immediate of this type can hold.

Expand source code Browse git
@property
def integer_range(self) -> range | None:
    """
    For integer types, this gives the range of values that an immediate of this type can hold.
    """
    return _TC_INTEGER_RANGE.get(self)
var integer

Indicates whether the code represents an integer type.

Expand source code Browse git
@property
def integer(self) -> bool:
    """
    Indicates whether the code represents an integer type.
    """
    return self in _TC_INTEGER_RANGE
var signed

Indicates whether the code represents a signed integer type.

Expand source code Browse git
@property
def signed(self) -> bool:
    """
    Indicates whether the code represents a signed integer type.
    """
    return self in _TC_SIGNED
var character

Indicates whether the code represents a character type.

Expand source code Browse git
@property
def character(self) -> bool:
    """
    Indicates whether the code represents a character type.
    """
    return self in _TC_CHARACTER
class IFPSTypeBase (code, *, symbol=None, attributes=None)

The base class for any IFPS type.

Expand source code Browse git
@dataclass
class IFPSTypeBase(abc.ABC):
    """
    The base class for any IFPS type.
    """
    code: TC
    symbol: str | None = field(default=None, kw_only=True)
    attributes: list[FunctionAttribute] | None = field(default=None, kw_only=True)

    def simple(self, nested: bool = False) -> bool:
        """
        Indicate whether the type requires more than one line to pretty print.
        """
        return True

    def display(self, indent=0):
        """
        Compute a display string that can be used to represent the type in disassembly.
        """
        return indent * _TAB + self.code.name

    @property
    def primitive(self) -> bool:
        """
        Indicates whether the type is primitive.
        """
        return self.code.primitive

    @property
    def container(self) -> bool:
        """
        Indicates whether the type is a container.
        """
        return self.code.container

    @property
    def boolean(self) -> bool:
        """
        Indicates whether the type is the Boolean type of Pascal Script: a byte that the runtime
        recognizes by its exported name and converts into a truth value for a Variant.
        """
        return self.code is TC.U08 and (self.symbol or '').upper() == 'BOOLEAN'

    def __str__(self):
        if self.symbol is not None:
            return self.symbol
        return self.display(0)

Ancestors

  • abc.ABC

Subclasses

Instance variables

var code

The type of the None singleton.

var symbol

The type of the None singleton.

var attributes

The type of the None singleton.

var primitive

Indicates whether the type is primitive.

Expand source code Browse git
@property
def primitive(self) -> bool:
    """
    Indicates whether the type is primitive.
    """
    return self.code.primitive
var container

Indicates whether the type is a container.

Expand source code Browse git
@property
def container(self) -> bool:
    """
    Indicates whether the type is a container.
    """
    return self.code.container
var boolean

Indicates whether the type is the Boolean type of Pascal Script: a byte that the runtime recognizes by its exported name and converts into a truth value for a Variant.

Expand source code Browse git
@property
def boolean(self) -> bool:
    """
    Indicates whether the type is the Boolean type of Pascal Script: a byte that the runtime
    recognizes by its exported name and converts into a truth value for a Variant.
    """
    return self.code is TC.U08 and (self.symbol or '').upper() == 'BOOLEAN'

Methods

def simple(self, nested=False)

Indicate whether the type requires more than one line to pretty print.

Expand source code Browse git
def simple(self, nested: bool = False) -> bool:
    """
    Indicate whether the type requires more than one line to pretty print.
    """
    return True
def display(self, indent=0)

Compute a display string that can be used to represent the type in disassembly.

Expand source code Browse git
def display(self, indent=0):
    """
    Compute a display string that can be used to represent the type in disassembly.
    """
    return indent * _TAB + self.code.name
class TPrimitive (code, *, symbol=None, attributes=None)

A primitive IFPS type.

Expand source code Browse git
@dataclass
class TPrimitive(IFPSTypeBase):
    """
    A primitive IFPS type.
    """

Ancestors

Inherited members

class TProcPtr (code, void, args, *, symbol=None, attributes=None)

The procedure pointer IFPS type.

Expand source code Browse git
@dataclass
class TProcPtr(IFPSTypeBase):
    """
    The procedure pointer IFPS type.
    """
    void: bool
    args: tuple[DeclSpecParam, ...]

    def display(self, indent=0):
        name = super().display(indent)
        args = []
        for k, spec in enumerate(self.args, 1):
            arg = F'Arg{k}'
            if not spec.const:
                arg = F'*{arg}'
            if spec.type is not None:
                arg = F'{spec.type!s} {arg}'
            args.append(arg)
        args = ', '.join(args)
        return F'{name}({args})'

Ancestors

Instance variables

var void

The type of the None singleton.

var args

The type of the None singleton.

Inherited members

class TInterface (code, uuid, *, symbol=None, attributes=None)

An IFPS type representing a COM interface.

Expand source code Browse git
@dataclass
class TInterface(IFPSTypeBase):
    """
    An IFPS type representing a COM interface.
    """
    uuid: UUID

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.uuid!s})'

Ancestors

Instance variables

var uuid

The type of the None singleton.

Inherited members

class TClass (code, name, *, symbol=None, attributes=None)

An IFPS type representing an IFPS class.

Expand source code Browse git
@dataclass
class TClass(IFPSTypeBase):
    """
    An IFPS type representing an IFPS class.
    """
    name: str

Ancestors

Instance variables

var name

The type of the None singleton.

Inherited members

class TSet (code, size, *, symbol=None, attributes=None)

An IFPS type representing a bit vector.

Expand source code Browse git
@dataclass
class TSet(IFPSTypeBase):
    """
    An IFPS type representing a bit vector.
    """
    size: int

    @property
    def size_in_bytes(self):
        q, r = divmod(self.size, 8)
        return q + (r and 1 or 0)

    def display(self, indent=0):
        display = super().display(indent)
        return F'{display}({self.size})'

Ancestors

Instance variables

var size

The type of the None singleton.

var size_in_bytes
Expand source code Browse git
@property
def size_in_bytes(self):
    q, r = divmod(self.size, 8)
    return q + (r and 1 or 0)

Inherited members

class TArray (code, type, *, symbol=None, attributes=None)

An IFPS type representing a dynamic array.

Expand source code Browse git
@dataclass
class TArray(IFPSTypeBase):
    """
    An IFPS type representing a dynamic array.
    """
    type: IFPSType

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'array of {display}'

    def simple(self, nested: bool = False) -> bool:
        return self.type.simple(nested)

Ancestors

Instance variables

var type

The type of the None singleton.

Inherited members

class TStaticArray (code, type, size, offset=None, *, symbol=None, attributes=None)

An IFPS type representing a static array (i.e. a tuple).

Expand source code Browse git
@dataclass
class TStaticArray(IFPSTypeBase):
    """
    An IFPS type representing a static array (i.e. a tuple).
    """
    type: IFPSType
    size: int
    offset: int | None = None

    def display(self, indent=0):
        display = F'{_TAB * indent}{self.type!s}'
        return F'{display}[{self.size}]'

    def simple(self, nested: bool = False) -> bool:
        return self.type.simple(nested)

Ancestors

Instance variables

var type

The type of the None singleton.

var size

The type of the None singleton.

var offset

The type of the None singleton.

Inherited members

class TRecord (code, members, *, symbol=None, attributes=None)

An IFPS type representing a structure.

Expand source code Browse git
@dataclass
class TRecord(IFPSTypeBase):
    """
    An IFPS type representing a structure.
    """
    members: tuple[IFPSType, ...]

    @property
    def size(self):
        return len(self.members)

    def simple(self, nested: bool = False) -> bool:
        if nested:
            return False
        if len(self.members) > 10:
            return False
        return all(m.simple(True) for m in self.members)

    def display(self, indent=0):
        output = io.StringIO()
        output.write(indent * _TAB)
        output.write('struct {')
        if self.simple():
            output.write(', '.join(str(m) for m in self.members))
        else:
            for k, member in enumerate(self.members):
                if k > 0:
                    output.write(',')
                output.write('\n')
                output.write(member.display(indent + 1))
            if self.members:
                output.write(F'\n{_TAB * indent}')
        output.write('}')
        return output.getvalue()

Ancestors

Instance variables

var members

The type of the None singleton.

var size
Expand source code Browse git
@property
def size(self):
    return len(self.members)

Inherited members

class Value (type, value)

A value of the given type within the IFPS runtime.

Expand source code Browse git
class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: str | int | float | x87.Extended | Fraction | bytes | Function

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        if isinstance(v, x87.Extended):
            return numbers.shortest(v)
        if isinstance(v, Fraction):
            return variants.currency_text(v, numbers.FormatSettings())
        return repr(v)

Ancestors

  • builtins.tuple

Instance variables

var type

Alias for field number 0

Expand source code Browse git
class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: str | int | float | x87.Extended | Fraction | bytes | Function

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        if isinstance(v, x87.Extended):
            return numbers.shortest(v)
        if isinstance(v, Fraction):
            return variants.currency_text(v, numbers.FormatSettings())
        return repr(v)
var value

Alias for field number 1

Expand source code Browse git
class Value(NamedTuple):
    """
    A value of the given type within the IFPS runtime.
    """
    type: IFPSType
    value: str | int | float | x87.Extended | Fraction | bytes | Function

    def __repr__(self):
        value = self.value
        if isinstance(value, bytes):
            value = value.hex()
        return F'{self.type.code.name}({value!r})'

    def __str__(self):
        v = self.value
        if isinstance(v, Function):
            return F'&{v!s}'
        if isinstance(v, x87.Extended):
            return numbers.shortest(v)
        if isinstance(v, Fraction):
            return variants.currency_text(v, numbers.FormatSettings())
        return repr(v)
class FunctionAttribute (name, fields)

A function attribute.

Expand source code Browse git
class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name

Ancestors

  • builtins.tuple

Instance variables

var name

Alias for field number 0

Expand source code Browse git
class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name
var fields

Alias for field number 1

Expand source code Browse git
class FunctionAttribute(NamedTuple):
    """
    A function attribute.
    """
    name: str
    fields: tuple[Value, ...]

    def __repr__(self):
        name = self.name
        if self.fields:
            name += '[{}]'.format(','.join(repr(f) for f in self.fields))
        return name
class DeclSpecParam (const, type=None, name=None)

A function parameter specification.

Expand source code Browse git
@dataclass
class DeclSpecParam:
    """
    A function parameter specification.
    """
    const: bool
    """
    True if this parameter is passed by value, not by reference.
    """
    type: IFPSType | None = None
    """
    The type of this parameter.
    """
    name: str | None = None
    """
    The name of this parameter.
    """

Instance variables

var const

True if this parameter is passed by value, not by reference.

var type

The type of this parameter.

var name

The name of this parameter.

class CallType (*args, **kwds)

This enumeration classifies the different call types.

Expand source code Browse git
class CallType(str, enum.Enum):
    """
    This enumeration classifies the different call types.
    """
    Symbol = 'symbol'
    Procedure = 'procedure'
    Function = 'function'

    def __str__(self):
        return self.value

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var Symbol

The type of the None singleton.

var Procedure

The type of the None singleton.

var Function

The type of the None singleton.

class DeclSpec (void, parameters=<factory>, name='', calling_convention=None, return_type=None, module=None, classname=None, method=False, delay_load=False, vtable_index=None, load_with_altered_search_path=False)

This class captures the declaration info of a function symbol.

Expand source code Browse git
@dataclass
class DeclSpec:
    """
    This class captures the declaration info of a function symbol.
    """
    void: bool
    parameters: list[DeclSpecParam] = field(default_factory=list)
    name: str = ''
    calling_convention: str | None = None
    return_type: IFPSType | None = None
    module: str | None = None
    classname: str | None = None
    method: bool = False
    """
    True for a method of a class, which Pascal Script calls with its object before the parameters
    that the declaration lists.
    """
    delay_load: bool = False
    vtable_index: int | None = None
    load_with_altered_search_path: bool = False

    @property
    def argc(self):
        return len(self.parameters)

    def represent(self, name: str, ref: bool = False, rel: bool = False):
        def pparam(k: int, p: DeclSpecParam):
            name = p.name or F'{VariableType.Argument!s}{k}'
            if p.type is not None:
                name = F'{name}: {p.type!s}'
            if not p.const:
                name = F'*{name}'
            return name
        if self.name and name in self.name:
            name = self.name
        spec = name
        if self.vtable_index is not None:
            spec = F'{self.name}[{self.vtable_index}]'
        if not rel and self.classname:
            spec = F'{self.classname}.{spec}'
        if not rel and self.module:
            spec = F'{self.module}::{spec}'
        if not ref:
            if self.delay_load:
                spec = F'__delay_load {spec}'
            if self.calling_convention:
                spec = F'__{self.calling_convention} {spec}'
            spec = F'{self.type} {spec}'
            args = self.parameters
            args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
            spec = F'{spec}({args})'
            if self.return_type:
                spec = F'{spec}: {self.return_type!s}'
        return spec

    @property
    def type(self):
        if self.void:
            return CallType.Procedure
        return CallType.Function

    def __repr__(self):
        return self.represent(self.name or '(*)')

    @classmethod
    def ParseF(cls, reader: StructReader[bytes], load_flags: int, max_argc: int):
        """
        Parses the declaration of an import. Pascal Script writes `load_flags` flag bytes into the
        declaration of a DLL import. The parameter count of a property is a number that no other
        data backs; a count above `max_argc` is refused.
        """
        def ascii():
            return reader.read_c_string('latin1')

        def boolean():
            return bool(reader.u8())

        def cc():
            return {
                0: 'register',
                1: 'pascal',
                2: 'cdecl',
                3: 'stdcall',
            }.get(reader.u8(), cls.calling_convention)

        def read_parameters():
            nonlocal void
            void = not boolean()
            parameters.extend(DeclSpecParam(not b) for b in reader.read())

        void = True
        name = None
        properties = {}
        parameters = []

        if reader.readif(b'dll:'):
            reader.readif(B'files:')
            if (module := ascii()).lower().endswith('.dll'):
                module = module[:-4]
            properties.update(module=module)
            name = ascii()
            properties.update(calling_convention=cc())
            if load_flags > 0:
                properties.update(delay_load=boolean())
            if load_flags > 1:
                properties.update(load_with_altered_search_path=boolean())
            read_parameters()
        elif reader.readif(b'class:'):
            if reader.remaining_bytes == 1:
                spec = reader.peek(1)
                void = False
                parameters.append(DeclSpecParam(False))
                name = {
                    b'+': 'CastToType',
                    B'-': 'SetNil'
                }.get(spec)
                properties.update(classname='Class', calling_convention='pascal')
            else:
                properties.update(classname=reader.read_terminated_array(b'|').decode('latin1'))
                name = reader.read_terminated_array(b'|').decode('latin1')
                if write := name.endswith('@'):
                    name = name[:-1]
                if write or reader.peek(1) != bytes(1):
                    void = write
                    if (argc := reader.u32() + write) > max_argc:
                        raise ValueError(F'The property {name} takes {argc} arguments, more than any call can pass.')
                    parameters.extend(DeclSpecParam(True) for _ in range(argc))
                    name = F'Set{name}' if write else F'Get{name}'
                else:
                    properties.update(calling_convention=cc(), method=True)
                    read_parameters()
        elif reader.readif(b'intf:.'):
            name = 'CoInterface'
            properties.update(vtable_index=reader.u32())
            properties.update(calling_convention=cc())
            read_parameters()
        else:
            read_parameters()
        return cls(void, parameters, name=name, **properties)

    @classmethod
    def ParseE(cls, data: bytes, ipfs: IFPSFile):
        decl = data.split(B'\x20')
        try:
            return_type = int(decl.pop(0))
        except Exception:
            void = True
        else:
            void = return_type < 0
        if not void:
            return_type = ipfs.types[return_type]
        else:
            return_type = None
        parameters = []
        for param in decl:
            try:
                i = int(param[1:])
            except Exception:
                tv = None
            else:
                tv = ipfs.types[i]
            parameters.append(
                DeclSpecParam(param[:1] == B'@', tv))
        return cls(void, parameters, return_type=return_type)

Static methods

def ParseF(reader, load_flags, max_argc)

Parses the declaration of an import. Pascal Script writes load_flags flag bytes into the declaration of a DLL import. The parameter count of a property is a number that no other data backs; a count above max_argc is refused.

def ParseE(data, ipfs)

Instance variables

var void

The type of the None singleton.

var parameters

The type of the None singleton.

var name

The type of the None singleton.

var calling_convention

The type of the None singleton.

var return_type

The type of the None singleton.

var module

The type of the None singleton.

var classname

The type of the None singleton.

var method

True for a method of a class, which Pascal Script calls with its object before the parameters that the declaration lists.

var delay_load

The type of the None singleton.

var vtable_index

The type of the None singleton.

var load_with_altered_search_path

The type of the None singleton.

var argc
Expand source code Browse git
@property
def argc(self):
    return len(self.parameters)
var type
Expand source code Browse git
@property
def type(self):
    if self.void:
        return CallType.Procedure
    return CallType.Function

Methods

def represent(self, name, ref=False, rel=False)
Expand source code Browse git
def represent(self, name: str, ref: bool = False, rel: bool = False):
    def pparam(k: int, p: DeclSpecParam):
        name = p.name or F'{VariableType.Argument!s}{k}'
        if p.type is not None:
            name = F'{name}: {p.type!s}'
        if not p.const:
            name = F'*{name}'
        return name
    if self.name and name in self.name:
        name = self.name
    spec = name
    if self.vtable_index is not None:
        spec = F'{self.name}[{self.vtable_index}]'
    if not rel and self.classname:
        spec = F'{self.classname}.{spec}'
    if not rel and self.module:
        spec = F'{self.module}::{spec}'
    if not ref:
        if self.delay_load:
            spec = F'__delay_load {spec}'
        if self.calling_convention:
            spec = F'__{self.calling_convention} {spec}'
        spec = F'{self.type} {spec}'
        args = self.parameters
        args = args and ', '.join(pparam(*t) for t in enumerate(args, 1)) or ''
        spec = F'{spec}({args})'
        if self.return_type:
            spec = F'{spec}: {self.return_type!s}'
    return spec
class Function (symbol='', decl=None, body=None, attributes=None)

Represents a function in the IFPS runtime.

Expand source code Browse git
@dataclass
class Function:
    """
    Represents a function in the IFPS runtime.
    """
    symbol: str = ''
    decl: DeclSpec | None = None
    body: list[Instruction] | None = None
    attributes: list[FunctionAttribute] | None = None
    _bbs: dict[int, BasicBlock] | None = None
    _ins: dict[int, Instruction] | None = None

    @property
    def name(self):
        symbol = self.symbol
        if (decl := self.decl) and (name := decl.name) and (symbol in name):
            symbol = name
        return symbol

    @property
    def code(self):
        if code := self._ins:
            return code
        self._ins = code = {i.offset: i for i in self.body}
        return code

    def reference(self, rel: bool = False) -> str:
        if self.decl is None:
            return self.symbol
        return self.decl.represent(self.symbol, ref=True, rel=rel)

    def __repr__(self):
        if self.decl is None:
            return F'symbol {self.symbol}'
        return self.decl.represent(self.symbol)

    def __str__(self):
        return self.reference()

    @property
    def type(self):
        if self.decl is None:
            return CallType.Symbol
        return self.decl.type

    def get_basic_blocks(self) -> dict[int, BasicBlock]:
        if (bbs := self._bbs) is not None:
            return bbs
        if self.body is None:
            bbs = self._bbs = {}
            return bbs

        bbs: dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
        self._bbs = bbs
        jump = False

        for insn in self.body:
            try:
                bb = bbs[insn.offset]
            except KeyError:
                if jump or insn.jumptarget:
                    nb = bbs[insn.offset] = BasicBlock(insn.offset)
                    if not jump:
                        nb.sources[bb.offset] = bb
                        bb.targets[nb.offset] = nb
                    bb = nb
            bb.body.append(insn)
            if not insn.branches:
                jump = False
                continue
            targets = [insn.operands[0]]
            sequence = insn.offset + insn.size
            jump = insn.jumps
            if not jump and insn.opcode != Op.Ret:
                targets.append(sequence)
            for t in targets:
                if not (bt := bbs.get(t)):
                    bt = bbs[t] = BasicBlock(t)
                bb.targets[t] = bt
                bt.sources[bb.offset] = bb

        for offset, bb in list(bbs.items()):
            if bb.body:
                continue
            del bbs[offset]
            for source in bb.sources.values():
                source.targets.pop(offset, None)

        _trace_stack(bbs)

        for insn in self.body:
            if (stack := insn.stack) is None:
                continue
            for k, op in enumerate(insn.operands):
                if not isinstance(op, Operand):
                    continue
                if not (v := op.variable) or v.type != VariableType.Local:
                    continue
                if v.index <= stack:
                    continue
                raise IndexError(
                    F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                    F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

        return bbs

Instance variables

var symbol

The type of the None singleton.

var decl

The type of the None singleton.

var body

The type of the None singleton.

var attributes

The type of the None singleton.

var name
Expand source code Browse git
@property
def name(self):
    symbol = self.symbol
    if (decl := self.decl) and (name := decl.name) and (symbol in name):
        symbol = name
    return symbol
var code
Expand source code Browse git
@property
def code(self):
    if code := self._ins:
        return code
    self._ins = code = {i.offset: i for i in self.body}
    return code
var type
Expand source code Browse git
@property
def type(self):
    if self.decl is None:
        return CallType.Symbol
    return self.decl.type

Methods

def reference(self, rel=False)
Expand source code Browse git
def reference(self, rel: bool = False) -> str:
    if self.decl is None:
        return self.symbol
    return self.decl.represent(self.symbol, ref=True, rel=rel)
def get_basic_blocks(self)
Expand source code Browse git
def get_basic_blocks(self) -> dict[int, BasicBlock]:
    if (bbs := self._bbs) is not None:
        return bbs
    if self.body is None:
        bbs = self._bbs = {}
        return bbs

    bbs: dict[int, BasicBlock] = {0: (bb := BasicBlock(0))}
    self._bbs = bbs
    jump = False

    for insn in self.body:
        try:
            bb = bbs[insn.offset]
        except KeyError:
            if jump or insn.jumptarget:
                nb = bbs[insn.offset] = BasicBlock(insn.offset)
                if not jump:
                    nb.sources[bb.offset] = bb
                    bb.targets[nb.offset] = nb
                bb = nb
        bb.body.append(insn)
        if not insn.branches:
            jump = False
            continue
        targets = [insn.operands[0]]
        sequence = insn.offset + insn.size
        jump = insn.jumps
        if not jump and insn.opcode != Op.Ret:
            targets.append(sequence)
        for t in targets:
            if not (bt := bbs.get(t)):
                bt = bbs[t] = BasicBlock(t)
            bb.targets[t] = bt
            bt.sources[bb.offset] = bb

    for offset, bb in list(bbs.items()):
        if bb.body:
            continue
        del bbs[offset]
        for source in bb.sources.values():
            source.targets.pop(offset, None)

    _trace_stack(bbs)

    for insn in self.body:
        if (stack := insn.stack) is None:
            continue
        for k, op in enumerate(insn.operands):
            if not isinstance(op, Operand):
                continue
            if not (v := op.variable) or v.type != VariableType.Local:
                continue
            if v.index <= stack:
                continue
            raise IndexError(
                F'Instruction {op!s} at offset 0x{insn.offset:X} in function {self.name} has '
                F'variable operand {k} whose index {v.index} exceeds the stack depth {stack}.')

    return bbs
class VariableBase (type, spec)

This class represents a variable within the IFPS runtime. This is primarily a base class for the more sophisticated Variable.

Expand source code Browse git
class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: VariableSpec | str | None
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable, or the
    name of a global variable that the script exports. If this property is `None`, the variable is
    unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy` opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec | str | None):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'

Subclasses

Instance variables

var spec

A VariableSpec that uniquely identifies the base variable, or the name of a global variable that the script exports. If this property is None, the variable is unbound: The Op.SetPtrToCopy opcode can create such variables.

Expand source code Browse git
class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: VariableSpec | str | None
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable, or the
    name of a global variable that the script exports. If this property is `None`, the variable is
    unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy` opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec | str | None):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'
var type

The type of the variable, see IFPSType.

Expand source code Browse git
class VariableBase:
    """
    This class represents a variable within the IFPS runtime. This is primarily a base class for
    the more sophisticated `refinery.lib.inno.emulator.Variable`.
    """
    type: IFPSType
    """
    The type of the variable, see `refinery.lib.inno.ifps.IFPSType`.
    """
    spec: VariableSpec | str | None
    """
    A `refinery.lib.inno.ifps.VariableSpec` that uniquely identifies the base variable, or the
    name of a global variable that the script exports. If this property is `None`, the variable is
    unbound: The `refinery.lib.inno.ifps.Op.SetPtrToCopy` opcode can create such variables.
    """

    __slots__ = 'type', 'spec'

    def __init__(self, type: IFPSType, spec: VariableSpec | str | None):
        self.type = type
        self.spec = spec

    def __str__(self):
        return F'{self.spec}: {self.type!s}'
class OperandType (*args, **kwds)

Classifies the type of an Operand.

Expand source code Browse git
@represent
class OperandType(enum.IntEnum):
    """
    Classifies the type of an `refinery.lib.inno.ifps.Operand`.
    """
    Variable = 0
    Value = 1
    IndexedByInt = 2
    IndexedByVar = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var Variable

The type of the None singleton.

var Value

The type of the None singleton.

var IndexedByInt

The type of the None singleton.

var IndexedByVar

The type of the None singleton.

class EHType (*args, **kwds)

This enumeration lists the possible types of code region covered by an exception handler.

Expand source code Browse git
@represent
class EHType(enum.IntEnum):
    """
    This enumeration lists the possible types of code region covered by an exception handler.
    """
    Try = 0
    Finally = 1
    Catch = 2
    SecondFinally = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var Try

The type of the None singleton.

var Finally

The type of the None singleton.

var Catch

The type of the None singleton.

var SecondFinally

The type of the None singleton.

class NewEH (*args, **kwds)

This enumeration gives names to the 4 arguments of the opcode responsible for registering a new exception handler. The first argument specifies the location of a finally, the second argument specifies the location of a catch handler, and so on.

Expand source code Browse git
@represent
class NewEH(enum.IntEnum):
    """
    This enumeration gives names to the 4 arguments of the opcode responsible for registering a new
    exception handler. The first argument specifies the location of a finally, the second argument
    specifies the location of a catch handler, and so on.
    """
    Finally = 0
    CatchAt = 1
    SecondFinally = 2
    End = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.ReprEnum
  • enum.Enum

Class variables

var Finally

The type of the None singleton.

var CatchAt

The type of the None singleton.

var SecondFinally

The type of the None singleton.

var End

The type of the None singleton.

class VariableType (*args, **kwds)

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

Expand source code Browse git
class VariableType(str, enum.Enum):
    Global = 'GlobalVar'
    Local = 'LocalVar'
    Argument = 'Argument'

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.value

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var Global

The type of the None singleton.

var Local

The type of the None singleton.

var Argument

The type of the None singleton.

class VariableSpec (index, type)

Represents a reference to a variable within the IFPS runtime. There are three variable types: Locals, globals, and function arguments; see VariableType. A variable is then uniquely defined by its type and index within the (localized) list of such variables. The function argument of index zero is the return value of a function.

Expand source code Browse git
class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'

Ancestors

  • builtins.tuple

Instance variables

var index

Alias for field number 0

Expand source code Browse git
class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'
var type

Alias for field number 1

Expand source code Browse git
class VariableSpec(NamedTuple):
    """
    Represents a reference to a variable within the IFPS runtime. There are three variable types:
    Locals, globals, and function arguments; see `refinery.lib.inno.ifps.VariableType`. A variable
    is then uniquely defined by its type and index within the (localized) list of such variables.
    The function argument of index zero is the return value of a function.
    """
    index: int
    type: VariableType

    def __repr__(self):
        if self.index == 0 and self.type == VariableType.Argument:
            return 'ReturnValue'
        return F'{self.type!s}{self.index}'
class Operand (type, variable=None, value=None, index=None)

Represends an operand to an IFPS opcode. An operand can either contain a value, which is an immediate that is encoded into the opcode, or a reference to a variable. A variable is given by its VariableSpec. Additionally, the operand can specify an index for this variable which can either be given by an immediate, or by another variable. In the latter case, the encoded index is also a VariableSpec. The type of operand is encoded as an OperandType.

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')

Ancestors

  • builtins.tuple

Instance variables

var type

Alias for field number 0

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')
var variable

Alias for field number 1

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')
var value

Alias for field number 2

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')
var index

Alias for field number 3

Expand source code Browse git
class Operand(NamedTuple):
    """
    Represends an operand to an IFPS opcode. An operand can either contain a value, which is an
    immediate that is encoded into the opcode, or a reference to a variable. A variable is given
    by its `refinery.lib.inno.ifps.VariableSpec`. Additionally, the operand can specify an index
    for this variable which can either be given by an immediate, or by another variable. In the
    latter case, the encoded index is also a `refinery.lib.inno.ifps.VariableSpec`. The type of
    operand is encoded as an `refinery.lib.inno.ifps.OperandType`.
    """
    type: OperandType
    variable: VariableSpec | None = None
    value: Value | None = None
    index: VariableSpec | int | None = None

    def __repr__(self):
        return self.__tostring(repr)

    def __str__(self):
        return self.__tostring(str)

    @property
    def immediate(self):
        return self.type == OperandType.Value

    def __tostring(self, converter):
        if self.type is OperandType.Value:
            return converter(self.value)
        if self.type is OperandType.Variable:
            return converter(self.variable)
        if self.type is OperandType.IndexedByInt:
            return F'{converter(self.variable)}[0x{self.index:02X}]'
        if self.type is OperandType.IndexedByVar:
            return F'{converter(self.variable)}[{self.index!s}]'
        raise RuntimeError(F'Unexpected OperandType {self.type!r} in {self.__class__.__name__}')
var immediate
Expand source code Browse git
@property
def immediate(self):
    return self.type == OperandType.Value
class Instruction (offset, opcode, encoded=b'', stack=None, operands=<factory>, operator=None, jumptarget=False)

Instruction(offset: 'int', opcode: 'Op', encoded: 'bytes' = b'', stack: 'int | None' = None, operands: 'list[str | bool | int | float | Operand | IFPSType | Function | None]' = , operator: 'AOp | COp | None' = None, jumptarget: 'bool' = False)

Expand source code Browse git
@dataclass
class Instruction:
    offset: int
    opcode: Op
    encoded: bytes = B''
    stack: int | None = None
    operands: list[str | bool | int | float | Operand | IFPSType | Function | None] = field(default_factory=list)
    operator: AOp | COp | None = None
    jumptarget: bool = False

    def callee(self) -> Function:
        """
        The function that this instruction calls; it must be a `refinery.lib.inno.ifps.Op.Call`.
        """
        if not isinstance(target := self.operands[0], Function):
            raise TypeError(F'The instruction at offset 0x{self.offset:X} calls no function.')
        return target

    def op(self, index: int):
        arg = self.operands[index]
        if not isinstance(arg, Operand):
            raise TypeError
        return arg

    @property
    def size(self):
        return len(self.encoded)

    @property
    def branches(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpFalse,
            Op.JumpTrue,
            Op.JumpFlag,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def jumps(self):
        return self.opcode in (
            Op.Jump,
            Op.JumpPop1,
            Op.JumpPop2,
        )

    @property
    def stack_delta(self):
        return _Op_StackD.get(self.opcode, 0)

    def oprep(self, labels: dict[int, str] | None = None):
        if self.branches:
            dst = self.operands[0]
            if not labels or not (label := labels.get(dst)):
                label = F'0x{dst:X}'
            var = [str(op) for op in self.operands[1:]]
            return ', '.join((label, *var))
        elif self.opcode is Op.PushEH:
            ops = []
            for op, name in reversed(list(zip(self.operands, NewEH))):
                if op is None:
                    continue
                ops.append(F'{name}:0x{op:X}')
            return '\x20'.join(ops)
        elif self.opcode is Op.PopEH:
            return F'End{EHType(self.operands[0])}'
        elif self.opcode is Op.SetFlag:
            rep, negated = self.operands
            return F'!{rep}' if negated else str(rep)
        elif self.opcode is Op.Compare:
            dst, a, b = self.operands
            return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
        elif self.opcode is Op.Calculate:
            dst, src = self.operands
            return F'{dst!s} {self.operator!s} {src!s}'
        elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
            dst, src = self.operands
            return F'{dst!s} := {src!s}'
        else:
            return ', '.join(str(op) for op in self.operands)

    def pretty(self, labels: dict[int, str] | None = None):
        return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()

    def __repr__(self):
        return F'{self.opcode.name}({self.oprep()})'

    def __str__(self):
        return self.pretty()

Instance variables

var offset

The type of the None singleton.

var opcode

The type of the None singleton.

var operands

The type of the None singleton.

var encoded

The type of the None singleton.

var stack

The type of the None singleton.

var operator

The type of the None singleton.

var jumptarget

The type of the None singleton.

var size
Expand source code Browse git
@property
def size(self):
    return len(self.encoded)
var branches
Expand source code Browse git
@property
def branches(self):
    return self.opcode in (
        Op.Jump,
        Op.JumpFalse,
        Op.JumpTrue,
        Op.JumpFlag,
        Op.JumpPop1,
        Op.JumpPop2,
    )
var jumps
Expand source code Browse git
@property
def jumps(self):
    return self.opcode in (
        Op.Jump,
        Op.JumpPop1,
        Op.JumpPop2,
    )
var stack_delta
Expand source code Browse git
@property
def stack_delta(self):
    return _Op_StackD.get(self.opcode, 0)

Methods

def callee(self)

The function that this instruction calls; it must be a Op.Call.

Expand source code Browse git
def callee(self) -> Function:
    """
    The function that this instruction calls; it must be a `refinery.lib.inno.ifps.Op.Call`.
    """
    if not isinstance(target := self.operands[0], Function):
        raise TypeError(F'The instruction at offset 0x{self.offset:X} calls no function.')
    return target
def op(self, index)
Expand source code Browse git
def op(self, index: int):
    arg = self.operands[index]
    if not isinstance(arg, Operand):
        raise TypeError
    return arg
def oprep(self, labels=None)
Expand source code Browse git
def oprep(self, labels: dict[int, str] | None = None):
    if self.branches:
        dst = self.operands[0]
        if not labels or not (label := labels.get(dst)):
            label = F'0x{dst:X}'
        var = [str(op) for op in self.operands[1:]]
        return ', '.join((label, *var))
    elif self.opcode is Op.PushEH:
        ops = []
        for op, name in reversed(list(zip(self.operands, NewEH))):
            if op is None:
                continue
            ops.append(F'{name}:0x{op:X}')
        return '\x20'.join(ops)
    elif self.opcode is Op.PopEH:
        return F'End{EHType(self.operands[0])}'
    elif self.opcode is Op.SetFlag:
        rep, negated = self.operands
        return F'!{rep}' if negated else str(rep)
    elif self.opcode is Op.Compare:
        dst, a, b = self.operands
        return F'{dst!s} := {a!s} {self.operator!s} {b!s}'
    elif self.opcode is Op.Calculate:
        dst, src = self.operands
        return F'{dst!s} {self.operator!s} {src!s}'
    elif self.opcode in (Op.Assign, Op.SetPtr, Op.SetPtrToCopy):
        dst, src = self.operands
        return F'{dst!s} := {src!s}'
    else:
        return ', '.join(str(op) for op in self.operands)
def pretty(self, labels=None)
Expand source code Browse git
def pretty(self, labels: dict[int, str] | None = None):
    return F'{self.opcode!s:<{_Op_Maxlen}}{_TAB}{self.oprep(labels)}'.strip()
class BasicBlock (offset, stack=None, body=<factory>, sources=<factory>, targets=<factory>)

BasicBlock(offset: 'int', stack: 'int | None' = None, body: 'list[Instruction]' = , sources: 'dict[int, BasicBlock]' = , targets: 'dict[int, BasicBlock]' = )

Expand source code Browse git
@dataclass
class BasicBlock:
    offset: int
    stack: int | None = None
    body: list[Instruction] = field(default_factory=list)
    sources: dict[int, BasicBlock] = field(default_factory=dict)
    targets: dict[int, BasicBlock] = field(default_factory=dict)

    @property
    def stack_delta(self):
        return sum(insn.stack_delta for insn in self.body)

    @property
    def size(self):
        return sum(insn.size for insn in self.body)

Instance variables

var offset

The type of the None singleton.

var body

The type of the None singleton.

var sources

The type of the None singleton.

var targets

The type of the None singleton.

var stack

The type of the None singleton.

var stack_delta
Expand source code Browse git
@property
def stack_delta(self):
    return sum(insn.stack_delta for insn in self.body)
var size
Expand source code Browse git
@property
def size(self):
    return sum(insn.size for insn in self.body)
class FTag (*args, **kwds)

Support for integer-based Flags

Expand source code Browse git
class FTag(enum.IntFlag):
    External = 0b0001
    Exported = 0b0010
    HasAttrs = 0b0100

    def check(self, v):
        return bool(self & v)

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.ReprEnum
  • enum.Flag
  • enum.Enum

Class variables

var External

The type of the None singleton.

var Exported

The type of the None singleton.

var HasAttrs

The type of the None singleton.

Methods

def check(self, v)
Expand source code Browse git
def check(self, v):
    return bool(self & v)
class IFPSFile (reader, codec='latin1', unicode=None, load_flags=None)

A class to parse structured data. A Struct class can be instantiated as follows:

foo = Struct(data, bar=29)

The initialization routine of the structure will be called with a single argument reader. If the object data is already a StructReader, then it will be passed as reader. Otherwise, the argument will be wrapped in a StructReader. Additional arguments to the struct are passed through.

The setup that compiled the script decides whether String is a Unicode string and how many load flags a DLL import has. When the caller does not know them, they are read from the script: it is Unicode unless its only string type is AnsiString, and the load flags are decided by the calls of the DLL imports. The script's standard types tell which Pascal Script compiled it and for which platform.

Expand source code Browse git
class IFPSFile(Struct):
    MinVer = 12
    MaxVer = 23

    Magic = B'IFPS'

    def __init__(
        self,
        reader: StructReader[memoryview],
        codec: str = 'latin1',
        unicode: bool | None = None,
        load_flags: int | None = None,
    ):
        """
        The setup that compiled the script decides whether `String` is a Unicode string and how many
        load flags a DLL import has. When the caller does not know them, they are read from the
        script: it is Unicode unless its only string type is `AnsiString`, and the load flags are
        decided by the calls of the DLL imports. The script's standard types tell which Pascal
        Script compiled it and for which platform.
        """
        self.codec = codec
        self.load_flags = load_flags
        self.types: list[IFPSType] = []
        self.functions: list[Function] = []
        self.globals: list[VariableBase] = []
        self._strings: dict[str, None] = {}
        self.reader = reader
        if reader.remaining_bytes < 28:
            raise ValueError('Less than 28 bytes in file, not enough data to parse.')
        magic = reader.read(4)
        if magic != self.Magic:
            raise ValueError(F'Invalid magic sequence: {magic.hex()}')
        self.version = reader.u32()
        self.count_types = reader.u32()
        self.count_functions = reader.u32()
        self.count_variables = reader.u32()
        self.entry = reader.u32()
        self.import_size = reader.u32()

        if self.version not in range(self.MinVer, self.MaxVer + 1):
            raise NotImplementedError(
                F'This IFPS file has version {self.version}, which is not in the supported range '
                F'[{self.MinVer},{self.MaxVer}].')

        self._known_type_names = {
            TC.U08        : {'Byte', 'Boolean'},
            TC.S08        : {'ShortInt'},
            TC.U16        : {'Word'},
            TC.S16        : {'SmallInt'},
            TC.S32        : {'Integer', 'LongInt'},
            TC.U32        : {'LongWord', 'Cardinal', 'HWND', 'TSetupProcessorArchitecture'},
            TC.Char       : {'AnsiChar'},
            TC.PChar      : {'PAnsiChar'},
            TC.AnsiString : {'AnyString'},
            TC.S64        : {'Int64', 'NativeInt'},
            TC.U64        : {'UInt64', 'NativeUInt'},
        }

        self._load_types()
        if unicode is None:
            codes = {t.code for t in self.types}
            unicode = TC.UnicodeString in codes or TC.AnsiString not in codes
        self.unicode = unicode
        self.dialect = self._dialect()
        self.platform = self._platform()
        self._name_types()
        self._load_functions()
        self._load_variables()

        del self._known_type_names
        self.strings: list[str] = list(self._strings)
        del self._strings

    def _dialect(self) -> Dialect:
        """
        The Pascal Script that compiled the script. The compiler of Inno Setup 7 declares the type
        `UInt64` in every script, and Pascal Script has no such type before Inno Setup 7.
        """
        if any(t.code is TC.U64 for t in self.types):
            return Dialect.Inno7
        return Dialect.Inno6

    def _platform(self) -> Platform:
        """
        The platform of the Setup that the script was compiled for. The compiler declares its
        standard types first, in a fixed order; from Inno Setup 7 on, `NativeUInt` follows the first
        `Single` and is 64 bits wide in a script for a 64-bit Setup. Earlier versions only build
        32-bit Setups and have no 64-bit unsigned type. The standard types decide, so the types
        read so far decide while the type table is read.
        """
        types = self.types
        for k, t in enumerate(types):
            if t.code is TC.Single:
                if k + 1 < len(types) and types[k + 1].code is TC.U64:
                    return Platform.Win64
                break
        return Platform.Win32

    def _read_extended(self, reader: StructReader) -> x87.Real:
        """
        An Extended constant, which is a Double in a script for a 64-bit Setup.
        """
        if self._platform() is Platform.Win64:
            return reader.f64()
        return extended(reader.read(10))

    def _name_types(self, missing_types: set[str] | None = None):
        tbn: dict[str, IFPSType] = CaseInsensitiveDict()
        self.types_by_name = tbn
        for t in self.types:
            name = str(t)
            code = t.code
            known = self._known_type_names.get(code)
            if known:
                known.discard(name)
            tbn[name] = t

        if missing_types:
            def add_type(name: str, type: IFPSType):
                tbn[name] = type
                self.types.append(type)
                return type

            def make_string(name: str = 'String'):
                try:
                    return tbn[name]
                except KeyError:
                    pass
                code = TC.UnicodeString if self.unicode else TC.AnsiString
                for type in tbn.values():
                    if type.code is code:
                        break
                else:
                    return add_type(name, TPrimitive(code, symbol=name))
                tbn[name] = type
                return type

            for name in tbn:
                missing_types.discard(name)

            if 'TGUID' in missing_types:
                missing_types |= {'LongWord', 'Word', 'Byte'}

            for code in TC:
                name = code.name
                if name not in missing_types:
                    continue
                missing_types.discard(name)
                add_type(name, TPrimitive(code, symbol=name))

            for code, names in self._known_type_names.items():
                for name in names:
                    if name not in missing_types:
                        continue
                    missing_types.discard(name)
                    add_type(name, TPrimitive(code, symbol=name))

            for name in [
                'TVarType',
                'TInputQueryWizardPage',
                'TInputOptionWizardPage',
                'TInputDirWizardPage',
                'TInputFileWizardPage',
                'TOutputMsgWizardPage',
                'TOutputMsgMemoWizardPage',
                'TOutputProgressWizardPage',
                'TOutputMarqueeProgressWizardPage',
                'TDownloadWizardPage',
                'ExtractionWizardPage',
                'TWizardPage',
                'TSetupForm',
                'TComponent',
                'TNewNotebookPage',
            ]:
                if name not in missing_types:
                    continue
                add_type(name, TClass(TC.Class, name, symbol=name))

            if (name := 'String') in missing_types:
                make_string(name)

            if (name := 'TArrayOfString') in missing_types:
                add_type(name, TArray(TC.Array, make_string()))

            if (name := 'IUnknown') in missing_types:
                add_type(name, TInterface(TC.Interface, UUID('{00000000-0000-0000-C000-000000000046}')))

            if (name := 'TGUID') in missing_types:
                add_type(name, TRecord(TC.Record, (
                    tbn['LongWord'],
                    tbn['Word'],
                    tbn['Word'],
                    TStaticArray(TC.StaticArray, tbn['Byte'], 8)
                ), symbol=name))

    def _load_types(self):
        def _normalize(n: str):
            return IFPSClasses.Types.get(n.casefold(), n)
        reader = self.reader
        types = self.types
        for k in range(self.count_types):
            typecode = reader.u8()
            exported = bool(typecode & 0x80)
            typecode = typecode & 0x7F
            try:
                code = TC(typecode)
            except ValueError as V:
                raise ValueError(F'Unknown type code value 0x{typecode:02X}.') from V
            if code in (TC.Class, TC.ExtClass):
                t = TClass(code, _normalize(reader.read_length_prefixed_ascii()))
            elif code is TC.ProcPtr:
                spec = reader.read_length_prefixed()
                void = bool(spec[0])
                args = tuple(DeclSpecParam(not b) for b in spec[1:])
                t = TProcPtr(code, void, args)
            elif code is TC.Interface:
                guid = UUID(bytes=bytes(reader.read(0x10)))
                t = TInterface(code, guid)
            elif code is TC.Set:
                t = TSet(code, reader.u32())
            elif code is TC.StaticArray:
                type = types[reader.u32()]
                size = reader.u32()
                offset = None if self.version <= 22 else reader.u32()
                t = TStaticArray(code, type, size, offset)
            elif code is TC.Array:
                t = TArray(code, types[reader.u32()])
            elif code is TC.Record:
                length = reader.u32()
                members = tuple(types[reader.u32()] for _ in range(length))
                t = TRecord(code, members, symbol=F'RECORD{k}')
            else:
                t = TPrimitive(code, symbol=code.name)
            if exported:
                t.symbol = _normalize(reader.read_length_prefixed_ascii())
                if self.version <= 21:
                    t.name = _normalize(reader.read_length_prefixed_ascii())
            types.append(t)
            if self.version >= 21:
                t.attributes = list(self._read_attributes())

    def _read_value(self, reader: StructReader | None = None) -> Value:
        if reader is None:
            reader = self.reader
        type = self.types[reader.u32()]
        size = type.code.width
        processor: Callable[[], int | float | str | bytes] | None = {
            TC.U08           : reader.u8,
            TC.S08           : reader.i8,
            TC.U16           : reader.u16,
            TC.S16           : reader.i16,
            TC.U32           : reader.u32,
            TC.S32           : reader.i32,
            TC.S64           : reader.i64,
            TC.U64           : reader.u64,
            TC.Single        : reader.f32,
            TC.Double        : reader.f64,
            TC.Extended      : lambda: self._read_extended(reader),
            TC.AnsiString    : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.PChar         : lambda: reader.read_length_prefixed(encoding=self.codec),
            TC.WideString    : reader.read_length_prefixed_utf16,
            TC.UnicodeString : reader.read_length_prefixed_utf16,
            TC.Char          : lambda: chr(reader.u8()),
            TC.WideChar      : lambda: chr(reader.u16()),
            TC.ProcPtr       : lambda: self.functions[reader.u32()],
            TC.Set           : lambda: int.from_bytes(reader.read(type.size_in_bytes), 'little'),
            TC.Currency      : lambda: Fraction(reader.i64(), 10_000),
        }.get(type.code, None)
        if processor is not None:
            data = processor()
        elif size > 0:
            data = bytes(reader.read(size))
        else:
            raise ValueError(F'Unable to read attribute of type {type!s}.')
        if isinstance(data, str):
            self._strings[data] = None
        return Value(type, data)

    def _read_attributes(self) -> Generator[FunctionAttribute]:
        reader = self.reader
        count = reader.u32()
        for _ in range(count):
            name = reader.read_length_prefixed_ascii()
            fields = tuple(self._read_value() for _ in range(reader.u32()))
            yield FunctionAttribute(name, fields)

    def _load_functions(self):
        reader = self.reader
        width = len(F'{self.count_functions:X}')
        dll_imports: list[tuple[Function, bytes]] = []
        max_argc = len(reader) // _ARGUMENT_SIZE

        for k in range(self.count_functions):
            decl = None
            body = None
            name = F'F{k:0{width}X}'
            tags = reader.u8()
            attributes = None
            exported = FTag.Exported.check(tags)
            spec = None
            if FTag.External.check(tags):
                name = reader.read_length_prefixed_ascii(8)
                if exported:
                    spec = bytes(reader.read_length_prefixed())
                    if not spec.startswith(B'dll:'):
                        decl = DeclSpec.ParseF(StructReader(spec), 0, max_argc)
            else:
                offset = reader.u32()
                length = reader.u32()
                if exported:
                    name = reader.read_length_prefixed_ascii()
                    decl = DeclSpec.ParseE(bytes(reader.read_length_prefixed()), self)
                with reader.detour(offset):
                    body = reader.read(length)
            if FTag.HasAttrs.check(tags):
                attributes = list(self._read_attributes())
            fn = Function(name, decl=decl, body=body, attributes=attributes)
            self.functions.append(fn)
            if decl is None and spec is not None:
                dll_imports.append((fn, spec))

        for function in self.functions:
            if body := function.body:
                void = decl.void if (decl := function.decl) else False
                function.body = list(self._parse_bytecode(body, void))

        calls: dict[int, list[_CallSite]] = {}

        for function in self.functions:
            body = function.body or ()
            for k, instruction in enumerate(body):
                if instruction.opcode is not Op.Call:
                    continue
                callee = instruction.operands[0] = self.functions[instruction.operands[0]]
                pops = 0
                for after in itertools.islice(body, k + 1, None):
                    if after.opcode is not Op.Pop:
                        break
                    pops += 1
                result = k > 0 and body[k - 1].opcode is Op.PushVar
                calls.setdefault(id(callee), []).append(_CallSite(result, pops))

        self._load_dll_imports(dll_imports, calls, max_argc)

        missing_types = set()
        signatures: list[tuple[Function, IFPSSignature]] = []

        for function in self.functions:
            decl = function.decl
            if (
                decl is not None
                and not decl.parameters
                and not decl.module
                and not decl.classname
                and function.body is None
                and (builtin := _BUILTIN_PARAMETERS.get(function.name.casefold()))
            ):
                decl.parameters = [DeclSpecParam(p.const, name=p.name) for p in builtin]
            if (signature := self._signature(function, missing_types)) is None:
                continue
            if decl is None:
                decl = function.decl = DeclSpec(
                    signature.void or any(not call.result for call in calls.get(id(function), ())),
                    [DeclSpecParam(p.const) for p in signature.parameters],
                )
            function.symbol = decl.name = signature.name
            if not _shapes_agree(decl, signature):
                continue
            signatures.append((function, signature))
            for old, new in zip(decl.parameters, signature.parameters):
                if (t := old.type) is not None:
                    t.symbol = new.type
                elif t := self.types_by_name.get(new.type):
                    t.symbol = new.type
                else:
                    missing_types.add(new.type)
            if sr := signature.return_type:
                if (dr := decl.return_type) or (dr := self.types_by_name.get(sr)):
                    dr.symbol = sr
                else:
                    missing_types.add(sr)

        self.type_name_conflicts = self._name_types(missing_types)

        for function, signature in signatures:
            decl = function.decl
            assert decl is not None
            for old, new in zip(decl.parameters, signature.parameters):
                if old.type is None:
                    old.type = self.types_by_name.get(new.type)
                old.name = new.name or old.name
            if (rt := signature.return_type) and (decl.return_type is None):
                decl.return_type = self.types_by_name.get(rt)

    def _load_dll_imports(
        self,
        imports: list[tuple[Function, bytes]],
        calls: dict[int, list[_CallSite]],
        max_argc: int,
    ):
        """
        Parses the declarations of the DLL imports. Depending on its revision, Pascal Script wrote
        zero, one or two load flags into them, independent of the version of the script. Unless the
        caller gave the count, it is the one that the calls of the imports contradict least, then
        the one that leaves the fewest extra pops after the calls, then the larger one. A count for
        which a declaration cannot be parsed is out.
        """
        def parse(load_flags: int) -> list[DeclSpec]:
            return [DeclSpec.ParseF(StructReader(spec), load_flags, max_argc) for _, spec in imports]

        def fit(load_flags: int) -> tuple[int, int, int]:
            contradictions = slack = 0
            for (function, _), decl in zip(imports, candidates[load_flags]):
                for call in calls.get(id(function), ()):
                    if call.contradicts(decl):
                        contradictions += 1
                    else:
                        slack += call.slack(decl)
            return contradictions, slack, -load_flags

        if (load_flags := self.load_flags) is None:
            candidates: dict[int, list[DeclSpec]] = {}
            for count in (0, 1, 2):
                try:
                    candidates[count] = parse(count)
                except EOFError:
                    continue
            if not candidates:
                raise ValueError('The declarations of the DLL imports do not parse with any count of load flags.')
            load_flags = min(candidates, key=fit)
            decls = candidates[load_flags]
        else:
            decls = parse(load_flags)
        for (function, _), decl in zip(imports, decls):
            function.decl = decl

    def _signature(self, function: Function, missing_types: set[str]) -> IFPSSignature | None:
        """
        The declaration that Inno Setup documents for the given function: an event for a function
        of the script, a member of a class for a method or a property, else one of the functions
        that Inno Setup and Pascal Script provide. Imports from a DLL have none.
        """
        decl = function.decl
        if function.body is not None:
            return IFPSEvents.get(function.name)
        if decl is not None and decl.module is not None:
            return None
        if decl is not None and decl.classname:
            if (info := IFPSClasses.Classes.get(decl.classname)) is None:
                return None
            if info.name not in self.types_by_name:
                missing_types.add(info.name)
            decl.classname = info.name
            return info.members.get(decl.name)
        return IFPSAPI.get(function.name)

    def _load_variables(self):
        reader = self.reader
        for index in range(self.count_variables):
            code = reader.u32()
            spec = VariableSpec(index, VariableType.Global)
            if reader.u8() & 1:
                spec = reader.read_length_prefixed_ascii()
            self.globals.append(VariableBase(self.types[code], spec))

    def _read_variable_spec(self, index: int, void: bool) -> VariableSpec:
        if index < 0x40000000:
            return VariableSpec(index, VariableType.Global)
        index -= 0x60000000
        if index >= 0:
            return VariableSpec(index, VariableType.Local)
        index = -index if void else ~index
        return VariableSpec(index, VariableType.Argument)

    def _read_operand(self, reader: StructReader, void: bool) -> Operand:
        ot = OperandType(reader.u8())
        kw = {}
        if ot is OperandType.Variable:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
        if ot is OperandType.Value:
            kw.update(value=self._read_value(reader))
        if ot >= OperandType.IndexedByInt:
            kw.update(variable=self._read_variable_spec(reader.u32(), void))
            index = reader.u32()
            if ot is OperandType.IndexedByVar:
                index = self._read_variable_spec(index, void)
            kw.update(index=index)
        return Operand(ot, **kw)

    def _parse_bytecode(self, data: memoryview, void: bool) -> Generator[Instruction]:
        disassembly: dict[int, Instruction] = OrderedDict()
        reader = StructReader(data)

        argcount = {
            Op.Assign: 2,
            Op.CallVar: 1,
            Op.Dec: 1,
            Op.Inc: 1,
            Op.BooleanNot: 1,
            Op.Neg: 1,
            Op.IntegerNot: 1,
            Op.SetPtrToCopy: 2,
            Op.SetPtr: 2,
        }

        while not reader.eof:
            def arg(k=1):
                for _ in range(k):
                    args.append(self._read_operand(reader, void))
            addr = reader.tell()
            cval = reader.u8()
            code = Op.FromInt(cval)
            insn = Instruction(addr, code)
            args = insn.operands
            disassembly[insn.offset] = insn
            aryness = argcount.get(code)
            if aryness is not None:
                arg(aryness)
            elif code in (Op.Ret, Op.Nop, Op.Pop):
                pass
            elif code is Op.Calculate:
                insn.operator = AOp(reader.u8())
                arg(2)
            elif code in (Op.Push, Op.PushVar):
                arg()
            elif code in (Op.Jump, Op.JumpFlag):
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.Call:
                args.append(reader.u32())
            elif code in (Op.JumpTrue, Op.JumpFalse):
                target = reader.i32()
                val = self._read_operand(reader, void)
                args.append(reader.tell() + target)
                args.append(val)
            elif code is Op.JumpPop1:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.JumpPop2:
                target = reader.i32()
                args.append(reader.tell() + target)
            elif code is Op.StackType:
                args.append(self._read_variable_spec(reader.u32(), void))
                args.append(reader.u32())
            elif code is Op.PushType:
                args.append(self.types[reader.u32()])
            elif code is Op.Compare:
                insn.operator = COp(reader.u8())
                arg(3)
            elif code is Op.SetFlag:
                arg()
                args.append(bool(reader.u8()))
            elif code is Op.PushEH:
                args.extend(reader.i32() for _ in range(4))
                for k, a in enumerate(args):
                    args[k] = a + reader.tell() if a >= 0 else None
            elif code is Op.PopEH:
                args.append(reader.u8())
            elif code is Op._INVALID:
                raise ValueError(F'Unsupported opcode: 0x{cval:02X}')
            else:
                raise ValueError(F'Unhandled opcode: {code.name}')
            size = reader.tell() - addr
            reader.seekrel(-size)
            insn.encoded = bytes(reader.read(size))

        for k, instruction in enumerate(disassembly.values()):
            if not instruction.branches:
                continue
            target = instruction.operands[0]
            try:
                disassembly[target].jumptarget = True
            except KeyError as K:
                raise RuntimeError(
                    F'The jump target of instruction {k} at 0x{instruction.offset:X} is invalid; '
                    F'the invalid instruction is a {instruction.opcode.name} to 0x{target:X}.'
                ) from K

        yield from disassembly.values()

    def __str__(self):
        return self.disassembly()

    def disassembly(self, print_bytes: bool = False, print_bytes_count: int = 12) -> str:
        def sortkey(f: Function):
            d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
            return (*d, f.name)

        for function in self.functions:
            function.get_basic_blocks()

        classes: dict[str, dict[str, Function]] = {}
        external: list[Function] = []
        internal: list[Function] = []

        for t in self.types:
            if isinstance(t, TClass):
                classes[t.name] = {}

        for function in self.functions:
            if (decl := function.decl) and (name := decl.classname):
                try:
                    members = classes[name]
                except KeyError:
                    members = classes[name] = {}
                members[decl.name] = function
                continue
            dl = internal if function.body else external
            dl.append(function)

        external.sort(key=sortkey)

        output = io.StringIO()
        _omax = max((
            max(insn.offset for insn in fn.body)
            for fn in self.functions if fn.body
        ), default=0)
        _smax = max((
            max((insn.stack for insn in fn.body if insn.stack is not None), default=1)
            for fn in self.functions if fn.body
        ), default=0)
        _omax = max(len(self.types), len(self.globals), _omax)
        _omax = len(F'{_omax:X}')
        _smax = len(F'{_smax:d}')

        if classes:
            for name, members in classes.items():
                if not members:
                    output.write(F'external class {name};\n')
            output.write('\n')
            for name, members in classes.items():
                if not members:
                    continue
                output.write(F'external class {name}')
                if members:
                    for spec in members.values():
                        output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                    output.write('\nend')
                output.write(';\n\n')

        if self.types:
            typedefs = []
            for type in self.types:
                if type.code != TC.Record and type.symbol in (type.code.name, None):
                    continue
                if isinstance(type, TClass):
                    continue
                typedefs.append((type.symbol, type.display()))
            typedefs.sort()
            for symbol, display in typedefs:
                output.write(F'typedef {symbol} = {display}\n')
            output.write('\n')

        if self.globals:
            for variable in self.globals:
                output.write(F'global {variable!s}\n')
            output.write('\n')

        if external:
            for function in external:
                output.write(F'external {function!r}\n')
            output.write('\n')

        if internal:
            def create_prefix(instruction: Instruction):
                stack = instruction.stack
                stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
                return F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}'

            for function in internal:
                labels = [insn.offset for insn in function.body if insn.jumptarget]
                labelw = max(len(str(len(labels))), 2)
                labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}

                output.write(F'{function!r}\nbegin\n')
                labelc = 0

                for instruction in function.body:
                    prefix = create_prefix(instruction)
                    if instruction.jumptarget:
                        output.write(F'{labeld[labels[labelc]]}:\n')
                        labelc += 1
                    if print_bytes:
                        hexbytes = instruction.encoded.hex(' ').split()
                    else:
                        hexbytes = ['']
                    hexbytes_iter = iter(hexbytes)
                    instruction_written = False
                    prefix_length = len(prefix)
                    ic = instruction.pretty(labeld)
                    while line := list(itertools.islice(hexbytes_iter, 0, print_bytes_count)):
                        output.write(prefix)
                        if print_bytes:
                            prefix = prefix_length * '\x20'
                            output.write('\x20'.join(line).ljust(3 * print_bytes_count - 1))
                        if not instruction_written:
                            output.write(ic)
                            instruction_written = True
                        output.write('\n')

                output.write('end;\n\n')

        return output.getvalue().strip()

Ancestors

  • Struct
  • typing.Generic
  • collections.abc.Buffer

Class variables

var MinVer

The type of the None singleton.

var MaxVer

The type of the None singleton.

var Magic

The type of the None singleton.

Static methods

def Parse(reader, *args, **kwargs)

Methods

def disassembly(self, print_bytes=False, print_bytes_count=12)
Expand source code Browse git
def disassembly(self, print_bytes: bool = False, print_bytes_count: int = 12) -> str:
    def sortkey(f: Function):
        d = (d.module or '', d.classname or '', d.void) if (d := f.decl) else ('', '', True)
        return (*d, f.name)

    for function in self.functions:
        function.get_basic_blocks()

    classes: dict[str, dict[str, Function]] = {}
    external: list[Function] = []
    internal: list[Function] = []

    for t in self.types:
        if isinstance(t, TClass):
            classes[t.name] = {}

    for function in self.functions:
        if (decl := function.decl) and (name := decl.classname):
            try:
                members = classes[name]
            except KeyError:
                members = classes[name] = {}
            members[decl.name] = function
            continue
        dl = internal if function.body else external
        dl.append(function)

    external.sort(key=sortkey)

    output = io.StringIO()
    _omax = max((
        max(insn.offset for insn in fn.body)
        for fn in self.functions if fn.body
    ), default=0)
    _smax = max((
        max((insn.stack for insn in fn.body if insn.stack is not None), default=1)
        for fn in self.functions if fn.body
    ), default=0)
    _omax = max(len(self.types), len(self.globals), _omax)
    _omax = len(F'{_omax:X}')
    _smax = len(F'{_smax:d}')

    if classes:
        for name, members in classes.items():
            if not members:
                output.write(F'external class {name};\n')
        output.write('\n')
        for name, members in classes.items():
            if not members:
                continue
            output.write(F'external class {name}')
            if members:
                for spec in members.values():
                    output.write(F'\n{_TAB}{spec.decl.represent(spec.symbol, rel=True)}')
                output.write('\nend')
            output.write(';\n\n')

    if self.types:
        typedefs = []
        for type in self.types:
            if type.code != TC.Record and type.symbol in (type.code.name, None):
                continue
            if isinstance(type, TClass):
                continue
            typedefs.append((type.symbol, type.display()))
        typedefs.sort()
        for symbol, display in typedefs:
            output.write(F'typedef {symbol} = {display}\n')
        output.write('\n')

    if self.globals:
        for variable in self.globals:
            output.write(F'global {variable!s}\n')
        output.write('\n')

    if external:
        for function in external:
            output.write(F'external {function!r}\n')
        output.write('\n')

    if internal:
        def create_prefix(instruction: Instruction):
            stack = instruction.stack
            stack = '?' * _smax if stack is None else F'{stack:>{_smax}d}'
            return F'{_TAB}0x{instruction.offset:0{_omax}X}{_TAB}{stack}{_TAB}'

        for function in internal:
            labels = [insn.offset for insn in function.body if insn.jumptarget]
            labelw = max(len(str(len(labels))), 2)
            labeld = {v: F'JumpDestination{k:0{labelw}d}' for k, v in enumerate(labels, 1)}

            output.write(F'{function!r}\nbegin\n')
            labelc = 0

            for instruction in function.body:
                prefix = create_prefix(instruction)
                if instruction.jumptarget:
                    output.write(F'{labeld[labels[labelc]]}:\n')
                    labelc += 1
                if print_bytes:
                    hexbytes = instruction.encoded.hex(' ').split()
                else:
                    hexbytes = ['']
                hexbytes_iter = iter(hexbytes)
                instruction_written = False
                prefix_length = len(prefix)
                ic = instruction.pretty(labeld)
                while line := list(itertools.islice(hexbytes_iter, 0, print_bytes_count)):
                    output.write(prefix)
                    if print_bytes:
                        prefix = prefix_length * '\x20'
                        output.write('\x20'.join(line).ljust(3 * print_bytes_count - 1))
                    if not instruction_written:
                        output.write(ic)
                        instruction_written = True
                    output.write('\n')

            output.write('end;\n\n')

    return output.getvalue().strip()