Module refinery.lib.scripts.xlm.handlers.mutate
The mutation commands of the macro language: the formulas the program writes into cells, the names it defines, and the selection it moves through the workbook.
Expand source code Browse git
"""
The mutation commands of the macro language: the formulas the program writes into cells, the
names it defines, and the selection it moves through the workbook.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from refinery.lib.excel import synthesize_formula
from refinery.lib.excel.formula.model import (
XlA1Reference,
XlArrayConstant,
XlBinaryExpression,
XlBinaryOperator,
XlBoolean,
XlMissingArgument,
XlNumber,
XlR1C1Reference,
XlString,
)
from refinery.lib.scripts.xlm.evaluate import evaluate_expression
from refinery.lib.scripts.xlm.names import XlmNameEntry
from refinery.lib.scripts.xlm.references import XlmCursor, expand_range
from refinery.lib.scripts.xlm.values import (
XlmOutcome,
XlmReference,
XlmValue,
condition,
error_outcome,
is_number,
unwrap_literal,
)
if TYPE_CHECKING:
from refinery.lib.excel.formula.model import XlFunctionCall
from refinery.lib.scripts.xlm.engine import XlmEngine
def _partial(spelled: str) -> XlmOutcome:
return XlmOutcome(value=XlmValue(value=spelled, partial=True))
def _absolute_reference(reference: XlmReference) -> XlA1Reference:
"""
The reference node that names an address absolutely.
"""
return XlA1Reference(
sheets=(reference.sheet,) if reference.sheet is not None else None,
row=reference.row,
col=reference.col,
relative_row=False,
relative_col=False,
)
def _name_formula(node, value: XlmValue):
"""
The formula a defined name stands for once a command stores a value under it: numbers,
texts, and booleans keep their literal shape, a cell address becomes an absolute reference,
a range spans its corners, and an array constant keeps the node it was spelled as.
"""
if isinstance(node, (XlA1Reference, XlR1C1Reference, XlArrayConstant)):
return node
if isinstance(value.value, XlmReference):
return _absolute_reference(value.value)
if value.reference is not None:
return _absolute_reference(value.reference)
if value.cells is not None and len(value.cells) == 2:
first, second = value.cells
if first.reference is not None and second.reference is not None:
return XlBinaryExpression(
left=_absolute_reference(first.reference),
operator=XlBinaryOperator.RANGE,
right=_absolute_reference(second.reference),
)
if value.cells is not None:
return XlArrayConstant(rows=[tuple(
XlString(value=cell.unwrap())
for cell in value.cells
)])
if isinstance(value.value, bool):
return XlBoolean(value=value.value)
if is_number(value.value):
number = float(value.value)
return XlNumber(value=int(number) if number.is_integer() else number)
return XlString(value=value.unwrap())
def _write_formula(
engine: XlmEngine,
call: XlFunctionCall,
cursor: XlmCursor,
value_only: bool = False,
swapped: bool = False,
) -> XlmOutcome:
"""
Write what a command's source argument evaluates to into the cells its destination argument
names; a command that names no destination writes into the cell the program selected. A
source the program never finishes marks the destination cells as failed instead of writing
them — a later reference to one of them reads its own address. The command spells its own
name, because the trace keeps the distinction the spellings of the write commands carry even
though they share one behavior.
"""
name = call.callee if isinstance(call.callee, str) else 'FORMULA'
if not call.arguments or isinstance(call.arguments[0], XlMissingArgument):
return XlmOutcome(value=XlmValue(value=False, text=F'{name}()'))
destination_node = None
if swapped:
if len(call.arguments) < 2:
return _partial(synthesize_formula(call))
destination_node, source_node = call.arguments[0], call.arguments[1]
else:
source_node = call.arguments[0]
if len(call.arguments) > 1 and not isinstance(call.arguments[1], XlMissingArgument):
destination_node = call.arguments[1]
source = evaluate_expression(engine, source_node, cursor)
if destination_node is None:
if engine.active_cell is None:
return _partial(synthesize_formula(call))
corners = (engine.active_cell, engine.active_cell)
text = F'{name}({source.text})'
else:
corners = engine.range_corners(destination_node, cursor)
if corners is None:
reference = engine.node_reference(destination_node, cursor)
if reference is None:
return _partial(synthesize_formula(call))
corners = (reference, reference)
spelled = synthesize_formula(destination_node)
if swapped:
text = F'{name}({spelled},{source.text})'
else:
text = F'{name}({source.text},{spelled})'
if source.partial:
for reference in expand_range(*corners, guard=engine.check_deadline):
engine.mark_failed(reference)
return XlmOutcome(value=XlmValue(value=0, text=text, partial=True))
for reference in expand_range(*corners, guard=engine.check_deadline):
engine.unmark_failed(reference)
engine.write_cell(reference, source, cursor, value_only=value_only)
return XlmOutcome(value=XlmValue(value=0, text=text))
def _set_value(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
return _write_formula(engine, call, cursor, value_only=True, swapped=True)
def _set_name(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
"""
Define a name as `SET.NAME` does: a reference is stored as it is spelled, any other value
as the literal it computes, and a call that names no value deletes the name.
"""
spelled = synthesize_formula(call)
label = unwrap_literal(synthesize_formula(call.arguments[0])).lower()
if len(call.arguments) < 2 or isinstance(call.arguments[1], XlMissingArgument):
engine.assign_name(label, None, cursor)
return XlmOutcome(value=XlmValue(value=0, text=F'SET.NAME({label})'))
node = call.arguments[1]
if isinstance(node, (XlA1Reference, XlR1C1Reference)):
engine.assign_name(label, node, cursor)
return XlmOutcome(value=XlmValue(
value=0,
text=F'SET.NAME({label},{synthesize_formula(node)})',
))
value = evaluate_expression(engine, node, cursor)
if value.partial:
return _partial(spelled)
engine.assign_name(label, _name_formula(node, value), cursor)
return XlmOutcome(value=XlmValue(
value=0,
text=F'SET.NAME({label},{value.unwrap()})',
))
def _define_name(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
"""
Define a name as `DEFINE.NAME` does: for the whole workbook, or for the sheet of the cell
that runs the command when its seventh argument asks for a local name.
"""
spelled = synthesize_formula(call)
label = evaluate_expression(engine, call.arguments[0], cursor)
if label.partial:
return _partial(spelled)
value = evaluate_expression(engine, call.arguments[1], cursor)
if value.partial:
return _partial(spelled)
sheet = None
if len(call.arguments) > 6 and not isinstance(call.arguments[6], XlMissingArgument):
local = evaluate_expression(engine, call.arguments[6], cursor)
if local.partial:
return _partial(spelled)
truth = condition(local)
if isinstance(truth, XlmValue):
return error_outcome(truth)
if truth:
sheet = engine.view.sheet_index(cursor.sheet)
name = label.unwrap().lower()
engine.define_name(XlmNameEntry(
name=name,
sheet=sheet,
formula=_name_formula(call.arguments[1], value),
))
return XlmOutcome(value=XlmValue(
value=value.value,
text=F'DEFINE.NAME("{name}",{value.value})',
))
def _selection_base(engine: XlmEngine, cursor: XlmCursor) -> XlmCursor:
if engine.active_cell is None:
return cursor
return XlmCursor(
engine.active_cell.sheet or cursor.sheet,
engine.active_cell.row,
engine.active_cell.col,
)
def _select(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
evaluate_expression(engine, call.arguments[0], cursor)
base = _selection_base(engine, cursor)
if len(call.arguments) == 2:
node = call.arguments[1]
else:
node = call.arguments[0]
if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.RANGE:
if not isinstance(node.left, XlBinaryExpression):
return _partial(spelled)
node = node.right
elif not isinstance(node, (XlA1Reference, XlR1C1Reference)):
return _partial(spelled)
reference = engine.node_reference(node, base)
if reference is None:
return _partial(spelled)
engine.active_cell = reference
return XlmOutcome(value=XlmValue(value=0, text=spelled))
def _active_cell(engine: XlmEngine, call: XlFunctionCall, cursor: XlmCursor) -> XlmOutcome:
spelled = synthesize_formula(call)
if engine.active_cell is None:
return _partial(spelled)
return XlmOutcome(value=engine.read_reference(engine.active_cell, cursor))
MUTATION_HANDLERS = {
'ACTIVE.CELL': _active_cell,
'DEFINE.NAME': _define_name,
'FORMULA': _write_formula,
'FORMULA.ARRAY': _write_formula,
'FORMULA.FILL': _write_formula,
'SELECT': _select,
'SET.NAME': _set_name,
'SET.VALUE': _set_value,
}