Module refinery.lib.scripts.xlm.engine

The engine of the emulator: it owns the mutable state of a run — the coordinate indexes the cell reads and the row fall-through consult, the values the run left in its cells, the undo journal behind the branch snapshots, the registered function aliases, and the branch, loop, and call stacks — runs the program of a workbook entry point by entry point, and dispatches the function calls a formula makes to the handlers that answer them.

Expand source code Browse git
"""
The engine of the emulator: it owns the mutable state of a run — the coordinate indexes the
cell reads and the row fall-through consult, the values the run left in its cells, the undo
journal behind the branch snapshots, the registered function aliases, and the branch, loop,
and call stacks — runs the program of a workbook entry point by entry point, and dispatches
the function calls a formula makes to the handlers that answer them.
"""
from __future__ import annotations

import bisect
import dataclasses
import hashlib
import time

from typing import Any, Iterator, NamedTuple, Protocol

from refinery.lib.excel import CellKind, parse_formula, synthesize_formula
from refinery.lib.excel.common import column_letters
from refinery.lib.excel.formula.model import (
    XlA1Reference,
    XlBinaryExpression,
    XlBinaryOperator,
    XlDefinedName,
    XlFunctionCall,
    XlR1C1Reference,
    XlString,
)
from refinery.lib.scripts import TREE_RECURSION_DEPTH, BodyEdit, set_body, set_child, set_value
from refinery.lib.scripts.xlm.blocks import XlmBlock, marker_of, pair_blocks
from refinery.lib.scripts.xlm.commands import severity
from refinery.lib.scripts.xlm.environment import XlmEnvironment
from refinery.lib.scripts.xlm.evaluate import evaluate_expression
from refinery.lib.scripts.xlm.handlers import EXPRESSION_HANDLERS, HANDLERS
from refinery.lib.scripts.xlm.memory import XlmFiles, XlmMemory
from refinery.lib.scripts.xlm.model import XlmCell
from refinery.lib.scripts.xlm.names import XlmNameEntry
from refinery.lib.scripts.xlm.references import (
    XlmArrival,
    XlmCursor,
    XlmFrame,
    XlmLoop,
    XlmReturnSlot,
    XlmSnapshot,
    resolve_reference,
)
from refinery.lib.scripts.xlm.trace import XlmSeverity, XlmStatus, XlmStep
from refinery.lib.scripts.xlm.values import XlmOutcome, XlmReference, XlmValue
from refinery.lib.tools import RecursionDepth

_FALL_THROUGH = frozenset((
    XlmStatus.FullEvaluation,
    XlmStatus.PartialEvaluation,
    XlmStatus.NotImplemented,
    XlmStatus.IGNORED,
))

_ROW_FALL_THROUGH_LIMIT = 10000

#: The commands that still run while the engine skips the body of a loop: the heads and the end
#: of a loop, because a skipped body ends at the `NEXT` that pairs with its own head.
_LOOP_COMMANDS = frozenset((
    'FOR.CELL',
    'NEXT',
    'WHILE',
))

#: How deeply macro calls inside expressions may nest: a deeper call is left unfinished, which
#: bounds a subroutine that calls itself from inside a formula.
_SUBROUTINE_DEPTH_LIMIT = 64

#: How many formula cells the reads of one evaluation may evaluate: a read past the budget
#: answers the value the cell was stored with, which bounds a formula whose references fan out
#: over cells that read each other.
_READ_BUDGET = 100_000


#: How many steps the loop detector leaves between two computations of the state digest, so
#: that a window that repeats keeps the run cheap until a comparison can tell frozen from
#: moving.
_DIGEST_STEPS = 1000

#: How many addresses a run observes before its window of the last ten addresses starts.
_WINDOW_WARMUP = 20


class _Exhausted(Exception):
    """
    Raised once a run used up its step budget or its deadline: the run of the entry ends at the
    step that exhausted it, even when that step runs inside a subroutine an expression called.
    """


def callee_name(call: XlFunctionCall) -> str:
    """
    The name the callee of a call spells, without the quotes of a string literal; a callee
    that is a reference or another call is not named at all.
    """
    callee = call.callee
    if isinstance(callee, str):
        return callee.strip('"')
    if isinstance(callee, XlDefinedName):
        return callee.name
    return ''


def _blank(reference: XlmReference) -> XlmValue:
    """
    The value of a cell that holds nothing: the empty text, which an arithmetic operator reads
    as zero.
    """
    return XlmValue(value='', text='', reference=reference)


def _held(value: XlmValue) -> XlmValue:
    """
    The value a cell holds once a step, a value command, or a return left a value in it. The
    trace spells a command by its call, but a read of the cell spells what the command computed,
    so a complete value is respelled from the value itself; an unfinished one keeps the spelling
    of what was left unfinished. A value that computes nothing leaves the cell blank.
    """
    if value.partial:
        return XlmValue(
            value=value.value,
            text=value.text,
            partial=True,
            date=value.date,
            cells=value.cells,
        )
    if value.value is None:
        return XlmValue(value='', text='')
    return XlmValue(
        value=value.value,
        date=value.date,
        cells=value.cells,
        error=value.error,
    )


class _CellIndex:
    """
    The coordinate indexes over the macrosheet cells of a view, and the values the runs left in
    them: the cell at every position of every sheet, the rows of every column that hold a
    formula — the rows the fall-through walks — the sheet every cell sits on, and the value a
    step, a value command, or a return left in a cell, which a later read of the cell answers
    instead of evaluating its formula again.
    """

    def __init__(self, view):
        self.cells: dict[str, dict[tuple[int, int], XlmCell]] = {}
        self.formula_rows: dict[str, dict[int, list[int]]] = {}
        self.sheet_of: dict[int, str] = {}
        self.held: dict[int, XlmValue] = {}
        for macrosheet in view.macrosheets():
            sheet = macrosheet.name.lower()
            for cell in macrosheet.body:
                self.insert(sheet, cell)

    def insert(self, sheet: str, cell: XlmCell) -> None:
        self.sheet_of[id(cell)] = sheet
        self.cells.setdefault(sheet, {})[(cell.row, cell.col)] = cell
        if cell.formula is not None:
            rows = self.formula_rows.setdefault(sheet, {}).setdefault(cell.col, [])
            bisect.insort(rows, cell.row)

    def remove(self, sheet: str, cell: XlmCell) -> None:
        cells = self.cells.get(sheet)
        if cells is not None:
            cells.pop((cell.row, cell.col), None)
        self.sheet_of.pop(id(cell), None)
        self.held.pop(id(cell), None)

    def find(self, sheet: str, row: int, col: int) -> XlmCell | None:
        cells = self.cells.get(sheet.lower())
        if cells is None:
            return None
        return cells.get((row, col))

    def hold(self, cell: XlmCell, value: XlmValue | None) -> None:
        if value is None:
            self.held.pop(id(cell), None)
        else:
            self.held[id(cell)] = value

    def update_formula_rows(self, sheet: str, cell: XlmCell, had: bool, has: bool) -> None:
        if had == has:
            return
        rows = self.formula_rows.setdefault(sheet, {}).setdefault(cell.col, [])
        if has:
            bisect.insort(rows, cell.row)
        elif cell.row in rows:
            rows.remove(cell.row)

    def fall_through(self, sheet: str, col: int, row: int) -> int | None:
        rows = self.formula_rows.get(sheet.lower(), {}).get(col)
        if not rows:
            return None
        position = bisect.bisect_left(rows, row)
        if position >= len(rows):
            return None
        found = rows[position]
        if found - row > _ROW_FALL_THROUGH_LIMIT:
            return None
        return found


class _Undo(Protocol):
    """
    One change the journal undoes: every state a run mutates — cells, names, aliases, files,
    memory regions, failed-write marks — lands in the journal as one record that knows how to
    take the state it found back.
    """

    def undo(self, engine: XlmEngine) -> None:
        ...


class _Write(NamedTuple):
    """
    One cell write the journal undoes: the sheet the cell sits on, the cell, the value and the
    formula it carried before, the value the runs had left in it, and whether the write created
    it.
    """

    sheet: str
    cell: XlmCell
    old_value: Any
    old_formula: Any
    old_held: XlmValue | None
    created: bool

    def undo(self, engine: XlmEngine) -> None:
        index = engine._index
        index.update_formula_rows(
            self.sheet,
            self.cell,
            self.cell.formula is not None,
            self.old_formula is not None,
        )
        set_value(self.cell, 'value', self.old_value)
        set_child(self.cell, 'formula', self.old_formula)
        index.hold(self.cell, self.old_held)
        if self.created:
            index.remove(self.sheet, self.cell)
            macrosheet = engine.view.macrosheet(self.sheet)
            if macrosheet is not None:
                edit = BodyEdit(macrosheet)
                edit.splice(self.cell, [])
                edit.apply()


class _NameDefine(NamedTuple):
    """
    One name definition the journal undoes: every entry the table held before the definition.
    """

    before: list[XlmNameEntry]

    def undo(self, engine: XlmEngine) -> None:
        engine.view.names.restore(self.before)


class _AliasRegistration(NamedTuple):
    """
    One registered alias the journal undoes: the target the name dispatched to before, if any.
    """

    name: str
    before: str | None

    def undo(self, engine: XlmEngine) -> None:
        if self.before is None:
            engine.aliases.pop(self.name, None)
        else:
            engine.aliases[self.name] = self.before


class _FileOpen(NamedTuple):
    """
    One file the program opened, the journal undoes by closing it again.
    """

    name: str

    def undo(self, engine: XlmEngine) -> None:
        engine.files.close(self.name)


class _FileWrite(NamedTuple):
    """
    One append to a file the journal undoes: the length of the content before the append.
    """

    name: str
    length: int

    def undo(self, engine: XlmEngine) -> None:
        engine.files.truncate(self.name, self.length)


class _RegionAlloc(NamedTuple):
    """
    One region of memory a command reserved, the journal undoes by releasing it.
    """

    def undo(self, engine: XlmEngine) -> None:
        engine.memory.release()


class _MemoryWrite(NamedTuple):
    """
    One write into a region of memory the journal undoes: the bytes the slice held before.
    """

    base: int
    before: bytes

    def undo(self, engine: XlmEngine) -> None:
        engine.memory.restore(self.base, self.before)


class _FailedMark(NamedTuple):
    """
    One failed-write mark the journal undoes, in the direction the mark was made.
    """

    reference: XlmReference
    added: bool

    def undo(self, engine: XlmEngine) -> None:
        if self.added:
            engine.failed_writes.discard(self.reference)
        else:
            engine.failed_writes.add(self.reference)


class XlmEngine:
    """
    The interpreter of a workbook view. The macrosheet model is read through coordinate indexes
    the engine maintains itself, so that cells the program writes at run time stay visible to
    every reference that reads them afterwards; every write lands in an undo journal that a
    false branch of a partial `IF` rolls back from. The start point and the deadline of the
    latest run are kept on the engine, because the trial runs of the day guess share them.
    """

    def __init__(
        self,
        view,
        output_level: int = 0,
        day: int = -1,
        timeout: int = 0,
        max_steps: int = 1_000_000,
        *,
        cells: _CellIndex | None = None,
    ):
        self.view = view
        self.output_level = output_level
        self.day = day
        self.timeout = timeout
        self.max_steps = max_steps
        self.start_point = ''
        self.deadline: float | None = None
        self.aliases: dict[str, str] = {}
        self.indent_level = 0
        self.indent_current_line = False
        self.arrival = XlmArrival.JUMP
        self.now_count = 0
        self.char_errors = 0
        self.iserror_at: XlmCursor | None = None
        self.iserror_flag = False
        self.iserror_repeats = 0
        self.files = XlmFiles()
        self.memory = XlmMemory()
        self.environment = XlmEnvironment()
        self.active_cell: XlmReference | None = None
        self.failed_writes: set[XlmReference] = set()
        self.call_stack: list[XlmCursor | XlmReturnSlot] = []
        self.branch_stack: list[XlmFrame] = []
        self.while_stack: list[XlmLoop] = []
        self._index = _CellIndex(view) if cells is None else cells
        self._journal: list[_Undo] = []
        self._evaluating: set[int] = set()
        self._resolving: set[tuple[str, int | None]] = set()
        self._buffers: list[list[XlmStep]] = []
        self._depth = 0
        self._steps = 0
        self._reads = 0
        self._halted = False

    @property
    def ignore_processing(self) -> bool:
        """
        Whether the engine skips the body of a loop: true while any open loop does not hold.
        """
        return any(not loop.holds for loop in self.while_stack)

    def run(self, start_point: str = '') -> Iterator[XlmStep]:
        """
        The trace of running the program: one step per executed cell. The entry points are the
        defined names that fuzzy-spell `auto_open` or `auto_close`; without one, the start point
        a caller named is the only entry. The run of an entry that exhausts its step budget ends
        with a step that says so, and a run that exhausts its deadline ends the same way and
        runs no further entry. An engine without a timeout of its own keeps the deadline it was
        given.
        """
        self.start_point = start_point
        if self.timeout > 0:
            self.deadline = time.monotonic() + self.timeout
        return self._run()

    def trial(self, day: int) -> XlmEngine:
        """
        A fresh engine for one trial run of the day guess: it shares this engine's view and the
        index over its cells — the cells, values, and names the runs so far have written — and
        the deadline of this engine's run, and answers the given day, but starts with no files,
        no memory, and no registered names of its own.
        """
        trial = XlmEngine(
            self.view,
            self.output_level,
            day,
            0,
            self.max_steps,
            cells=self._index,
        )
        trial.deadline = self.deadline
        return trial

    def read_reference(self, reference: XlmReference, cursor: XlmCursor) -> XlmValue:
        """
        The value a cell address holds. The sheet the reference fails to name is the sheet the
        cursor sits on. A cell the run left a value in — by a step that executed it, a value
        command, or a return — holds that value. Any other formula cell is evaluated with itself
        as the cursor its relative references resolve against, unless its evaluation is already
        under way, the reads of the running evaluation exhausted their budget, or it nests too
        deeply; such a cell, and a cell that holds only a value, reads as the value it was
        stored with, with the date flag a date cell carries. An address the workbook does not
        hold is blank.
        """
        sheet = reference.sheet or cursor.sheet
        reference = XlmReference(sheet, reference.row, reference.col)
        cell = self._find_cell(sheet, reference.row, reference.col)
        if cell is None:
            if reference in self.failed_writes:
                address = F'{column_letters(reference.col)}{reference.row}'
                return XlmValue(value=address, partial=True)
            return _blank(reference)
        held = self._index.held.get(id(cell))
        if held is not None:
            return dataclasses.replace(held, reference=reference)
        if not self._evaluating:
            self._reads = 0
        if (
            cell.formula is not None
            and id(cell) not in self._evaluating
            and self._reads < _READ_BUDGET
        ):
            self.check_deadline()
            self._reads += 1
            self._evaluating.add(id(cell))
            try:
                value = evaluate_expression(
                    self,
                    cell.formula,
                    XlmCursor(sheet, cell.row, cell.col),
                )
            except RecursionError:
                return self._stored(cell, reference)
            finally:
                self._evaluating.discard(id(cell))
            value.reference = reference
            return value
        return self._stored(cell, reference)

    def resolve_name(self, node: XlDefinedName, cursor: XlmCursor) -> XlmValue:
        """
        The value a defined name stands for: the formula the entry for it holds, evaluated
        through the evaluator with the scope of the reading cell. A name the workbook does not
        define, or one whose formula resolves no further, answers its own spelling; a formula
        that reads the name it defines, or nests too deeply, answers the spelling of the formula.
        """
        entry = self._resolve_entry(node.name, node.sheet, cursor)
        if entry is None or entry.formula is None:
            return XlmValue(value=node.name, text=node.name)
        key = (entry.name.lower(), entry.sheet)
        if key in self._resolving:
            return XlmValue(value=synthesize_formula(entry.formula))
        self._resolving.add(key)
        try:
            return evaluate_expression(self, entry.formula, cursor)
        except RecursionError:
            return XlmValue(value=synthesize_formula(entry.formula))
        finally:
            self._resolving.discard(key)

    def call(self, call: XlFunctionCall, cursor: XlmCursor, nested: bool = False) -> XlmOutcome:
        """
        The outcome of one function call. While the engine skips the body of a loop, every call
        but the heads and ends of loops is ignored. A call whose callee is itself a call answers
        what the inner call computed, spelled as the outer one; a registered alias dispatches
        under the name it was registered for; a callee that names a cell — directly or through
        a defined name — calls the macro there. A call is nested when an expression reads it
        instead of a step running it as the whole formula of its cell: a nested macro call runs
        its subroutine to the value it returns, and a nested command answers through the
        handler an expression reads it by, where it has one. Once the deadline of the run
        passed, no call answers.
        """
        self.check_deadline()
        name = callee_name(call)
        if self.ignore_processing and name not in _LOOP_COMMANDS:
            return XlmOutcome(
                value=XlmValue(value=0, text='', partial=True),
                status=XlmStatus.IGNORED,
            )
        if isinstance(call.callee, XlFunctionCall):
            inner = self.call(call.callee, cursor, nested)
            spelled = synthesize_formula(call)
            if inner.value.partial:
                return XlmOutcome(value=XlmValue(value=spelled, partial=True))
            inner.value.text = spelled
            return inner
        target = self.aliases.get(name)
        if target is not None:
            return self.dispatch(target, call, cursor, nested)
        if isinstance(call.callee, (XlA1Reference, XlR1C1Reference)):
            reference = resolve_reference(call.callee, cursor)
            return self._call_cell(reference, cursor, call, nested)
        if isinstance(call.callee, XlDefinedName):
            reference = self._name_reference(call.callee, cursor)
            if reference is not None:
                return self._call_cell(reference, cursor, call, nested)
        else:
            entry = self._resolve_entry(name, None, cursor)
            reference = self._reference_of(entry.formula if entry else None, cursor)
            if reference is not None:
                return self._call_cell(reference, cursor, call, nested)
        return self.dispatch(name, call, cursor, nested)

    def dispatch(
        self,
        name: str,
        call: XlFunctionCall,
        cursor: XlmCursor,
        nested: bool = False,
    ) -> XlmOutcome:
        """
        The outcome of a call under a name no alias rewrote. A nested call answers through the
        handler an expression reads its command by, where the command has one; every other
        command the handler table holds answers through its handler, and a command without one
        answers the spelled fallback.
        """
        handler = EXPRESSION_HANDLERS.get(name) if nested else None
        if handler is None:
            handler = HANDLERS.get(name)
        if handler is not None:
            return handler(self, call, cursor)
        return self._unknown_command(name, call, cursor)

    def return_value(self, value: XlmValue) -> tuple[XlmCursor | None, bool]:
        """
        Hand the value a `RETURN` computes to the macro call it returns from: a call that is the
        whole formula of its cell leaves the value in that cell and continues below it, and a
        call inside an expression takes the value as its own, which ends the run of the
        subroutine. A return without a call to return from continues below itself. The answer
        is the cursor to continue at and whether the run of a subroutine ended.
        """
        if not self.call_stack:
            return None, False
        caller = self.call_stack.pop()
        if isinstance(caller, XlmReturnSlot):
            caller.value = value
            return None, True
        cell = self._find_cell(caller.sheet, caller.row, caller.col)
        if cell is not None:
            self.write_value(cell, value)
        return self.next_formula_cell(caller), False

    def anchor(self, reference: XlmReference) -> XlmCursor | None:
        """
        The cursor the program continues at from an address: the first formula cell at or below
        it on the same column of the same macrosheet, or `None` when the sheet is no macrosheet,
        is not named at all, or holds no formula cell within the row bound.
        """
        sheet = reference.sheet
        if sheet is None or self.view.macrosheet(sheet) is None:
            return None
        row = self._index.fall_through(sheet, reference.col, reference.row)
        if row is None:
            return None
        return XlmCursor(sheet, row, reference.col)

    def next_formula_cell(self, cursor: XlmCursor) -> XlmCursor | None:
        """
        The cursor the row fall-through moves to: the first formula cell below the given one on
        the same column, bounded by the row bound.
        """
        row = self._index.fall_through(cursor.sheet, cursor.col, cursor.row + 1)
        if row is None:
            return None
        return XlmCursor(cursor.sheet, row, cursor.col)

    def column_blocks(self, cursor: XlmCursor) -> dict[int, XlmBlock]:
        """
        The block structure of the column of the cursor: every marker row mapped to the block
        `IF` it belongs to, paired over the live formula rows of the column, so that cells the
        program wrote at run time take part in the pairing.
        """
        sheet = cursor.sheet.lower()
        markers: list[tuple[int, str]] = []
        for row in self._index.formula_rows.get(sheet, {}).get(cursor.col, ()):
            cell = self._index.find(sheet, row, cursor.col)
            if cell is None or cell.formula is None:
                continue
            marker = marker_of(cell.formula)
            if marker is not None:
                markers.append((row, marker))
        return pair_blocks(markers)

    def argument_reference(
        self,
        call: XlFunctionCall,
        index: int,
        cursor: XlmCursor,
    ) -> XlmReference | None:
        """
        The cell address one argument of a call names, as `XlmEngine.node_reference` reads it.
        """
        if index >= len(call.arguments):
            return None
        return self.node_reference(call.arguments[index], cursor)

    def node_reference(self, node, cursor: XlmCursor) -> XlmReference | None:
        """
        The cell address a node names, with the sheet of a local reference filled in from the
        cursor: a reference resolves against it, a defined name through the reference its entry
        holds, and a string literal parses as a formula. Any other expression evaluates to the
        reference its value carries, or to the address its text spells.
        """
        if isinstance(node, (XlA1Reference, XlR1C1Reference)):
            return self._local(resolve_reference(node, cursor), cursor)
        if isinstance(node, XlDefinedName):
            reference = self._name_reference(node, cursor)
            if reference is not None:
                return self._local(reference, cursor)
        if isinstance(node, XlString):
            return self._spelled_reference(node.value, cursor)
        value = evaluate_expression(self, node, cursor)
        if value.reference is not None:
            return self._local(value.reference, cursor)
        if value.partial or not isinstance(value.value, str):
            return None
        return self._spelled_reference(value.value, cursor)

    def range_corners(self, node, cursor: XlmCursor) -> tuple[XlmReference, XlmReference] | None:
        """
        The corners a range argument names, or `None` when the argument is no range.
        """
        if not isinstance(node, XlBinaryExpression) or node.operator is not XlBinaryOperator.RANGE:
            return None
        corners = []
        for side in (node.left, node.right):
            reference = self.node_reference(side, cursor)
            if reference is None:
                return None
            corners.append(reference)
        return corners[0], corners[1]

    def range_cells(self, corners: tuple[XlmReference, XlmReference]) -> Iterator[XlmReference]:
        """
        The addresses of the cells a range spans that the workbook holds, in row-major order:
        the cells the index holds on the sheet of the range, the cells of that sheet where it
        is a worksheet, and the failed writes inside it, which read as the values the program
        left unfinished. The walk counts the cells the workbook holds, not the coordinates the
        rectangle spans.
        """
        first, last = corners
        sheet = first.sheet
        if sheet is None:
            return
        key = sheet.lower()
        row_min = min(first.row, last.row)
        row_max = max(first.row, last.row)
        col_min = min(first.col, last.col)
        col_max = max(first.col, last.col)
        held = self._index.cells.get(key, {})
        worksheet = self.view.worksheet(sheet)
        coordinates = set(held) | set(worksheet or ())
        coordinates.update(
            (reference.row, reference.col)
            for reference in self.failed_writes
            if (reference.sheet or '').lower() == key
        )
        inside = (
            coordinate
            for coordinate in coordinates
            if row_min <= coordinate[0] <= row_max
            and col_min <= coordinate[1] <= col_max
        )
        for row, col in sorted(inside):
            yield XlmReference(sheet, row, col)

    def snapshot(self) -> XlmSnapshot:
        """
        The state a false branch of a partial `IF` rolls back to: the position of the journal,
        and the state of the run the journal does not hold.
        """
        return XlmSnapshot(
            len(self._journal),
            tuple(self.call_stack),
            tuple(loop.copy() for loop in self.while_stack),
            self.active_cell,
        )

    def state_digest(self) -> bytes:
        """
        The digest of the whole state a run can observe: the cells its index holds, with the
        value each holds and the formula it spells, the entries of the name table, the
        registered aliases, the failed-write marks, the content of every open file, every
        region of memory, and the cell the program selected. A run that returns to the digest
        it anchored changes nothing the program can see.
        """
        parts: list[str] = []
        for sheet in sorted(self._index.cells):
            cells = self._index.cells[sheet]
            for row, col in sorted(cells):
                cell = cells[(row, col)]
                held = self._index.held.get(id(cell))
                value = held.text if held is not None else repr(cell.value)
                formula = cell.formula
                spelling = synthesize_formula(formula) if formula is not None else ''
                parts.append(F'{sheet}!{row}!{col}={value}|{spelling}')
        for entry in self.view.names.entries():
            formula = entry.formula
            spelling = synthesize_formula(formula) if formula is not None else ''
            parts.append(F'name:{entry.name.lower()}@{entry.sheet}={spelling}')
        for name in sorted(self.aliases):
            parts.append(F'alias:{name}={self.aliases[name]}')
        for reference in sorted(self.failed_writes):
            parts.append(F'failed:{reference.a1()}')
        for name, content in self.files.snapshot():
            parts.append(F'file:{name}={content}')
        for base, data in self.memory.snapshot():
            parts.append(F'memory:{base}={len(data)}:{data.hex()}')
        parts.append(F'active:{self.active_cell}')
        return hashlib.sha256('\n'.join(parts).encode()).digest()

    def rollback(self, snapshot: XlmSnapshot) -> None:
        """
        Return to a snapshot: every change the journal holds past its position is undone —
        cells, names, aliases, files, memory regions, and failed-write marks return to what they
        carried, and cells the writes created leave their sheet again — and the call stack, the
        open loops, and the selected cell are what they were.
        """
        while len(self._journal) > snapshot.journal:
            self._journal.pop().undo(self)
        self.call_stack = list(snapshot.call_stack)
        self.while_stack = [loop.copy() for loop in snapshot.loops]
        self.active_cell = snapshot.active_cell

    def write_value(self, cell: XlmCell, value: XlmValue) -> None:
        """
        Leave the value a step computed — or a macro call returned — in the cell it belongs to,
        undoable by a branch that rolls back: a later read of the cell answers that value
        instead of evaluating the formula of the cell again.
        """
        self._journal.append(_Write(
            self._index.sheet_of.get(id(cell), ''),
            cell,
            cell.value,
            cell.formula,
            self._index.held.get(id(cell)),
            False,
        ))
        held = _held(value)
        set_value(cell, 'value', held.value)
        self._index.hold(cell, held)

    def write_cell(
        self,
        reference: XlmReference,
        value: XlmValue,
        cursor: XlmCursor,
        value_only: bool = False,
    ) -> None:
        """
        Write a value into the macrosheet cell an address names, as the mutation commands do,
        undoable by a branch that rolls back; a cell that does not exist is created. A
        value-only write changes the value the cell holds and leaves its formula in place. Any
        other write enters the value the way a typed entry does: a text that starts with `=`
        installs a formula, which a later read of the cell evaluates, and anything else replaces
        the formula of the cell by a constant. An address off every macrosheet is not written.
        """
        sheet = reference.sheet or cursor.sheet
        macrosheet = self.view.macrosheet(sheet)
        if macrosheet is None:
            return
        key = macrosheet.name.lower()
        cell = self._index.find(key, reference.row, reference.col)
        created = cell is None
        if cell is None:
            cell = XlmCell(row=reference.row, col=reference.col)
            self._index.insert(key, cell)
            if macrosheet.body:
                edit = BodyEdit(macrosheet)
                edit.splice(macrosheet.body[-1], [macrosheet.body[-1], cell])
                edit.apply()
            else:
                set_body(macrosheet, [cell])
        self._journal.append(_Write(
            key,
            cell,
            cell.value,
            cell.formula,
            self._index.held.get(id(cell)),
            created,
        ))
        held: XlmValue | None = _held(value)
        formula = cell.formula
        if not value_only:
            formula = None
            if isinstance(value.value, str) and value.value.startswith('='):
                formula = parse_formula(value.value)
                held = None
        self._index.update_formula_rows(key, cell, cell.formula is not None, formula is not None)
        set_value(cell, 'value', value.value if held is None else held.value)
        set_child(cell, 'formula', formula)
        self._index.hold(cell, held)

    def define_name(self, entry: XlmNameEntry) -> None:
        """
        Define the name an entry carries, as the name commands do, undoable by a branch that
        rolls back.
        """
        self._journal.append(_NameDefine(self.view.names.entries()))
        self.view.names.define(entry)

    def assign_name(self, name: str, formula, cursor: XlmCursor) -> None:
        """
        Define a name as `SET.NAME` and the `FOR.CELL` loop do, undoable by a branch that rolls
        back: the definition replaces the entry a read on the sheet of the cursor resolves the
        name to, so that the sheet reads back what it assigned, and a name the sheet resolves to
        no entry is defined for the whole workbook. A name assigned no formula reads as one the
        workbook does not define.
        """
        entry = self.view.names.resolve(name, self.view.sheet_index(cursor.sheet))
        sheet = None if entry is None else entry.sheet
        self.define_name(XlmNameEntry(name=name, sheet=sheet, formula=formula))

    def register_alias(self, name: str, target: str) -> None:
        """
        Register the target a command name dispatches to, as `REGISTER` does, undoable by a
        branch that rolls back.
        """
        self._journal.append(_AliasRegistration(name, self.aliases.get(name)))
        self.aliases[name] = target

    def open_file(self, name: str, access: str = '1') -> None:
        """
        Open the name as a file, as `FOPEN` does, undoable by a branch that rolls back; a name
        that already answers a file changes nothing.
        """
        if self.files.opened(name):
            return
        self._journal.append(_FileOpen(name))
        self.files.open(name, access)

    def write_file(self, name: str, text: str) -> bool:
        """
        Append to a file the program opened, as `FWRITE` does, undoable by a branch that rolls
        back, reporting whether the file took the write.
        """
        length = self.files.size(name)
        if length is None:
            return False
        self._journal.append(_FileWrite(name, length))
        self.files.write(name, text)
        return True

    def allocate_memory(self, base: int, size: int) -> int:
        """
        Reserve a region of memory, as `Kernel32.VirtualAlloc` does, undoable by a branch that
        rolls back.
        """
        self._journal.append(_RegionAlloc())
        return self.memory.allocate(base, size)

    def write_memory(self, base: int, data: bytes, size: int) -> bool:
        """
        Write bytes into a region of memory, as the Kernel32 write commands do, undoable by a
        branch that rolls back, reporting whether a region took the whole write.
        """
        before = self.memory.peek(base, size)
        if before is None:
            return False
        self.memory.write(base, data, size)
        self._journal.append(_MemoryWrite(base, before))
        return True

    def mark_failed(self, reference: XlmReference) -> None:
        """
        Mark an address as the destination of a write that never finished, undoable by a
        branch that rolls back.
        """
        if reference in self.failed_writes:
            return
        self._journal.append(_FailedMark(reference, True))
        self.failed_writes.add(reference)

    def unmark_failed(self, reference: XlmReference) -> None:
        """
        Drop the failed-write mark of an address, undoable by a branch that rolls back.
        """
        if reference not in self.failed_writes:
            return
        self._journal.append(_FailedMark(reference, False))
        self.failed_writes.discard(reference)

    def _run(self) -> Iterator[XlmStep]:
        for reference in self.view.entry_points(self.start_point):
            if self._expired():
                return
            anchor = self.anchor(reference)
            if anchor is None:
                continue
            self._steps = 0
            self._halted = False
            self.while_stack.clear()
            self.call_stack.clear()
            yield from self._run_entry(anchor)

    def _run_entry(
        self,
        anchor: XlmCursor,
        caller: XlmReturnSlot | None = None,
    ) -> Iterator[XlmStep]:
        """
        The steps of running the program from one anchor until every branch it opened ran out.
        The run of a subroutine names the call it returns to: its branches are its own, it ends
        with the first value it returns, and a halt ends it together with the branch of every
        run that called it, each after its calling step; a budget or deadline it exhausts ends
        the run that called it at once. The steps of the subroutines a step called precede the
        step.
        """
        observed: list[tuple[str, int, int]] = []
        anchors: dict[tuple[tuple[str, int, int], ...], bytes] = {}
        digest_step = 0
        buffer: list[XlmStep] = []
        branches = self.branch_stack
        indent = 0 if caller is None else self.indent_level
        self.branch_stack = [XlmFrame(anchor, None, None, indent, '')]
        self._buffers.append(buffer)
        cursor = anchor
        try:
            while self.branch_stack:
                frame = self.branch_stack.pop()
                if frame.snapshot is not None:
                    self.rollback(frame.snapshot)
                    anchors.clear()
                cursor = frame.cursor
                node = frame.branch
                self.indent_level = frame.indent
                self.arrival = XlmArrival.REPLAY if frame.replays else XlmArrival.RESUME
                stack_record = True
                while cursor is not None:
                    self._steps += 1
                    if self._steps > self.max_steps:
                        raise _Exhausted(F'step budget of {self.max_steps} exhausted')
                    cell = self._find_cell(cursor.sheet, cursor.row, cursor.col)
                    if cell is None:
                        break
                    if node is None:
                        node = cell.formula
                        if node is None:
                            break
                    if stack_record:
                        previous_indent = self.indent_level - 1 if self.indent_level > 0 else 0
                    else:
                        previous_indent = self.indent_level
                    if caller is None:
                        self._reads = 0
                    self._evaluating.add(id(cell))
                    try:
                        with RecursionDepth(TREE_RECURSION_DEPTH):
                            outcome = self._execute(node, cursor)
                    finally:
                        self._evaluating.discard(id(cell))
                    status = outcome.status
                    if status is None:
                        partial = outcome.value.partial
                        status = (
                            XlmStatus.PartialEvaluation if partial else XlmStatus.FullEvaluation
                        )
                    text = outcome.value.text or ''
                    if not self.while_stack and text != 'NEXT':
                        observed.append((cursor.sheet.lower(), cursor.row, cursor.col))
                        if len(observed) >= _WINDOW_WARMUP:
                            window = tuple(observed[-10:])
                            if window in anchors:
                                if self._steps - digest_step >= _DIGEST_STEPS:
                                    digest_step = self._steps
                                    if self.state_digest() == anchors[window]:
                                        break
                            else:
                                anchors[window] = self.state_digest()
                                digest_step = self._steps
                    if status is not XlmStatus.IGNORED:
                        self.write_value(cell, outcome.value)
                    if stack_record:
                        text = (frame.desc + ' ' + text).strip()
                    if self.indent_current_line:
                        previous_indent = self.indent_level
                        self.indent_current_line = False
                    yield from buffer
                    buffer.clear()
                    if status is not XlmStatus.IGNORED:
                        yield XlmStep(
                            cursor.sheet,
                            cursor.row,
                            cursor.col,
                            status,
                            text,
                            previous_indent,
                            self._step_severity(node),
                        )
                    if caller is not None:
                        if outcome.halts:
                            self._halted = True
                        if caller.value is not None or self._halted:
                            return
                    elif self._halted:
                        self._halted = False
                        break
                    self.check_deadline()
                    if outcome.jump is not None:
                        cursor = outcome.jump
                        self.arrival = (
                            XlmArrival.JUMP if outcome.arrival is None else outcome.arrival
                        )
                    elif status in _FALL_THROUGH:
                        cursor = self.next_formula_cell(cursor)
                        self.arrival = XlmArrival.FALL
                    else:
                        break
                    node = None
                    stack_record = False
        except _Exhausted as exhausted:
            yield from buffer
            if caller is not None:
                raise
            yield self._error_step(cursor or anchor, str(exhausted))
        except Exception as error:
            yield from buffer
            yield self._error_step(cursor or anchor, F'{type(error).__name__}: {error}')
        finally:
            self._buffers.pop()
            self.branch_stack = branches

    def _subroutine(self, reference: XlmReference, spelled: str) -> XlmValue:
        """
        The value a macro call inside an expression computes: the subroutine runs from where
        the call names it until it returns, its steps join the trace ahead of the step that
        called it, and the value it returns is the value of the call. Calls and loops the
        subroutine leaves open end with it. A subroutine that never returns — it halts, runs out
        of formulas, or nests too deeply — leaves the call unfinished.
        """
        anchor = self.anchor(reference)
        if anchor is None or self._depth >= _SUBROUTINE_DEPTH_LIMIT:
            return XlmValue(value=spelled, partial=True)
        caller = XlmReturnSlot()
        indent = self.indent_level
        calls = len(self.call_stack)
        loops = len(self.while_stack)
        steps: list[XlmStep] = []
        self.call_stack.append(caller)
        self._depth += 1
        try:
            steps.extend(self._run_entry(anchor, caller))
        finally:
            self._depth -= 1
            self.indent_level = indent
            del self.call_stack[calls:]
            del self.while_stack[loops:]
            if self._buffers:
                self._buffers[-1].extend(steps)
        if caller.value is None:
            return XlmValue(value=spelled, partial=True)
        return _held(caller.value)

    def _expired(self) -> bool:
        return self.deadline is not None and time.monotonic() > self.deadline

    def check_deadline(self) -> None:
        """
        Raise the exhaustion of the run's deadline, for the walks a command takes inside one
        step, so that a walk over a rectangle the workbook barely fills still answers to the
        deadline of the run.
        """
        if self._expired():
            raise _Exhausted('the run exceeded its timeout')

    def _execute(self, node, cursor: XlmCursor) -> XlmOutcome:
        """
        The outcome of the step that runs a node as the whole formula of its cell. A call runs
        as a command, and any other expression is evaluated. A step that starts inside the body
        of a loop the engine skips is ignored like every command there, even when the expression
        holds the loop end that stops the skipping.
        """
        if isinstance(node, XlFunctionCall):
            return self.call(node, cursor)
        skipping = self.ignore_processing
        value = evaluate_expression(self, node, cursor)
        if skipping:
            return XlmOutcome(value=value, status=XlmStatus.IGNORED)
        return XlmOutcome(value=value)

    def _stored(self, cell: XlmCell, reference: XlmReference) -> XlmValue:
        if cell.value is None:
            return _blank(reference)
        return XlmValue(
            value=cell.value,
            date=cell.kind is CellKind.DATE,
            reference=reference,
        )

    def _step_severity(self, node) -> XlmSeverity:
        if isinstance(node, XlFunctionCall) and isinstance(node.callee, str):
            return severity(node.callee.strip('"'))
        return XlmSeverity.JUMP

    def _error_step(self, cursor: XlmCursor, message: str) -> XlmStep:
        return XlmStep(
            cursor.sheet,
            cursor.row,
            cursor.col,
            XlmStatus.Error,
            message,
            self.indent_level,
            XlmSeverity.JUMP,
        )

    def _unknown_command(self, name: str, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
        """
        The fallback for a command no handler answers: the call spelled unevaluated, with
        every argument evaluated for the side effects it may have had and a missing argument
        spelling the empty slot the call wrote.
        """
        arguments = [
            evaluate_expression(self, argument, cursor).text or ''
            for argument in call.arguments
        ]
        text = F'={name}({",".join(arguments)})'
        return XlmOutcome(value=XlmValue(value=text, partial=True))

    def _call_cell(
        self,
        reference: XlmReference,
        cursor: XlmCursor,
        call: XlFunctionCall,
        nested: bool,
    ) -> XlmOutcome:
        spelled = synthesize_formula(call)
        if reference.sheet is None:
            reference = XlmReference(cursor.sheet, reference.row, reference.col)
        if self.view.macrosheet(reference.sheet) is None:
            return XlmOutcome(value=XlmValue(value=0, text=spelled), status=XlmStatus.Error)
        if nested:
            return XlmOutcome(value=self._subroutine(reference, spelled))
        self.call_stack.append(cursor)
        return XlmOutcome(
            value=XlmValue(value=0, text=spelled),
            jump=self.anchor(reference),
            arrival=XlmArrival.RESUME,
        )

    def _resolve_entry(self, name: str, sheet: str | None, cursor: XlmCursor):
        if sheet is not None:
            return self.view.names.resolve(name, self.view.sheet_index(sheet))
        return self.view.names.resolve(name, self.view.sheet_index(cursor.sheet))

    def _name_reference(self, node: XlDefinedName, cursor: XlmCursor) -> XlmReference | None:
        entry = self._resolve_entry(node.name, node.sheet, cursor)
        if entry is None:
            return None
        return self._reference_of(entry.formula, cursor)

    def _reference_of(self, formula, cursor: XlmCursor) -> XlmReference | None:
        if isinstance(formula, (XlA1Reference, XlR1C1Reference)):
            return resolve_reference(formula, cursor)
        return None

    def _spelled_reference(self, text: str, cursor: XlmCursor) -> XlmReference | None:
        return self._local(self._reference_of(parse_formula(text), cursor), cursor)

    def _local(self, reference: XlmReference | None, cursor: XlmCursor) -> XlmReference | None:
        if reference is None:
            return None
        if reference.sheet is not None:
            return reference
        return XlmReference(cursor.sheet, reference.row, reference.col)

    def _find_cell(self, sheet: str, row: int, col: int) -> XlmCell | None:
        cell = self._index.find(sheet, row, col)
        if cell is not None:
            return cell
        worksheet = self.view.worksheet(sheet)
        if worksheet is not None:
            return worksheet.get((row, col))
        return None

Functions

def callee_name(call)

The name the callee of a call spells, without the quotes of a string literal; a callee that is a reference or another call is not named at all.

Expand source code Browse git
def callee_name(call: XlFunctionCall) -> str:
    """
    The name the callee of a call spells, without the quotes of a string literal; a callee
    that is a reference or another call is not named at all.
    """
    callee = call.callee
    if isinstance(callee, str):
        return callee.strip('"')
    if isinstance(callee, XlDefinedName):
        return callee.name
    return ''

Classes

class XlmEngine (view, output_level=0, day=-1, timeout=0, max_steps=1000000, *, cells=None)

The interpreter of a workbook view. The macrosheet model is read through coordinate indexes the engine maintains itself, so that cells the program writes at run time stay visible to every reference that reads them afterwards; every write lands in an undo journal that a false branch of a partial IF rolls back from. The start point and the deadline of the latest run are kept on the engine, because the trial runs of the day guess share them.

Expand source code Browse git
class XlmEngine:
    """
    The interpreter of a workbook view. The macrosheet model is read through coordinate indexes
    the engine maintains itself, so that cells the program writes at run time stay visible to
    every reference that reads them afterwards; every write lands in an undo journal that a
    false branch of a partial `IF` rolls back from. The start point and the deadline of the
    latest run are kept on the engine, because the trial runs of the day guess share them.
    """

    def __init__(
        self,
        view,
        output_level: int = 0,
        day: int = -1,
        timeout: int = 0,
        max_steps: int = 1_000_000,
        *,
        cells: _CellIndex | None = None,
    ):
        self.view = view
        self.output_level = output_level
        self.day = day
        self.timeout = timeout
        self.max_steps = max_steps
        self.start_point = ''
        self.deadline: float | None = None
        self.aliases: dict[str, str] = {}
        self.indent_level = 0
        self.indent_current_line = False
        self.arrival = XlmArrival.JUMP
        self.now_count = 0
        self.char_errors = 0
        self.iserror_at: XlmCursor | None = None
        self.iserror_flag = False
        self.iserror_repeats = 0
        self.files = XlmFiles()
        self.memory = XlmMemory()
        self.environment = XlmEnvironment()
        self.active_cell: XlmReference | None = None
        self.failed_writes: set[XlmReference] = set()
        self.call_stack: list[XlmCursor | XlmReturnSlot] = []
        self.branch_stack: list[XlmFrame] = []
        self.while_stack: list[XlmLoop] = []
        self._index = _CellIndex(view) if cells is None else cells
        self._journal: list[_Undo] = []
        self._evaluating: set[int] = set()
        self._resolving: set[tuple[str, int | None]] = set()
        self._buffers: list[list[XlmStep]] = []
        self._depth = 0
        self._steps = 0
        self._reads = 0
        self._halted = False

    @property
    def ignore_processing(self) -> bool:
        """
        Whether the engine skips the body of a loop: true while any open loop does not hold.
        """
        return any(not loop.holds for loop in self.while_stack)

    def run(self, start_point: str = '') -> Iterator[XlmStep]:
        """
        The trace of running the program: one step per executed cell. The entry points are the
        defined names that fuzzy-spell `auto_open` or `auto_close`; without one, the start point
        a caller named is the only entry. The run of an entry that exhausts its step budget ends
        with a step that says so, and a run that exhausts its deadline ends the same way and
        runs no further entry. An engine without a timeout of its own keeps the deadline it was
        given.
        """
        self.start_point = start_point
        if self.timeout > 0:
            self.deadline = time.monotonic() + self.timeout
        return self._run()

    def trial(self, day: int) -> XlmEngine:
        """
        A fresh engine for one trial run of the day guess: it shares this engine's view and the
        index over its cells — the cells, values, and names the runs so far have written — and
        the deadline of this engine's run, and answers the given day, but starts with no files,
        no memory, and no registered names of its own.
        """
        trial = XlmEngine(
            self.view,
            self.output_level,
            day,
            0,
            self.max_steps,
            cells=self._index,
        )
        trial.deadline = self.deadline
        return trial

    def read_reference(self, reference: XlmReference, cursor: XlmCursor) -> XlmValue:
        """
        The value a cell address holds. The sheet the reference fails to name is the sheet the
        cursor sits on. A cell the run left a value in — by a step that executed it, a value
        command, or a return — holds that value. Any other formula cell is evaluated with itself
        as the cursor its relative references resolve against, unless its evaluation is already
        under way, the reads of the running evaluation exhausted their budget, or it nests too
        deeply; such a cell, and a cell that holds only a value, reads as the value it was
        stored with, with the date flag a date cell carries. An address the workbook does not
        hold is blank.
        """
        sheet = reference.sheet or cursor.sheet
        reference = XlmReference(sheet, reference.row, reference.col)
        cell = self._find_cell(sheet, reference.row, reference.col)
        if cell is None:
            if reference in self.failed_writes:
                address = F'{column_letters(reference.col)}{reference.row}'
                return XlmValue(value=address, partial=True)
            return _blank(reference)
        held = self._index.held.get(id(cell))
        if held is not None:
            return dataclasses.replace(held, reference=reference)
        if not self._evaluating:
            self._reads = 0
        if (
            cell.formula is not None
            and id(cell) not in self._evaluating
            and self._reads < _READ_BUDGET
        ):
            self.check_deadline()
            self._reads += 1
            self._evaluating.add(id(cell))
            try:
                value = evaluate_expression(
                    self,
                    cell.formula,
                    XlmCursor(sheet, cell.row, cell.col),
                )
            except RecursionError:
                return self._stored(cell, reference)
            finally:
                self._evaluating.discard(id(cell))
            value.reference = reference
            return value
        return self._stored(cell, reference)

    def resolve_name(self, node: XlDefinedName, cursor: XlmCursor) -> XlmValue:
        """
        The value a defined name stands for: the formula the entry for it holds, evaluated
        through the evaluator with the scope of the reading cell. A name the workbook does not
        define, or one whose formula resolves no further, answers its own spelling; a formula
        that reads the name it defines, or nests too deeply, answers the spelling of the formula.
        """
        entry = self._resolve_entry(node.name, node.sheet, cursor)
        if entry is None or entry.formula is None:
            return XlmValue(value=node.name, text=node.name)
        key = (entry.name.lower(), entry.sheet)
        if key in self._resolving:
            return XlmValue(value=synthesize_formula(entry.formula))
        self._resolving.add(key)
        try:
            return evaluate_expression(self, entry.formula, cursor)
        except RecursionError:
            return XlmValue(value=synthesize_formula(entry.formula))
        finally:
            self._resolving.discard(key)

    def call(self, call: XlFunctionCall, cursor: XlmCursor, nested: bool = False) -> XlmOutcome:
        """
        The outcome of one function call. While the engine skips the body of a loop, every call
        but the heads and ends of loops is ignored. A call whose callee is itself a call answers
        what the inner call computed, spelled as the outer one; a registered alias dispatches
        under the name it was registered for; a callee that names a cell — directly or through
        a defined name — calls the macro there. A call is nested when an expression reads it
        instead of a step running it as the whole formula of its cell: a nested macro call runs
        its subroutine to the value it returns, and a nested command answers through the
        handler an expression reads it by, where it has one. Once the deadline of the run
        passed, no call answers.
        """
        self.check_deadline()
        name = callee_name(call)
        if self.ignore_processing and name not in _LOOP_COMMANDS:
            return XlmOutcome(
                value=XlmValue(value=0, text='', partial=True),
                status=XlmStatus.IGNORED,
            )
        if isinstance(call.callee, XlFunctionCall):
            inner = self.call(call.callee, cursor, nested)
            spelled = synthesize_formula(call)
            if inner.value.partial:
                return XlmOutcome(value=XlmValue(value=spelled, partial=True))
            inner.value.text = spelled
            return inner
        target = self.aliases.get(name)
        if target is not None:
            return self.dispatch(target, call, cursor, nested)
        if isinstance(call.callee, (XlA1Reference, XlR1C1Reference)):
            reference = resolve_reference(call.callee, cursor)
            return self._call_cell(reference, cursor, call, nested)
        if isinstance(call.callee, XlDefinedName):
            reference = self._name_reference(call.callee, cursor)
            if reference is not None:
                return self._call_cell(reference, cursor, call, nested)
        else:
            entry = self._resolve_entry(name, None, cursor)
            reference = self._reference_of(entry.formula if entry else None, cursor)
            if reference is not None:
                return self._call_cell(reference, cursor, call, nested)
        return self.dispatch(name, call, cursor, nested)

    def dispatch(
        self,
        name: str,
        call: XlFunctionCall,
        cursor: XlmCursor,
        nested: bool = False,
    ) -> XlmOutcome:
        """
        The outcome of a call under a name no alias rewrote. A nested call answers through the
        handler an expression reads its command by, where the command has one; every other
        command the handler table holds answers through its handler, and a command without one
        answers the spelled fallback.
        """
        handler = EXPRESSION_HANDLERS.get(name) if nested else None
        if handler is None:
            handler = HANDLERS.get(name)
        if handler is not None:
            return handler(self, call, cursor)
        return self._unknown_command(name, call, cursor)

    def return_value(self, value: XlmValue) -> tuple[XlmCursor | None, bool]:
        """
        Hand the value a `RETURN` computes to the macro call it returns from: a call that is the
        whole formula of its cell leaves the value in that cell and continues below it, and a
        call inside an expression takes the value as its own, which ends the run of the
        subroutine. A return without a call to return from continues below itself. The answer
        is the cursor to continue at and whether the run of a subroutine ended.
        """
        if not self.call_stack:
            return None, False
        caller = self.call_stack.pop()
        if isinstance(caller, XlmReturnSlot):
            caller.value = value
            return None, True
        cell = self._find_cell(caller.sheet, caller.row, caller.col)
        if cell is not None:
            self.write_value(cell, value)
        return self.next_formula_cell(caller), False

    def anchor(self, reference: XlmReference) -> XlmCursor | None:
        """
        The cursor the program continues at from an address: the first formula cell at or below
        it on the same column of the same macrosheet, or `None` when the sheet is no macrosheet,
        is not named at all, or holds no formula cell within the row bound.
        """
        sheet = reference.sheet
        if sheet is None or self.view.macrosheet(sheet) is None:
            return None
        row = self._index.fall_through(sheet, reference.col, reference.row)
        if row is None:
            return None
        return XlmCursor(sheet, row, reference.col)

    def next_formula_cell(self, cursor: XlmCursor) -> XlmCursor | None:
        """
        The cursor the row fall-through moves to: the first formula cell below the given one on
        the same column, bounded by the row bound.
        """
        row = self._index.fall_through(cursor.sheet, cursor.col, cursor.row + 1)
        if row is None:
            return None
        return XlmCursor(cursor.sheet, row, cursor.col)

    def column_blocks(self, cursor: XlmCursor) -> dict[int, XlmBlock]:
        """
        The block structure of the column of the cursor: every marker row mapped to the block
        `IF` it belongs to, paired over the live formula rows of the column, so that cells the
        program wrote at run time take part in the pairing.
        """
        sheet = cursor.sheet.lower()
        markers: list[tuple[int, str]] = []
        for row in self._index.formula_rows.get(sheet, {}).get(cursor.col, ()):
            cell = self._index.find(sheet, row, cursor.col)
            if cell is None or cell.formula is None:
                continue
            marker = marker_of(cell.formula)
            if marker is not None:
                markers.append((row, marker))
        return pair_blocks(markers)

    def argument_reference(
        self,
        call: XlFunctionCall,
        index: int,
        cursor: XlmCursor,
    ) -> XlmReference | None:
        """
        The cell address one argument of a call names, as `XlmEngine.node_reference` reads it.
        """
        if index >= len(call.arguments):
            return None
        return self.node_reference(call.arguments[index], cursor)

    def node_reference(self, node, cursor: XlmCursor) -> XlmReference | None:
        """
        The cell address a node names, with the sheet of a local reference filled in from the
        cursor: a reference resolves against it, a defined name through the reference its entry
        holds, and a string literal parses as a formula. Any other expression evaluates to the
        reference its value carries, or to the address its text spells.
        """
        if isinstance(node, (XlA1Reference, XlR1C1Reference)):
            return self._local(resolve_reference(node, cursor), cursor)
        if isinstance(node, XlDefinedName):
            reference = self._name_reference(node, cursor)
            if reference is not None:
                return self._local(reference, cursor)
        if isinstance(node, XlString):
            return self._spelled_reference(node.value, cursor)
        value = evaluate_expression(self, node, cursor)
        if value.reference is not None:
            return self._local(value.reference, cursor)
        if value.partial or not isinstance(value.value, str):
            return None
        return self._spelled_reference(value.value, cursor)

    def range_corners(self, node, cursor: XlmCursor) -> tuple[XlmReference, XlmReference] | None:
        """
        The corners a range argument names, or `None` when the argument is no range.
        """
        if not isinstance(node, XlBinaryExpression) or node.operator is not XlBinaryOperator.RANGE:
            return None
        corners = []
        for side in (node.left, node.right):
            reference = self.node_reference(side, cursor)
            if reference is None:
                return None
            corners.append(reference)
        return corners[0], corners[1]

    def range_cells(self, corners: tuple[XlmReference, XlmReference]) -> Iterator[XlmReference]:
        """
        The addresses of the cells a range spans that the workbook holds, in row-major order:
        the cells the index holds on the sheet of the range, the cells of that sheet where it
        is a worksheet, and the failed writes inside it, which read as the values the program
        left unfinished. The walk counts the cells the workbook holds, not the coordinates the
        rectangle spans.
        """
        first, last = corners
        sheet = first.sheet
        if sheet is None:
            return
        key = sheet.lower()
        row_min = min(first.row, last.row)
        row_max = max(first.row, last.row)
        col_min = min(first.col, last.col)
        col_max = max(first.col, last.col)
        held = self._index.cells.get(key, {})
        worksheet = self.view.worksheet(sheet)
        coordinates = set(held) | set(worksheet or ())
        coordinates.update(
            (reference.row, reference.col)
            for reference in self.failed_writes
            if (reference.sheet or '').lower() == key
        )
        inside = (
            coordinate
            for coordinate in coordinates
            if row_min <= coordinate[0] <= row_max
            and col_min <= coordinate[1] <= col_max
        )
        for row, col in sorted(inside):
            yield XlmReference(sheet, row, col)

    def snapshot(self) -> XlmSnapshot:
        """
        The state a false branch of a partial `IF` rolls back to: the position of the journal,
        and the state of the run the journal does not hold.
        """
        return XlmSnapshot(
            len(self._journal),
            tuple(self.call_stack),
            tuple(loop.copy() for loop in self.while_stack),
            self.active_cell,
        )

    def state_digest(self) -> bytes:
        """
        The digest of the whole state a run can observe: the cells its index holds, with the
        value each holds and the formula it spells, the entries of the name table, the
        registered aliases, the failed-write marks, the content of every open file, every
        region of memory, and the cell the program selected. A run that returns to the digest
        it anchored changes nothing the program can see.
        """
        parts: list[str] = []
        for sheet in sorted(self._index.cells):
            cells = self._index.cells[sheet]
            for row, col in sorted(cells):
                cell = cells[(row, col)]
                held = self._index.held.get(id(cell))
                value = held.text if held is not None else repr(cell.value)
                formula = cell.formula
                spelling = synthesize_formula(formula) if formula is not None else ''
                parts.append(F'{sheet}!{row}!{col}={value}|{spelling}')
        for entry in self.view.names.entries():
            formula = entry.formula
            spelling = synthesize_formula(formula) if formula is not None else ''
            parts.append(F'name:{entry.name.lower()}@{entry.sheet}={spelling}')
        for name in sorted(self.aliases):
            parts.append(F'alias:{name}={self.aliases[name]}')
        for reference in sorted(self.failed_writes):
            parts.append(F'failed:{reference.a1()}')
        for name, content in self.files.snapshot():
            parts.append(F'file:{name}={content}')
        for base, data in self.memory.snapshot():
            parts.append(F'memory:{base}={len(data)}:{data.hex()}')
        parts.append(F'active:{self.active_cell}')
        return hashlib.sha256('\n'.join(parts).encode()).digest()

    def rollback(self, snapshot: XlmSnapshot) -> None:
        """
        Return to a snapshot: every change the journal holds past its position is undone —
        cells, names, aliases, files, memory regions, and failed-write marks return to what they
        carried, and cells the writes created leave their sheet again — and the call stack, the
        open loops, and the selected cell are what they were.
        """
        while len(self._journal) > snapshot.journal:
            self._journal.pop().undo(self)
        self.call_stack = list(snapshot.call_stack)
        self.while_stack = [loop.copy() for loop in snapshot.loops]
        self.active_cell = snapshot.active_cell

    def write_value(self, cell: XlmCell, value: XlmValue) -> None:
        """
        Leave the value a step computed — or a macro call returned — in the cell it belongs to,
        undoable by a branch that rolls back: a later read of the cell answers that value
        instead of evaluating the formula of the cell again.
        """
        self._journal.append(_Write(
            self._index.sheet_of.get(id(cell), ''),
            cell,
            cell.value,
            cell.formula,
            self._index.held.get(id(cell)),
            False,
        ))
        held = _held(value)
        set_value(cell, 'value', held.value)
        self._index.hold(cell, held)

    def write_cell(
        self,
        reference: XlmReference,
        value: XlmValue,
        cursor: XlmCursor,
        value_only: bool = False,
    ) -> None:
        """
        Write a value into the macrosheet cell an address names, as the mutation commands do,
        undoable by a branch that rolls back; a cell that does not exist is created. A
        value-only write changes the value the cell holds and leaves its formula in place. Any
        other write enters the value the way a typed entry does: a text that starts with `=`
        installs a formula, which a later read of the cell evaluates, and anything else replaces
        the formula of the cell by a constant. An address off every macrosheet is not written.
        """
        sheet = reference.sheet or cursor.sheet
        macrosheet = self.view.macrosheet(sheet)
        if macrosheet is None:
            return
        key = macrosheet.name.lower()
        cell = self._index.find(key, reference.row, reference.col)
        created = cell is None
        if cell is None:
            cell = XlmCell(row=reference.row, col=reference.col)
            self._index.insert(key, cell)
            if macrosheet.body:
                edit = BodyEdit(macrosheet)
                edit.splice(macrosheet.body[-1], [macrosheet.body[-1], cell])
                edit.apply()
            else:
                set_body(macrosheet, [cell])
        self._journal.append(_Write(
            key,
            cell,
            cell.value,
            cell.formula,
            self._index.held.get(id(cell)),
            created,
        ))
        held: XlmValue | None = _held(value)
        formula = cell.formula
        if not value_only:
            formula = None
            if isinstance(value.value, str) and value.value.startswith('='):
                formula = parse_formula(value.value)
                held = None
        self._index.update_formula_rows(key, cell, cell.formula is not None, formula is not None)
        set_value(cell, 'value', value.value if held is None else held.value)
        set_child(cell, 'formula', formula)
        self._index.hold(cell, held)

    def define_name(self, entry: XlmNameEntry) -> None:
        """
        Define the name an entry carries, as the name commands do, undoable by a branch that
        rolls back.
        """
        self._journal.append(_NameDefine(self.view.names.entries()))
        self.view.names.define(entry)

    def assign_name(self, name: str, formula, cursor: XlmCursor) -> None:
        """
        Define a name as `SET.NAME` and the `FOR.CELL` loop do, undoable by a branch that rolls
        back: the definition replaces the entry a read on the sheet of the cursor resolves the
        name to, so that the sheet reads back what it assigned, and a name the sheet resolves to
        no entry is defined for the whole workbook. A name assigned no formula reads as one the
        workbook does not define.
        """
        entry = self.view.names.resolve(name, self.view.sheet_index(cursor.sheet))
        sheet = None if entry is None else entry.sheet
        self.define_name(XlmNameEntry(name=name, sheet=sheet, formula=formula))

    def register_alias(self, name: str, target: str) -> None:
        """
        Register the target a command name dispatches to, as `REGISTER` does, undoable by a
        branch that rolls back.
        """
        self._journal.append(_AliasRegistration(name, self.aliases.get(name)))
        self.aliases[name] = target

    def open_file(self, name: str, access: str = '1') -> None:
        """
        Open the name as a file, as `FOPEN` does, undoable by a branch that rolls back; a name
        that already answers a file changes nothing.
        """
        if self.files.opened(name):
            return
        self._journal.append(_FileOpen(name))
        self.files.open(name, access)

    def write_file(self, name: str, text: str) -> bool:
        """
        Append to a file the program opened, as `FWRITE` does, undoable by a branch that rolls
        back, reporting whether the file took the write.
        """
        length = self.files.size(name)
        if length is None:
            return False
        self._journal.append(_FileWrite(name, length))
        self.files.write(name, text)
        return True

    def allocate_memory(self, base: int, size: int) -> int:
        """
        Reserve a region of memory, as `Kernel32.VirtualAlloc` does, undoable by a branch that
        rolls back.
        """
        self._journal.append(_RegionAlloc())
        return self.memory.allocate(base, size)

    def write_memory(self, base: int, data: bytes, size: int) -> bool:
        """
        Write bytes into a region of memory, as the Kernel32 write commands do, undoable by a
        branch that rolls back, reporting whether a region took the whole write.
        """
        before = self.memory.peek(base, size)
        if before is None:
            return False
        self.memory.write(base, data, size)
        self._journal.append(_MemoryWrite(base, before))
        return True

    def mark_failed(self, reference: XlmReference) -> None:
        """
        Mark an address as the destination of a write that never finished, undoable by a
        branch that rolls back.
        """
        if reference in self.failed_writes:
            return
        self._journal.append(_FailedMark(reference, True))
        self.failed_writes.add(reference)

    def unmark_failed(self, reference: XlmReference) -> None:
        """
        Drop the failed-write mark of an address, undoable by a branch that rolls back.
        """
        if reference not in self.failed_writes:
            return
        self._journal.append(_FailedMark(reference, False))
        self.failed_writes.discard(reference)

    def _run(self) -> Iterator[XlmStep]:
        for reference in self.view.entry_points(self.start_point):
            if self._expired():
                return
            anchor = self.anchor(reference)
            if anchor is None:
                continue
            self._steps = 0
            self._halted = False
            self.while_stack.clear()
            self.call_stack.clear()
            yield from self._run_entry(anchor)

    def _run_entry(
        self,
        anchor: XlmCursor,
        caller: XlmReturnSlot | None = None,
    ) -> Iterator[XlmStep]:
        """
        The steps of running the program from one anchor until every branch it opened ran out.
        The run of a subroutine names the call it returns to: its branches are its own, it ends
        with the first value it returns, and a halt ends it together with the branch of every
        run that called it, each after its calling step; a budget or deadline it exhausts ends
        the run that called it at once. The steps of the subroutines a step called precede the
        step.
        """
        observed: list[tuple[str, int, int]] = []
        anchors: dict[tuple[tuple[str, int, int], ...], bytes] = {}
        digest_step = 0
        buffer: list[XlmStep] = []
        branches = self.branch_stack
        indent = 0 if caller is None else self.indent_level
        self.branch_stack = [XlmFrame(anchor, None, None, indent, '')]
        self._buffers.append(buffer)
        cursor = anchor
        try:
            while self.branch_stack:
                frame = self.branch_stack.pop()
                if frame.snapshot is not None:
                    self.rollback(frame.snapshot)
                    anchors.clear()
                cursor = frame.cursor
                node = frame.branch
                self.indent_level = frame.indent
                self.arrival = XlmArrival.REPLAY if frame.replays else XlmArrival.RESUME
                stack_record = True
                while cursor is not None:
                    self._steps += 1
                    if self._steps > self.max_steps:
                        raise _Exhausted(F'step budget of {self.max_steps} exhausted')
                    cell = self._find_cell(cursor.sheet, cursor.row, cursor.col)
                    if cell is None:
                        break
                    if node is None:
                        node = cell.formula
                        if node is None:
                            break
                    if stack_record:
                        previous_indent = self.indent_level - 1 if self.indent_level > 0 else 0
                    else:
                        previous_indent = self.indent_level
                    if caller is None:
                        self._reads = 0
                    self._evaluating.add(id(cell))
                    try:
                        with RecursionDepth(TREE_RECURSION_DEPTH):
                            outcome = self._execute(node, cursor)
                    finally:
                        self._evaluating.discard(id(cell))
                    status = outcome.status
                    if status is None:
                        partial = outcome.value.partial
                        status = (
                            XlmStatus.PartialEvaluation if partial else XlmStatus.FullEvaluation
                        )
                    text = outcome.value.text or ''
                    if not self.while_stack and text != 'NEXT':
                        observed.append((cursor.sheet.lower(), cursor.row, cursor.col))
                        if len(observed) >= _WINDOW_WARMUP:
                            window = tuple(observed[-10:])
                            if window in anchors:
                                if self._steps - digest_step >= _DIGEST_STEPS:
                                    digest_step = self._steps
                                    if self.state_digest() == anchors[window]:
                                        break
                            else:
                                anchors[window] = self.state_digest()
                                digest_step = self._steps
                    if status is not XlmStatus.IGNORED:
                        self.write_value(cell, outcome.value)
                    if stack_record:
                        text = (frame.desc + ' ' + text).strip()
                    if self.indent_current_line:
                        previous_indent = self.indent_level
                        self.indent_current_line = False
                    yield from buffer
                    buffer.clear()
                    if status is not XlmStatus.IGNORED:
                        yield XlmStep(
                            cursor.sheet,
                            cursor.row,
                            cursor.col,
                            status,
                            text,
                            previous_indent,
                            self._step_severity(node),
                        )
                    if caller is not None:
                        if outcome.halts:
                            self._halted = True
                        if caller.value is not None or self._halted:
                            return
                    elif self._halted:
                        self._halted = False
                        break
                    self.check_deadline()
                    if outcome.jump is not None:
                        cursor = outcome.jump
                        self.arrival = (
                            XlmArrival.JUMP if outcome.arrival is None else outcome.arrival
                        )
                    elif status in _FALL_THROUGH:
                        cursor = self.next_formula_cell(cursor)
                        self.arrival = XlmArrival.FALL
                    else:
                        break
                    node = None
                    stack_record = False
        except _Exhausted as exhausted:
            yield from buffer
            if caller is not None:
                raise
            yield self._error_step(cursor or anchor, str(exhausted))
        except Exception as error:
            yield from buffer
            yield self._error_step(cursor or anchor, F'{type(error).__name__}: {error}')
        finally:
            self._buffers.pop()
            self.branch_stack = branches

    def _subroutine(self, reference: XlmReference, spelled: str) -> XlmValue:
        """
        The value a macro call inside an expression computes: the subroutine runs from where
        the call names it until it returns, its steps join the trace ahead of the step that
        called it, and the value it returns is the value of the call. Calls and loops the
        subroutine leaves open end with it. A subroutine that never returns — it halts, runs out
        of formulas, or nests too deeply — leaves the call unfinished.
        """
        anchor = self.anchor(reference)
        if anchor is None or self._depth >= _SUBROUTINE_DEPTH_LIMIT:
            return XlmValue(value=spelled, partial=True)
        caller = XlmReturnSlot()
        indent = self.indent_level
        calls = len(self.call_stack)
        loops = len(self.while_stack)
        steps: list[XlmStep] = []
        self.call_stack.append(caller)
        self._depth += 1
        try:
            steps.extend(self._run_entry(anchor, caller))
        finally:
            self._depth -= 1
            self.indent_level = indent
            del self.call_stack[calls:]
            del self.while_stack[loops:]
            if self._buffers:
                self._buffers[-1].extend(steps)
        if caller.value is None:
            return XlmValue(value=spelled, partial=True)
        return _held(caller.value)

    def _expired(self) -> bool:
        return self.deadline is not None and time.monotonic() > self.deadline

    def check_deadline(self) -> None:
        """
        Raise the exhaustion of the run's deadline, for the walks a command takes inside one
        step, so that a walk over a rectangle the workbook barely fills still answers to the
        deadline of the run.
        """
        if self._expired():
            raise _Exhausted('the run exceeded its timeout')

    def _execute(self, node, cursor: XlmCursor) -> XlmOutcome:
        """
        The outcome of the step that runs a node as the whole formula of its cell. A call runs
        as a command, and any other expression is evaluated. A step that starts inside the body
        of a loop the engine skips is ignored like every command there, even when the expression
        holds the loop end that stops the skipping.
        """
        if isinstance(node, XlFunctionCall):
            return self.call(node, cursor)
        skipping = self.ignore_processing
        value = evaluate_expression(self, node, cursor)
        if skipping:
            return XlmOutcome(value=value, status=XlmStatus.IGNORED)
        return XlmOutcome(value=value)

    def _stored(self, cell: XlmCell, reference: XlmReference) -> XlmValue:
        if cell.value is None:
            return _blank(reference)
        return XlmValue(
            value=cell.value,
            date=cell.kind is CellKind.DATE,
            reference=reference,
        )

    def _step_severity(self, node) -> XlmSeverity:
        if isinstance(node, XlFunctionCall) and isinstance(node.callee, str):
            return severity(node.callee.strip('"'))
        return XlmSeverity.JUMP

    def _error_step(self, cursor: XlmCursor, message: str) -> XlmStep:
        return XlmStep(
            cursor.sheet,
            cursor.row,
            cursor.col,
            XlmStatus.Error,
            message,
            self.indent_level,
            XlmSeverity.JUMP,
        )

    def _unknown_command(self, name: str, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
        """
        The fallback for a command no handler answers: the call spelled unevaluated, with
        every argument evaluated for the side effects it may have had and a missing argument
        spelling the empty slot the call wrote.
        """
        arguments = [
            evaluate_expression(self, argument, cursor).text or ''
            for argument in call.arguments
        ]
        text = F'={name}({",".join(arguments)})'
        return XlmOutcome(value=XlmValue(value=text, partial=True))

    def _call_cell(
        self,
        reference: XlmReference,
        cursor: XlmCursor,
        call: XlFunctionCall,
        nested: bool,
    ) -> XlmOutcome:
        spelled = synthesize_formula(call)
        if reference.sheet is None:
            reference = XlmReference(cursor.sheet, reference.row, reference.col)
        if self.view.macrosheet(reference.sheet) is None:
            return XlmOutcome(value=XlmValue(value=0, text=spelled), status=XlmStatus.Error)
        if nested:
            return XlmOutcome(value=self._subroutine(reference, spelled))
        self.call_stack.append(cursor)
        return XlmOutcome(
            value=XlmValue(value=0, text=spelled),
            jump=self.anchor(reference),
            arrival=XlmArrival.RESUME,
        )

    def _resolve_entry(self, name: str, sheet: str | None, cursor: XlmCursor):
        if sheet is not None:
            return self.view.names.resolve(name, self.view.sheet_index(sheet))
        return self.view.names.resolve(name, self.view.sheet_index(cursor.sheet))

    def _name_reference(self, node: XlDefinedName, cursor: XlmCursor) -> XlmReference | None:
        entry = self._resolve_entry(node.name, node.sheet, cursor)
        if entry is None:
            return None
        return self._reference_of(entry.formula, cursor)

    def _reference_of(self, formula, cursor: XlmCursor) -> XlmReference | None:
        if isinstance(formula, (XlA1Reference, XlR1C1Reference)):
            return resolve_reference(formula, cursor)
        return None

    def _spelled_reference(self, text: str, cursor: XlmCursor) -> XlmReference | None:
        return self._local(self._reference_of(parse_formula(text), cursor), cursor)

    def _local(self, reference: XlmReference | None, cursor: XlmCursor) -> XlmReference | None:
        if reference is None:
            return None
        if reference.sheet is not None:
            return reference
        return XlmReference(cursor.sheet, reference.row, reference.col)

    def _find_cell(self, sheet: str, row: int, col: int) -> XlmCell | None:
        cell = self._index.find(sheet, row, col)
        if cell is not None:
            return cell
        worksheet = self.view.worksheet(sheet)
        if worksheet is not None:
            return worksheet.get((row, col))
        return None

Subclasses

  • refinery.lib.scripts.xlm.deobfuscation.fold._StaticEngine

Instance variables

var ignore_processing

Whether the engine skips the body of a loop: true while any open loop does not hold.

Expand source code Browse git
@property
def ignore_processing(self) -> bool:
    """
    Whether the engine skips the body of a loop: true while any open loop does not hold.
    """
    return any(not loop.holds for loop in self.while_stack)

Methods

def run(self, start_point='')

The trace of running the program: one step per executed cell. The entry points are the defined names that fuzzy-spell auto_open or auto_close; without one, the start point a caller named is the only entry. The run of an entry that exhausts its step budget ends with a step that says so, and a run that exhausts its deadline ends the same way and runs no further entry. An engine without a timeout of its own keeps the deadline it was given.

Expand source code Browse git
def run(self, start_point: str = '') -> Iterator[XlmStep]:
    """
    The trace of running the program: one step per executed cell. The entry points are the
    defined names that fuzzy-spell `auto_open` or `auto_close`; without one, the start point
    a caller named is the only entry. The run of an entry that exhausts its step budget ends
    with a step that says so, and a run that exhausts its deadline ends the same way and
    runs no further entry. An engine without a timeout of its own keeps the deadline it was
    given.
    """
    self.start_point = start_point
    if self.timeout > 0:
        self.deadline = time.monotonic() + self.timeout
    return self._run()
def trial(self, day)

A fresh engine for one trial run of the day guess: it shares this engine's view and the index over its cells — the cells, values, and names the runs so far have written — and the deadline of this engine's run, and answers the given day, but starts with no files, no memory, and no registered names of its own.

Expand source code Browse git
def trial(self, day: int) -> XlmEngine:
    """
    A fresh engine for one trial run of the day guess: it shares this engine's view and the
    index over its cells — the cells, values, and names the runs so far have written — and
    the deadline of this engine's run, and answers the given day, but starts with no files,
    no memory, and no registered names of its own.
    """
    trial = XlmEngine(
        self.view,
        self.output_level,
        day,
        0,
        self.max_steps,
        cells=self._index,
    )
    trial.deadline = self.deadline
    return trial
def read_reference(self, reference, cursor)

The value a cell address holds. The sheet the reference fails to name is the sheet the cursor sits on. A cell the run left a value in — by a step that executed it, a value command, or a return — holds that value. Any other formula cell is evaluated with itself as the cursor its relative references resolve against, unless its evaluation is already under way, the reads of the running evaluation exhausted their budget, or it nests too deeply; such a cell, and a cell that holds only a value, reads as the value it was stored with, with the date flag a date cell carries. An address the workbook does not hold is blank.

Expand source code Browse git
def read_reference(self, reference: XlmReference, cursor: XlmCursor) -> XlmValue:
    """
    The value a cell address holds. The sheet the reference fails to name is the sheet the
    cursor sits on. A cell the run left a value in — by a step that executed it, a value
    command, or a return — holds that value. Any other formula cell is evaluated with itself
    as the cursor its relative references resolve against, unless its evaluation is already
    under way, the reads of the running evaluation exhausted their budget, or it nests too
    deeply; such a cell, and a cell that holds only a value, reads as the value it was
    stored with, with the date flag a date cell carries. An address the workbook does not
    hold is blank.
    """
    sheet = reference.sheet or cursor.sheet
    reference = XlmReference(sheet, reference.row, reference.col)
    cell = self._find_cell(sheet, reference.row, reference.col)
    if cell is None:
        if reference in self.failed_writes:
            address = F'{column_letters(reference.col)}{reference.row}'
            return XlmValue(value=address, partial=True)
        return _blank(reference)
    held = self._index.held.get(id(cell))
    if held is not None:
        return dataclasses.replace(held, reference=reference)
    if not self._evaluating:
        self._reads = 0
    if (
        cell.formula is not None
        and id(cell) not in self._evaluating
        and self._reads < _READ_BUDGET
    ):
        self.check_deadline()
        self._reads += 1
        self._evaluating.add(id(cell))
        try:
            value = evaluate_expression(
                self,
                cell.formula,
                XlmCursor(sheet, cell.row, cell.col),
            )
        except RecursionError:
            return self._stored(cell, reference)
        finally:
            self._evaluating.discard(id(cell))
        value.reference = reference
        return value
    return self._stored(cell, reference)
def resolve_name(self, node, cursor)

The value a defined name stands for: the formula the entry for it holds, evaluated through the evaluator with the scope of the reading cell. A name the workbook does not define, or one whose formula resolves no further, answers its own spelling; a formula that reads the name it defines, or nests too deeply, answers the spelling of the formula.

Expand source code Browse git
def resolve_name(self, node: XlDefinedName, cursor: XlmCursor) -> XlmValue:
    """
    The value a defined name stands for: the formula the entry for it holds, evaluated
    through the evaluator with the scope of the reading cell. A name the workbook does not
    define, or one whose formula resolves no further, answers its own spelling; a formula
    that reads the name it defines, or nests too deeply, answers the spelling of the formula.
    """
    entry = self._resolve_entry(node.name, node.sheet, cursor)
    if entry is None or entry.formula is None:
        return XlmValue(value=node.name, text=node.name)
    key = (entry.name.lower(), entry.sheet)
    if key in self._resolving:
        return XlmValue(value=synthesize_formula(entry.formula))
    self._resolving.add(key)
    try:
        return evaluate_expression(self, entry.formula, cursor)
    except RecursionError:
        return XlmValue(value=synthesize_formula(entry.formula))
    finally:
        self._resolving.discard(key)
def call(self, call, cursor, nested=False)

The outcome of one function call. While the engine skips the body of a loop, every call but the heads and ends of loops is ignored. A call whose callee is itself a call answers what the inner call computed, spelled as the outer one; a registered alias dispatches under the name it was registered for; a callee that names a cell — directly or through a defined name — calls the macro there. A call is nested when an expression reads it instead of a step running it as the whole formula of its cell: a nested macro call runs its subroutine to the value it returns, and a nested command answers through the handler an expression reads it by, where it has one. Once the deadline of the run passed, no call answers.

Expand source code Browse git
def call(self, call: XlFunctionCall, cursor: XlmCursor, nested: bool = False) -> XlmOutcome:
    """
    The outcome of one function call. While the engine skips the body of a loop, every call
    but the heads and ends of loops is ignored. A call whose callee is itself a call answers
    what the inner call computed, spelled as the outer one; a registered alias dispatches
    under the name it was registered for; a callee that names a cell — directly or through
    a defined name — calls the macro there. A call is nested when an expression reads it
    instead of a step running it as the whole formula of its cell: a nested macro call runs
    its subroutine to the value it returns, and a nested command answers through the
    handler an expression reads it by, where it has one. Once the deadline of the run
    passed, no call answers.
    """
    self.check_deadline()
    name = callee_name(call)
    if self.ignore_processing and name not in _LOOP_COMMANDS:
        return XlmOutcome(
            value=XlmValue(value=0, text='', partial=True),
            status=XlmStatus.IGNORED,
        )
    if isinstance(call.callee, XlFunctionCall):
        inner = self.call(call.callee, cursor, nested)
        spelled = synthesize_formula(call)
        if inner.value.partial:
            return XlmOutcome(value=XlmValue(value=spelled, partial=True))
        inner.value.text = spelled
        return inner
    target = self.aliases.get(name)
    if target is not None:
        return self.dispatch(target, call, cursor, nested)
    if isinstance(call.callee, (XlA1Reference, XlR1C1Reference)):
        reference = resolve_reference(call.callee, cursor)
        return self._call_cell(reference, cursor, call, nested)
    if isinstance(call.callee, XlDefinedName):
        reference = self._name_reference(call.callee, cursor)
        if reference is not None:
            return self._call_cell(reference, cursor, call, nested)
    else:
        entry = self._resolve_entry(name, None, cursor)
        reference = self._reference_of(entry.formula if entry else None, cursor)
        if reference is not None:
            return self._call_cell(reference, cursor, call, nested)
    return self.dispatch(name, call, cursor, nested)
def dispatch(self, name, call, cursor, nested=False)

The outcome of a call under a name no alias rewrote. A nested call answers through the handler an expression reads its command by, where the command has one; every other command the handler table holds answers through its handler, and a command without one answers the spelled fallback.

Expand source code Browse git
def dispatch(
    self,
    name: str,
    call: XlFunctionCall,
    cursor: XlmCursor,
    nested: bool = False,
) -> XlmOutcome:
    """
    The outcome of a call under a name no alias rewrote. A nested call answers through the
    handler an expression reads its command by, where the command has one; every other
    command the handler table holds answers through its handler, and a command without one
    answers the spelled fallback.
    """
    handler = EXPRESSION_HANDLERS.get(name) if nested else None
    if handler is None:
        handler = HANDLERS.get(name)
    if handler is not None:
        return handler(self, call, cursor)
    return self._unknown_command(name, call, cursor)
def return_value(self, value)

Hand the value a RETURN computes to the macro call it returns from: a call that is the whole formula of its cell leaves the value in that cell and continues below it, and a call inside an expression takes the value as its own, which ends the run of the subroutine. A return without a call to return from continues below itself. The answer is the cursor to continue at and whether the run of a subroutine ended.

Expand source code Browse git
def return_value(self, value: XlmValue) -> tuple[XlmCursor | None, bool]:
    """
    Hand the value a `RETURN` computes to the macro call it returns from: a call that is the
    whole formula of its cell leaves the value in that cell and continues below it, and a
    call inside an expression takes the value as its own, which ends the run of the
    subroutine. A return without a call to return from continues below itself. The answer
    is the cursor to continue at and whether the run of a subroutine ended.
    """
    if not self.call_stack:
        return None, False
    caller = self.call_stack.pop()
    if isinstance(caller, XlmReturnSlot):
        caller.value = value
        return None, True
    cell = self._find_cell(caller.sheet, caller.row, caller.col)
    if cell is not None:
        self.write_value(cell, value)
    return self.next_formula_cell(caller), False
def anchor(self, reference)

The cursor the program continues at from an address: the first formula cell at or below it on the same column of the same macrosheet, or None when the sheet is no macrosheet, is not named at all, or holds no formula cell within the row bound.

Expand source code Browse git
def anchor(self, reference: XlmReference) -> XlmCursor | None:
    """
    The cursor the program continues at from an address: the first formula cell at or below
    it on the same column of the same macrosheet, or `None` when the sheet is no macrosheet,
    is not named at all, or holds no formula cell within the row bound.
    """
    sheet = reference.sheet
    if sheet is None or self.view.macrosheet(sheet) is None:
        return None
    row = self._index.fall_through(sheet, reference.col, reference.row)
    if row is None:
        return None
    return XlmCursor(sheet, row, reference.col)
def next_formula_cell(self, cursor)

The cursor the row fall-through moves to: the first formula cell below the given one on the same column, bounded by the row bound.

Expand source code Browse git
def next_formula_cell(self, cursor: XlmCursor) -> XlmCursor | None:
    """
    The cursor the row fall-through moves to: the first formula cell below the given one on
    the same column, bounded by the row bound.
    """
    row = self._index.fall_through(cursor.sheet, cursor.col, cursor.row + 1)
    if row is None:
        return None
    return XlmCursor(cursor.sheet, row, cursor.col)
def column_blocks(self, cursor)

The block structure of the column of the cursor: every marker row mapped to the block IF it belongs to, paired over the live formula rows of the column, so that cells the program wrote at run time take part in the pairing.

Expand source code Browse git
def column_blocks(self, cursor: XlmCursor) -> dict[int, XlmBlock]:
    """
    The block structure of the column of the cursor: every marker row mapped to the block
    `IF` it belongs to, paired over the live formula rows of the column, so that cells the
    program wrote at run time take part in the pairing.
    """
    sheet = cursor.sheet.lower()
    markers: list[tuple[int, str]] = []
    for row in self._index.formula_rows.get(sheet, {}).get(cursor.col, ()):
        cell = self._index.find(sheet, row, cursor.col)
        if cell is None or cell.formula is None:
            continue
        marker = marker_of(cell.formula)
        if marker is not None:
            markers.append((row, marker))
    return pair_blocks(markers)
def argument_reference(self, call, index, cursor)

The cell address one argument of a call names, as XlmEngine.node_reference() reads it.

Expand source code Browse git
def argument_reference(
    self,
    call: XlFunctionCall,
    index: int,
    cursor: XlmCursor,
) -> XlmReference | None:
    """
    The cell address one argument of a call names, as `XlmEngine.node_reference` reads it.
    """
    if index >= len(call.arguments):
        return None
    return self.node_reference(call.arguments[index], cursor)
def node_reference(self, node, cursor)

The cell address a node names, with the sheet of a local reference filled in from the cursor: a reference resolves against it, a defined name through the reference its entry holds, and a string literal parses as a formula. Any other expression evaluates to the reference its value carries, or to the address its text spells.

Expand source code Browse git
def node_reference(self, node, cursor: XlmCursor) -> XlmReference | None:
    """
    The cell address a node names, with the sheet of a local reference filled in from the
    cursor: a reference resolves against it, a defined name through the reference its entry
    holds, and a string literal parses as a formula. Any other expression evaluates to the
    reference its value carries, or to the address its text spells.
    """
    if isinstance(node, (XlA1Reference, XlR1C1Reference)):
        return self._local(resolve_reference(node, cursor), cursor)
    if isinstance(node, XlDefinedName):
        reference = self._name_reference(node, cursor)
        if reference is not None:
            return self._local(reference, cursor)
    if isinstance(node, XlString):
        return self._spelled_reference(node.value, cursor)
    value = evaluate_expression(self, node, cursor)
    if value.reference is not None:
        return self._local(value.reference, cursor)
    if value.partial or not isinstance(value.value, str):
        return None
    return self._spelled_reference(value.value, cursor)
def range_corners(self, node, cursor)

The corners a range argument names, or None when the argument is no range.

Expand source code Browse git
def range_corners(self, node, cursor: XlmCursor) -> tuple[XlmReference, XlmReference] | None:
    """
    The corners a range argument names, or `None` when the argument is no range.
    """
    if not isinstance(node, XlBinaryExpression) or node.operator is not XlBinaryOperator.RANGE:
        return None
    corners = []
    for side in (node.left, node.right):
        reference = self.node_reference(side, cursor)
        if reference is None:
            return None
        corners.append(reference)
    return corners[0], corners[1]
def range_cells(self, corners)

The addresses of the cells a range spans that the workbook holds, in row-major order: the cells the index holds on the sheet of the range, the cells of that sheet where it is a worksheet, and the failed writes inside it, which read as the values the program left unfinished. The walk counts the cells the workbook holds, not the coordinates the rectangle spans.

Expand source code Browse git
def range_cells(self, corners: tuple[XlmReference, XlmReference]) -> Iterator[XlmReference]:
    """
    The addresses of the cells a range spans that the workbook holds, in row-major order:
    the cells the index holds on the sheet of the range, the cells of that sheet where it
    is a worksheet, and the failed writes inside it, which read as the values the program
    left unfinished. The walk counts the cells the workbook holds, not the coordinates the
    rectangle spans.
    """
    first, last = corners
    sheet = first.sheet
    if sheet is None:
        return
    key = sheet.lower()
    row_min = min(first.row, last.row)
    row_max = max(first.row, last.row)
    col_min = min(first.col, last.col)
    col_max = max(first.col, last.col)
    held = self._index.cells.get(key, {})
    worksheet = self.view.worksheet(sheet)
    coordinates = set(held) | set(worksheet or ())
    coordinates.update(
        (reference.row, reference.col)
        for reference in self.failed_writes
        if (reference.sheet or '').lower() == key
    )
    inside = (
        coordinate
        for coordinate in coordinates
        if row_min <= coordinate[0] <= row_max
        and col_min <= coordinate[1] <= col_max
    )
    for row, col in sorted(inside):
        yield XlmReference(sheet, row, col)
def snapshot(self)

The state a false branch of a partial IF rolls back to: the position of the journal, and the state of the run the journal does not hold.

Expand source code Browse git
def snapshot(self) -> XlmSnapshot:
    """
    The state a false branch of a partial `IF` rolls back to: the position of the journal,
    and the state of the run the journal does not hold.
    """
    return XlmSnapshot(
        len(self._journal),
        tuple(self.call_stack),
        tuple(loop.copy() for loop in self.while_stack),
        self.active_cell,
    )
def state_digest(self)

The digest of the whole state a run can observe: the cells its index holds, with the value each holds and the formula it spells, the entries of the name table, the registered aliases, the failed-write marks, the content of every open file, every region of memory, and the cell the program selected. A run that returns to the digest it anchored changes nothing the program can see.

Expand source code Browse git
def state_digest(self) -> bytes:
    """
    The digest of the whole state a run can observe: the cells its index holds, with the
    value each holds and the formula it spells, the entries of the name table, the
    registered aliases, the failed-write marks, the content of every open file, every
    region of memory, and the cell the program selected. A run that returns to the digest
    it anchored changes nothing the program can see.
    """
    parts: list[str] = []
    for sheet in sorted(self._index.cells):
        cells = self._index.cells[sheet]
        for row, col in sorted(cells):
            cell = cells[(row, col)]
            held = self._index.held.get(id(cell))
            value = held.text if held is not None else repr(cell.value)
            formula = cell.formula
            spelling = synthesize_formula(formula) if formula is not None else ''
            parts.append(F'{sheet}!{row}!{col}={value}|{spelling}')
    for entry in self.view.names.entries():
        formula = entry.formula
        spelling = synthesize_formula(formula) if formula is not None else ''
        parts.append(F'name:{entry.name.lower()}@{entry.sheet}={spelling}')
    for name in sorted(self.aliases):
        parts.append(F'alias:{name}={self.aliases[name]}')
    for reference in sorted(self.failed_writes):
        parts.append(F'failed:{reference.a1()}')
    for name, content in self.files.snapshot():
        parts.append(F'file:{name}={content}')
    for base, data in self.memory.snapshot():
        parts.append(F'memory:{base}={len(data)}:{data.hex()}')
    parts.append(F'active:{self.active_cell}')
    return hashlib.sha256('\n'.join(parts).encode()).digest()
def rollback(self, snapshot)

Return to a snapshot: every change the journal holds past its position is undone — cells, names, aliases, files, memory regions, and failed-write marks return to what they carried, and cells the writes created leave their sheet again — and the call stack, the open loops, and the selected cell are what they were.

Expand source code Browse git
def rollback(self, snapshot: XlmSnapshot) -> None:
    """
    Return to a snapshot: every change the journal holds past its position is undone —
    cells, names, aliases, files, memory regions, and failed-write marks return to what they
    carried, and cells the writes created leave their sheet again — and the call stack, the
    open loops, and the selected cell are what they were.
    """
    while len(self._journal) > snapshot.journal:
        self._journal.pop().undo(self)
    self.call_stack = list(snapshot.call_stack)
    self.while_stack = [loop.copy() for loop in snapshot.loops]
    self.active_cell = snapshot.active_cell
def write_value(self, cell, value)

Leave the value a step computed — or a macro call returned — in the cell it belongs to, undoable by a branch that rolls back: a later read of the cell answers that value instead of evaluating the formula of the cell again.

Expand source code Browse git
def write_value(self, cell: XlmCell, value: XlmValue) -> None:
    """
    Leave the value a step computed — or a macro call returned — in the cell it belongs to,
    undoable by a branch that rolls back: a later read of the cell answers that value
    instead of evaluating the formula of the cell again.
    """
    self._journal.append(_Write(
        self._index.sheet_of.get(id(cell), ''),
        cell,
        cell.value,
        cell.formula,
        self._index.held.get(id(cell)),
        False,
    ))
    held = _held(value)
    set_value(cell, 'value', held.value)
    self._index.hold(cell, held)
def write_cell(self, reference, value, cursor, value_only=False)

Write a value into the macrosheet cell an address names, as the mutation commands do, undoable by a branch that rolls back; a cell that does not exist is created. A value-only write changes the value the cell holds and leaves its formula in place. Any other write enters the value the way a typed entry does: a text that starts with = installs a formula, which a later read of the cell evaluates, and anything else replaces the formula of the cell by a constant. An address off every macrosheet is not written.

Expand source code Browse git
def write_cell(
    self,
    reference: XlmReference,
    value: XlmValue,
    cursor: XlmCursor,
    value_only: bool = False,
) -> None:
    """
    Write a value into the macrosheet cell an address names, as the mutation commands do,
    undoable by a branch that rolls back; a cell that does not exist is created. A
    value-only write changes the value the cell holds and leaves its formula in place. Any
    other write enters the value the way a typed entry does: a text that starts with `=`
    installs a formula, which a later read of the cell evaluates, and anything else replaces
    the formula of the cell by a constant. An address off every macrosheet is not written.
    """
    sheet = reference.sheet or cursor.sheet
    macrosheet = self.view.macrosheet(sheet)
    if macrosheet is None:
        return
    key = macrosheet.name.lower()
    cell = self._index.find(key, reference.row, reference.col)
    created = cell is None
    if cell is None:
        cell = XlmCell(row=reference.row, col=reference.col)
        self._index.insert(key, cell)
        if macrosheet.body:
            edit = BodyEdit(macrosheet)
            edit.splice(macrosheet.body[-1], [macrosheet.body[-1], cell])
            edit.apply()
        else:
            set_body(macrosheet, [cell])
    self._journal.append(_Write(
        key,
        cell,
        cell.value,
        cell.formula,
        self._index.held.get(id(cell)),
        created,
    ))
    held: XlmValue | None = _held(value)
    formula = cell.formula
    if not value_only:
        formula = None
        if isinstance(value.value, str) and value.value.startswith('='):
            formula = parse_formula(value.value)
            held = None
    self._index.update_formula_rows(key, cell, cell.formula is not None, formula is not None)
    set_value(cell, 'value', value.value if held is None else held.value)
    set_child(cell, 'formula', formula)
    self._index.hold(cell, held)
def define_name(self, entry)

Define the name an entry carries, as the name commands do, undoable by a branch that rolls back.

Expand source code Browse git
def define_name(self, entry: XlmNameEntry) -> None:
    """
    Define the name an entry carries, as the name commands do, undoable by a branch that
    rolls back.
    """
    self._journal.append(_NameDefine(self.view.names.entries()))
    self.view.names.define(entry)
def assign_name(self, name, formula, cursor)

Define a name as SET.NAME and the FOR.CELL loop do, undoable by a branch that rolls back: the definition replaces the entry a read on the sheet of the cursor resolves the name to, so that the sheet reads back what it assigned, and a name the sheet resolves to no entry is defined for the whole workbook. A name assigned no formula reads as one the workbook does not define.

Expand source code Browse git
def assign_name(self, name: str, formula, cursor: XlmCursor) -> None:
    """
    Define a name as `SET.NAME` and the `FOR.CELL` loop do, undoable by a branch that rolls
    back: the definition replaces the entry a read on the sheet of the cursor resolves the
    name to, so that the sheet reads back what it assigned, and a name the sheet resolves to
    no entry is defined for the whole workbook. A name assigned no formula reads as one the
    workbook does not define.
    """
    entry = self.view.names.resolve(name, self.view.sheet_index(cursor.sheet))
    sheet = None if entry is None else entry.sheet
    self.define_name(XlmNameEntry(name=name, sheet=sheet, formula=formula))
def register_alias(self, name, target)

Register the target a command name dispatches to, as REGISTER does, undoable by a branch that rolls back.

Expand source code Browse git
def register_alias(self, name: str, target: str) -> None:
    """
    Register the target a command name dispatches to, as `REGISTER` does, undoable by a
    branch that rolls back.
    """
    self._journal.append(_AliasRegistration(name, self.aliases.get(name)))
    self.aliases[name] = target
def open_file(self, name, access='1')

Open the name as a file, as FOPEN does, undoable by a branch that rolls back; a name that already answers a file changes nothing.

Expand source code Browse git
def open_file(self, name: str, access: str = '1') -> None:
    """
    Open the name as a file, as `FOPEN` does, undoable by a branch that rolls back; a name
    that already answers a file changes nothing.
    """
    if self.files.opened(name):
        return
    self._journal.append(_FileOpen(name))
    self.files.open(name, access)
def write_file(self, name, text)

Append to a file the program opened, as FWRITE does, undoable by a branch that rolls back, reporting whether the file took the write.

Expand source code Browse git
def write_file(self, name: str, text: str) -> bool:
    """
    Append to a file the program opened, as `FWRITE` does, undoable by a branch that rolls
    back, reporting whether the file took the write.
    """
    length = self.files.size(name)
    if length is None:
        return False
    self._journal.append(_FileWrite(name, length))
    self.files.write(name, text)
    return True
def allocate_memory(self, base, size)

Reserve a region of memory, as Kernel32.VirtualAlloc does, undoable by a branch that rolls back.

Expand source code Browse git
def allocate_memory(self, base: int, size: int) -> int:
    """
    Reserve a region of memory, as `Kernel32.VirtualAlloc` does, undoable by a branch that
    rolls back.
    """
    self._journal.append(_RegionAlloc())
    return self.memory.allocate(base, size)
def write_memory(self, base, data, size)

Write bytes into a region of memory, as the Kernel32 write commands do, undoable by a branch that rolls back, reporting whether a region took the whole write.

Expand source code Browse git
def write_memory(self, base: int, data: bytes, size: int) -> bool:
    """
    Write bytes into a region of memory, as the Kernel32 write commands do, undoable by a
    branch that rolls back, reporting whether a region took the whole write.
    """
    before = self.memory.peek(base, size)
    if before is None:
        return False
    self.memory.write(base, data, size)
    self._journal.append(_MemoryWrite(base, before))
    return True
def mark_failed(self, reference)

Mark an address as the destination of a write that never finished, undoable by a branch that rolls back.

Expand source code Browse git
def mark_failed(self, reference: XlmReference) -> None:
    """
    Mark an address as the destination of a write that never finished, undoable by a
    branch that rolls back.
    """
    if reference in self.failed_writes:
        return
    self._journal.append(_FailedMark(reference, True))
    self.failed_writes.add(reference)
def unmark_failed(self, reference)

Drop the failed-write mark of an address, undoable by a branch that rolls back.

Expand source code Browse git
def unmark_failed(self, reference: XlmReference) -> None:
    """
    Drop the failed-write mark of an address, undoable by a branch that rolls back.
    """
    if reference not in self.failed_writes:
        return
    self._journal.append(_FailedMark(reference, False))
    self.failed_writes.discard(reference)
def check_deadline(self)

Raise the exhaustion of the run's deadline, for the walks a command takes inside one step, so that a walk over a rectangle the workbook barely fills still answers to the deadline of the run.

Expand source code Browse git
def check_deadline(self) -> None:
    """
    Raise the exhaustion of the run's deadline, for the walks a command takes inside one
    step, so that a walk over a rectangle the workbook barely fills still answers to the
    deadline of the run.
    """
    if self._expired():
        raise _Exhausted('the run exceeded its timeout')