use crate::binding::{
BoundPort, ManifestBindings, PortBinding, RangeBinding, RecordBinding, RecordFieldBinding,
ScalarBinding, TableBinding,
};
use crate::context::WorkbookContext;
use crate::error::SheetPortError;
use crate::layout::{
EXCEL_MAX_COLUMNS, EXCEL_MAX_ROWS, col_to_index, resolve_range_layout,
resolve_range_layout_with_cancel, resolve_table_layout, resolve_table_layout_with_cancel,
};
use crate::validation::{ValidationScope, validate_port_value};
use crate::value::{InputSnapshot, InputUpdate, OutputSnapshot, PortValue, TableRow, TableValue};
use crate::{BatchExecutor, BatchOptions};
use formualizer_common::{LiteralValue, RangeAddress};
use formualizer_eval::engine::{
EvaluationBudgets, EvaluationTarget, OpaquePreparePolicy, RecalcPlan, TableSelection,
TargetEvalOptions,
};
use formualizer_eval::traits::VolatileLevel;
use formualizer_workbook::Workbook;
use sheetport_spec::{Direction, Manifest, TableArea};
use std::collections::BTreeMap;
use std::time::Instant;
struct GridWrite<'a> {
port_id: &'a str,
sheet: &'a str,
start_row: u32,
start_col: u32,
height: u32,
width: u32,
grid: Vec<Vec<LiteralValue>>,
}
struct PreparedWrite {
sheet: String,
row: u32,
col: u32,
value: LiteralValue,
}
impl PreparedWrite {
fn new(sheet: String, row: u32, col: u32, value: LiteralValue) -> Self {
Self {
sheet,
row,
col,
value,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedEvaluationRequest {
pub(crate) targets: Vec<EvaluationTarget>,
reads: Vec<PortBinding>,
}
pub struct SheetPort<'a> {
workbook: &'a mut Workbook,
bindings: ManifestBindings,
active_selector_cancel: Option<formualizer_eval::engine::CancelToken>,
}
#[derive(Debug, Clone)]
pub struct EvalOptions {
pub freeze_volatile: bool,
pub rng_seed: Option<u64>,
pub mode: EvalMode,
pub deterministic_mode: Option<formualizer_eval::engine::DeterministicMode>,
pub cancel: Option<formualizer_eval::engine::CancelToken>,
pub deadline: Option<Instant>,
pub budgets: Option<EvaluationBudgets>,
pub opaque_policy: OpaquePreparePolicy,
}
impl Default for EvalOptions {
fn default() -> Self {
Self {
freeze_volatile: false,
rng_seed: None,
mode: EvalMode::Full,
deterministic_mode: None,
cancel: None,
deadline: None,
budgets: None,
opaque_policy: OpaquePreparePolicy::Widen,
}
}
}
#[derive(Debug, Clone)]
pub enum EvalMode {
Full,
}
impl<'a> SheetPort<'a> {
pub fn new(workbook: &'a mut Workbook, manifest: Manifest) -> Result<Self, SheetPortError> {
let bindings = ManifestBindings::new(manifest)?;
let ctx = WorkbookContext::new(&*workbook);
ctx.validate(&bindings)?;
Ok(Self {
workbook,
bindings,
active_selector_cancel: None,
})
}
pub fn from_bindings(
workbook: &'a mut Workbook,
bindings: ManifestBindings,
) -> Result<Self, SheetPortError> {
let ctx = WorkbookContext::new(&*workbook);
ctx.validate(&bindings)?;
Ok(Self {
workbook,
bindings,
active_selector_cancel: None,
})
}
pub fn workbook(&self) -> &Workbook {
&*self.workbook
}
pub fn workbook_mut(&mut self) -> &mut Workbook {
&mut *self.workbook
}
pub fn manifest(&self) -> &Manifest {
self.bindings.manifest()
}
pub fn bindings(&self) -> &[PortBinding] {
self.bindings.bindings()
}
pub fn into_parts(self) -> (&'a mut Workbook, ManifestBindings) {
(self.workbook, self.bindings)
}
pub fn read_inputs(&mut self) -> Result<InputSnapshot, SheetPortError> {
let bindings: Vec<PortBinding> = self
.bindings
.bindings()
.iter()
.filter(|binding| binding.direction == Direction::In)
.cloned()
.collect();
let mut map = BTreeMap::new();
for binding in bindings.iter() {
let value = self.read_port_value(binding)?;
map.insert(binding.id.clone(), value);
}
Ok(InputSnapshot::new(map))
}
fn read_inputs_raw(&mut self) -> Result<InputSnapshot, SheetPortError> {
let bindings: Vec<PortBinding> = self
.bindings
.bindings()
.iter()
.filter(|binding| binding.direction == Direction::In)
.cloned()
.collect();
let mut map = BTreeMap::new();
for binding in bindings.iter() {
let value = self.read_port_value_raw(binding)?;
map.insert(binding.id.clone(), value);
}
Ok(InputSnapshot::new(map))
}
pub fn read_outputs(&mut self) -> Result<OutputSnapshot, SheetPortError> {
let bindings: Vec<PortBinding> = self
.bindings
.bindings()
.iter()
.filter(|binding| binding.direction == Direction::Out)
.cloned()
.collect();
let mut map = BTreeMap::new();
for binding in bindings.iter() {
let value = self.read_port_value(binding)?;
map.insert(binding.id.clone(), value);
}
Ok(OutputSnapshot::new(map))
}
pub fn write_inputs(&mut self, update: InputUpdate) -> Result<(), SheetPortError> {
self.write_inputs_inner(update, true)
}
pub(crate) fn write_inputs_raw(&mut self, update: InputUpdate) -> Result<(), SheetPortError> {
self.write_inputs_inner(update, false)
}
fn write_inputs_inner(
&mut self,
update: InputUpdate,
validate: bool,
) -> Result<(), SheetPortError> {
let mut writes = Vec::new();
for (port_id, value) in update.into_inner() {
let binding =
self.bindings
.get(&port_id)
.ok_or_else(|| SheetPortError::InvariantViolation {
port: port_id.clone(),
message: "unknown port".to_string(),
})?;
if binding.direction != Direction::In {
return Err(SheetPortError::InvariantViolation {
port: port_id,
message: "cannot write to output port".to_string(),
});
}
if validate {
let scope = match &binding.kind {
BoundPort::Record(_) => ValidationScope::Partial,
_ => ValidationScope::Full,
};
if let Err(violations) = validate_port_value(binding, &value, scope) {
return Err(SheetPortError::ConstraintViolation { violations });
}
}
let binding_clone = binding.clone();
self.write_port_value(&binding_clone, value, &mut writes)?;
}
self.apply_writes(writes)
}
fn apply_writes(&mut self, writes: Vec<PreparedWrite>) -> Result<(), SheetPortError> {
self.workbook
.action("sheetport.write_inputs", move |action| {
for write in writes {
action.set_value(&write.sheet, write.row, write.col, write.value)?;
}
Ok(())
})
.map_err(SheetPortError::from)
}
fn request_checkpoint(
options: &EvalOptions,
message: &'static str,
) -> Result<(), SheetPortError> {
if options
.cancel
.as_ref()
.is_some_and(|cancel| cancel.is_cancelled())
{
return Err(SheetPortError::Engine {
source: formualizer_common::ExcelError::new(
formualizer_common::ExcelErrorKind::Cancelled,
)
.with_message(message),
});
}
if options
.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
{
return Err(SheetPortError::Engine {
source: formualizer_common::ExcelError::new(
formualizer_common::ExcelErrorKind::NImpl,
)
.with_message(message)
.with_extra(formualizer_common::ExcelErrorExtra::Resource {
detail: Box::new(formualizer_common::ResourceExhaustionDetail {
reason: formualizer_common::ResourceExhaustionReason::Deadline,
limit: 0,
observed: 1,
request_id: None,
}),
}),
});
}
Ok(())
}
pub fn evaluate_once(
&mut self,
options: EvalOptions,
) -> Result<OutputSnapshot, SheetPortError> {
self.validate_eval_options(&options)?;
let restore = self.apply_eval_options(&options)?;
let result = (|| {
Self::request_checkpoint(&options, "evaluation cancelled before selector resolution")?;
let request = self.build_evaluation_request()?;
if !request.targets.is_empty() {
let prepare = TargetEvalOptions {
request_id: None,
cancel: options.cancel.clone(),
deadline: options.deadline,
budgets: options.budgets.as_ref(),
opaque_policy: options.opaque_policy,
};
self.workbook
.evaluate_targets_with_options(&request.targets, prepare)?;
}
Self::request_checkpoint(&options, "evaluation cancelled before output resolution")?;
self.read_evaluation_request(&request, &options)
})();
self.restore_eval_options(restore);
result
}
pub(crate) fn build_evaluation_request(
&mut self,
) -> Result<ResolvedEvaluationRequest, SheetPortError> {
let reads = self
.bindings
.bindings()
.iter()
.filter(|binding| binding.direction == Direction::Out)
.cloned()
.collect::<Vec<_>>();
let mut targets = Vec::new();
for binding in &reads {
self.collect_binding_evaluation_targets(binding, &mut targets)?;
}
Ok(ResolvedEvaluationRequest { targets, reads })
}
fn read_evaluation_request(
&mut self,
request: &ResolvedEvaluationRequest,
options: &EvalOptions,
) -> Result<OutputSnapshot, SheetPortError> {
self.active_selector_cancel = options.cancel.clone();
let result = (|| {
let mut map = BTreeMap::new();
for binding in &request.reads {
Self::request_checkpoint(options, "evaluation cancelled during output resolution")?;
map.insert(binding.id.clone(), self.read_port_value(binding)?);
}
Ok(OutputSnapshot::new(map))
})();
self.active_selector_cancel = None;
result
}
fn push_range_target(
targets: &mut Vec<EvaluationTarget>,
port: &str,
sheet: String,
start_row: u32,
start_col: u32,
end_row: u32,
end_col: u32,
) -> Result<(), SheetPortError> {
let range = RangeAddress::new(sheet, start_row, start_col, end_row, end_col).map_err(
|message| SheetPortError::InvariantViolation {
port: port.to_string(),
message: message.to_string(),
},
)?;
targets.push(EvaluationTarget::Range(range));
Ok(())
}
fn layout_envelope_end(&self, sheet: &str, scan_start: u32) -> u32 {
crate::layout::used_row_bound(&*self.workbook, sheet)
.min(EXCEL_MAX_ROWS)
.max(scan_start.saturating_sub(1))
}
fn collect_binding_evaluation_targets(
&mut self,
binding: &PortBinding,
targets: &mut Vec<EvaluationTarget>,
) -> Result<(), SheetPortError> {
match &binding.kind {
BoundPort::Scalar(scalar) => match &scalar.location {
crate::location::ScalarLocation::Cell(addr) => {
targets.push(EvaluationTarget::Cell {
sheet: addr.sheet.clone(),
row: addr.start_row,
col: addr.start_col,
})
}
crate::location::ScalarLocation::Name(name) => {
targets.push(EvaluationTarget::Name {
name: name.clone(),
scope_sheet: None,
})
}
crate::location::ScalarLocation::StructRef(selector) => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"free-form structured reference `{selector}` is unsupported"
),
});
}
},
BoundPort::Record(record) => {
for field in record.fields.values() {
match &field.location {
crate::location::FieldLocation::Cell(addr) => {
targets.push(EvaluationTarget::Cell {
sheet: addr.sheet.clone(),
row: addr.start_row,
col: addr.start_col,
})
}
crate::location::FieldLocation::Name(name) => {
targets.push(EvaluationTarget::Name {
name: name.clone(),
scope_sheet: None,
})
}
crate::location::FieldLocation::StructRef(selector) => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"free-form structured reference `{selector}` is unsupported"
),
});
}
}
}
}
BoundPort::Range(range) => match &range.location {
crate::location::AreaLocation::Range(addr) => {
targets.push(EvaluationTarget::Range(addr.clone()))
}
crate::location::AreaLocation::Name(name) => targets.push(EvaluationTarget::Name {
name: name.clone(),
scope_sheet: None,
}),
crate::location::AreaLocation::Layout(layout) => {
let start_col = col_to_index(&binding.id, &layout.anchor_col)?;
Self::push_range_target(
targets,
&binding.id,
layout.sheet.clone(),
layout.header_row,
start_col,
self.layout_envelope_end(
&layout.sheet,
layout.header_row.saturating_add(1),
),
EXCEL_MAX_COLUMNS,
)?;
}
crate::location::AreaLocation::StructRef(selector) => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"free-form structured reference `{selector}` is unsupported"
),
});
}
},
BoundPort::Table(table) => match &table.location {
crate::location::TableLocation::Layout(layout) => {
let anchor = col_to_index(&binding.id, &layout.anchor_col)?;
let mut columns = Vec::with_capacity(table.columns.len().max(1));
for (index, column) in table.columns.iter().enumerate() {
columns.push(match &column.column_hint {
Some(hint) => col_to_index(&binding.id, hint)?,
None => anchor.saturating_add(index as u32),
});
}
if columns.is_empty() {
columns.push(anchor);
}
let start_col = *columns.iter().min().unwrap_or(&anchor);
let end_col = *columns.iter().max().unwrap_or(&anchor);
let data_start = layout.header_row.saturating_add(1);
Self::push_range_target(
targets,
&binding.id,
layout.sheet.clone(),
layout.header_row,
start_col,
self.layout_envelope_end(&layout.sheet, data_start),
end_col,
)?;
}
crate::location::TableLocation::Table(selector) => {
let (metadata, _) =
self.validate_native_table_columns(binding, table, &selector.name)?;
let selection = match selector.area.unwrap_or(TableArea::Body) {
TableArea::Header if metadata.header_row => TableSelection::Headers,
TableArea::Header => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"native table `{}` has no header row",
selector.name
),
});
}
TableArea::Body => TableSelection::Data,
TableArea::Totals if metadata.totals_row => TableSelection::Totals,
TableArea::Totals => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"native table `{}` has no totals row",
selector.name
),
});
}
};
targets.push(EvaluationTarget::Table {
name: selector.name.clone(),
selection,
});
}
},
}
Ok(())
}
pub fn evaluate_with_plan(
&mut self,
plan: &RecalcPlan,
options: EvalOptions,
) -> Result<OutputSnapshot, SheetPortError> {
self.validate_eval_options(&options)?;
let restore = self.apply_eval_options(&options)?;
let result = (|| {
self.workbook.evaluate_with_plan_controls(
plan,
options.cancel.clone(),
options.deadline,
)?;
Self::request_checkpoint(&options, "evaluation cancelled before output resolution")?;
self.active_selector_cancel = options.cancel.clone();
let output = self.read_outputs();
self.active_selector_cancel = None;
output
})();
self.restore_eval_options(restore);
result
}
pub fn batch(
&'a mut self,
options: BatchOptions<'a>,
) -> Result<BatchExecutor<'a>, SheetPortError> {
self.read_inputs()?;
let baseline_update = self.read_inputs_raw()?.to_update();
self.validate_eval_options(&options.eval)?;
Self::request_checkpoint(
&options.eval,
"batch cancelled before output selector resolution",
)?;
let request = self.build_evaluation_request()?;
let restore = self.apply_eval_options(&options.eval)?;
let prepare = TargetEvalOptions {
request_id: None,
cancel: options.eval.cancel.clone(),
deadline: options.eval.deadline,
budgets: options.eval.budgets.as_ref(),
opaque_policy: options.eval.opaque_policy,
};
let plan_result = self
.workbook
.build_recalc_plan_for_targets_with_options(&request.targets, prepare);
self.restore_eval_options(restore);
let plan = plan_result?;
Ok(BatchExecutor::new(
self,
baseline_update,
options,
plan,
request,
))
}
pub(crate) fn rebuild_target_plan(
&mut self,
request: &ResolvedEvaluationRequest,
options: &EvalOptions,
) -> Result<RecalcPlan, SheetPortError> {
self.validate_eval_options(options)?;
let restore = self.apply_eval_options(options)?;
let prepare = TargetEvalOptions {
request_id: None,
cancel: options.cancel.clone(),
deadline: options.deadline,
budgets: options.budgets.as_ref(),
opaque_policy: options.opaque_policy,
};
let result = self
.workbook
.build_recalc_plan_for_targets_with_options(&request.targets, prepare)
.map_err(SheetPortError::from);
self.restore_eval_options(restore);
result
}
fn read_port_value(&mut self, binding: &PortBinding) -> Result<PortValue, SheetPortError> {
let mut value = self.read_port_value_raw(binding)?;
value = apply_defaults(binding, value);
if let Err(violations) = validate_port_value(binding, &value, ValidationScope::Full) {
return Err(SheetPortError::ConstraintViolation { violations });
}
Ok(value)
}
fn read_port_value_raw(&mut self, binding: &PortBinding) -> Result<PortValue, SheetPortError> {
match &binding.kind {
BoundPort::Scalar(scalar) => self.read_scalar(binding, scalar),
BoundPort::Record(record) => self.read_record(binding, record),
BoundPort::Range(range) => self.read_range(binding, range),
BoundPort::Table(table) => self.read_table(binding, table),
}
}
fn read_scalar(
&self,
binding: &PortBinding,
scalar: &ScalarBinding,
) -> Result<PortValue, SheetPortError> {
match &scalar.location {
crate::location::ScalarLocation::Cell(addr) => {
let value = self
.workbook
.get_value(&addr.sheet, addr.start_row, addr.start_col)
.unwrap_or(LiteralValue::Empty);
Ok(PortValue::Scalar(value))
}
crate::location::ScalarLocation::Name(name) => {
if let Some(value) = self.workbook.resolved_name_value(name, None) {
let scalar = match value {
LiteralValue::Array(rows)
if rows.len() == 1
&& rows.first().is_some_and(|row| row.len() == 1) =>
{
rows.into_iter()
.next()
.and_then(|mut row| row.pop())
.unwrap_or(LiteralValue::Empty)
}
LiteralValue::Array(_) => {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!("name `{name}` is not scalar"),
});
}
value => value,
};
return Ok(PortValue::Scalar(scalar));
}
let addr = self.named_range_address(&binding.id, name)?;
if addr.height() != 1 || addr.width() != 1 {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!("named range `{name}` must resolve to one cell"),
});
}
Ok(PortValue::Scalar(
self.workbook
.get_value(&addr.sheet, addr.start_row, addr.start_col)
.unwrap_or(LiteralValue::Empty),
))
}
_ => Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: "scalar selectors beyond cells or named ranges are not supported yet"
.to_string(),
}),
}
}
fn read_record(
&self,
binding: &PortBinding,
record: &RecordBinding,
) -> Result<PortValue, SheetPortError> {
let mut map = BTreeMap::new();
for (field_name, field_binding) in &record.fields {
let value = self.read_field_value(binding.id.as_str(), field_binding)?;
map.insert(field_name.clone(), value);
}
Ok(PortValue::Record(map))
}
fn read_field_value(
&self,
port_id: &str,
field: &RecordFieldBinding,
) -> Result<LiteralValue, SheetPortError> {
match &field.location {
crate::location::FieldLocation::Cell(addr) => Ok(self
.workbook
.get_value(&addr.sheet, addr.start_row, addr.start_col)
.unwrap_or(LiteralValue::Empty)),
crate::location::FieldLocation::Name(name) => {
if let Some(value) = self.workbook.resolved_name_value(name, None) {
return match value {
LiteralValue::Array(rows)
if rows.len() == 1
&& rows.first().is_some_and(|row| row.len() == 1) =>
{
Ok(rows
.into_iter()
.next()
.and_then(|mut row| row.pop())
.unwrap_or(LiteralValue::Empty))
}
LiteralValue::Array(_) => Err(SheetPortError::InvariantViolation {
port: port_id.to_string(),
message: format!("name `{name}` is not scalar"),
}),
value => Ok(value),
};
}
let addr = self.named_range_address(port_id, name)?;
if addr.height() != 1 || addr.width() != 1 {
return Err(SheetPortError::InvariantViolation {
port: port_id.to_string(),
message: format!("named range `{name}` must resolve to one cell"),
});
}
Ok(self
.workbook
.get_value(&addr.sheet, addr.start_row, addr.start_col)
.unwrap_or(LiteralValue::Empty))
}
crate::location::FieldLocation::StructRef(struct_ref) => {
Err(SheetPortError::UnsupportedSelector {
port: port_id.to_string(),
reason: format!("structured reference `{struct_ref}` is not yet supported"),
})
}
}
}
fn read_range(
&mut self,
binding: &PortBinding,
range: &RangeBinding,
) -> Result<PortValue, SheetPortError> {
let grid = match &range.location {
crate::location::AreaLocation::Range(addr) => self.workbook.read_range(addr),
crate::location::AreaLocation::Name(name) => {
if let Some(value) = self.workbook.resolved_name_value(name, None) {
match value {
LiteralValue::Array(rows) => rows,
value => vec![vec![value]],
}
} else {
let addr = self.named_range_address(&binding.id, name)?;
self.workbook.read_range(&addr)
}
}
crate::location::AreaLocation::Layout(layout) => {
let bounds = resolve_range_layout_with_cancel(
&binding.id,
self.workbook,
layout,
self.active_selector_cancel
.as_ref()
.map(|c| c.as_flag().as_ref()),
)?;
let start_row = bounds.start_row;
let end_row = bounds.end_row.max(bounds.start_row);
let start_col = bounds.start_col;
let end_col = bounds.end_col.max(bounds.start_col);
let addr = RangeAddress::new(bounds.sheet, start_row, start_col, end_row, end_col)
.map_err(|msg| SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: msg.to_string(),
})?;
self.workbook.read_range(&addr)
}
other => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("unsupported area selector `{other:?}` for range port"),
});
}
};
Ok(PortValue::Range(grid))
}
fn validate_native_table_columns(
&self,
binding: &PortBinding,
table: &TableBinding,
table_name: &str,
) -> Result<(formualizer_eval::engine::TableMetadata, Vec<usize>), SheetPortError> {
let metadata = self.workbook.table_metadata(table_name).ok_or_else(|| {
SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("native table `{table_name}` was not found"),
}
})?;
let mut indices = Vec::with_capacity(table.columns.len());
for requested in &table.columns {
let matches = metadata
.headers
.iter()
.enumerate()
.filter(|(_, header)| header.eq_ignore_ascii_case(&requested.name))
.map(|(index, _)| index)
.collect::<Vec<_>>();
if matches.len() != 1 {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"native table column `{}` is missing or ambiguous",
requested.name
),
});
}
indices.push(matches[0]);
}
if indices.windows(2).any(|pair| pair[1] != pair[0] + 1) {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: "requested native table columns must be contiguous and ordered".to_string(),
});
}
Ok((metadata, indices))
}
fn read_table(
&mut self,
binding: &PortBinding,
table: &TableBinding,
) -> Result<PortValue, SheetPortError> {
match &table.location {
crate::location::TableLocation::Layout(layout) => {
let column_hints: Vec<Option<String>> = table
.columns
.iter()
.map(|c| c.column_hint.clone())
.collect();
let bounds = resolve_table_layout_with_cancel(
&binding.id,
self.workbook,
layout,
&column_hints,
self.active_selector_cancel
.as_ref()
.map(|c| c.as_flag().as_ref()),
)?;
let mut rows = Vec::new();
if bounds.data_end_row >= bounds.data_start_row {
for row_idx in bounds.data_start_row..=bounds.data_end_row {
let mut values = BTreeMap::new();
for (col_binding, &col_index) in
table.columns.iter().zip(bounds.column_indices.iter())
{
let value = self
.workbook
.get_value(&bounds.sheet, row_idx, col_index)
.unwrap_or(LiteralValue::Empty);
values.insert(col_binding.name.clone(), value);
}
rows.push(TableRow::new(values));
}
}
Ok(PortValue::Table(TableValue::new(rows)))
}
crate::location::TableLocation::Table(selector) => {
let (metadata, indices) =
self.validate_native_table_columns(binding, table, &selector.name)?;
let area = selector.area.unwrap_or(TableArea::Body);
let (start_row, end_row) = match area {
TableArea::Header if metadata.header_row => {
(metadata.start_row, metadata.start_row)
}
TableArea::Header => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("native table `{}` has no header row", selector.name),
});
}
TableArea::Totals if metadata.totals_row => {
(metadata.end_row, metadata.end_row)
}
TableArea::Totals => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("native table `{}` has no totals row", selector.name),
});
}
TableArea::Body => (
metadata.start_row + u32::from(metadata.header_row),
metadata
.end_row
.saturating_sub(u32::from(metadata.totals_row)),
),
};
let mut rows = Vec::new();
if start_row <= end_row {
for row in start_row..=end_row {
let mut values = BTreeMap::new();
for (column, index) in table.columns.iter().zip(indices.iter().copied()) {
values.insert(
column.name.clone(),
self.workbook
.get_value(
&metadata.sheet,
row,
metadata.start_col + index as u32,
)
.unwrap_or(LiteralValue::Empty),
);
}
rows.push(TableRow::new(values));
}
}
Ok(PortValue::Table(TableValue::new(rows)))
}
}
}
fn write_port_value(
&mut self,
binding: &PortBinding,
value: PortValue,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
match (binding.kind.clone(), value) {
(BoundPort::Scalar(scalar), PortValue::Scalar(val)) => {
self.write_scalar(binding, &scalar, val, writes)
}
(BoundPort::Record(record), PortValue::Record(map)) => {
self.write_record(binding, &record, map, writes)
}
(BoundPort::Range(range), PortValue::Range(grid)) => {
self.write_range(binding, &range, grid, writes)
}
(BoundPort::Table(table), PortValue::Table(rows)) => {
self.write_table(binding, &table, rows, writes)
}
(_, unexpected) => Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!(
"port value did not match expected shape: got {:?}",
unexpected
),
}),
}
}
fn write_scalar(
&mut self,
binding: &PortBinding,
scalar: &ScalarBinding,
value: LiteralValue,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
match &scalar.location {
crate::location::ScalarLocation::Cell(addr) => {
writes.push(PreparedWrite::new(
addr.sheet.clone(),
addr.start_row,
addr.start_col,
value,
));
Ok(())
}
crate::location::ScalarLocation::Name(name) => {
let addr = self.named_range_address(&binding.id, name)?;
if addr.height() != 1 || addr.width() != 1 {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!(
"named range `{name}` must resolve to a single cell for scalar ports"
),
});
}
writes.push(PreparedWrite::new(
addr.sheet,
addr.start_row,
addr.start_col,
value,
));
Ok(())
}
_ => Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: "scalar selectors beyond cells are not supported yet".to_string(),
}),
}
}
fn write_record(
&mut self,
binding: &PortBinding,
record: &RecordBinding,
update: BTreeMap<String, LiteralValue>,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
for (field_name, value) in update {
let field_binding = record.fields.get(&field_name).ok_or_else(|| {
SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!("unknown record field `{field_name}`"),
}
})?;
match &field_binding.location {
crate::location::FieldLocation::Cell(addr) => {
writes.push(PreparedWrite::new(
addr.sheet.clone(),
addr.start_row,
addr.start_col,
value,
));
}
crate::location::FieldLocation::Name(name) => {
let addr = self.named_range_address(&binding.id, name)?;
if addr.height() != 1 || addr.width() != 1 {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!(
"record field `{field_name}` named range `{name}` must resolve to a single cell"
),
});
}
writes.push(PreparedWrite::new(
addr.sheet,
addr.start_row,
addr.start_col,
value,
));
}
_ => {
return Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("record field `{field_name}` uses unsupported selector"),
});
}
}
}
Ok(())
}
fn write_range(
&mut self,
binding: &PortBinding,
range: &RangeBinding,
grid: Vec<Vec<LiteralValue>>,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
match &range.location {
crate::location::AreaLocation::Range(addr) => self.write_grid(
GridWrite {
port_id: binding.id.as_str(),
sheet: &addr.sheet,
start_row: addr.start_row,
start_col: addr.start_col,
height: addr.height(),
width: addr.width(),
grid,
},
writes,
),
crate::location::AreaLocation::Layout(layout) => {
let bounds = resolve_range_layout(&binding.id, self.workbook, layout)?;
let expected_width = bounds.columns.len() as u32;
if grid.first().map(|row| row.len() as u32).unwrap_or(0) != expected_width {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: "range update width does not match layout".to_string(),
});
}
let height = grid.len() as u32;
self.write_grid(
GridWrite {
port_id: binding.id.as_str(),
sheet: &bounds.sheet,
start_row: bounds.start_row,
start_col: bounds.start_col,
height,
width: expected_width,
grid,
},
writes,
)?;
let existing_height = bounds.end_row - bounds.start_row + 1;
if height < existing_height {
for row in (bounds.start_row + height)..=bounds.end_row {
for &col in &bounds.columns {
writes.push(PreparedWrite::new(
bounds.sheet.clone(),
row,
col,
LiteralValue::Empty,
));
}
}
}
match layout.terminate {
sheetport_spec::LayoutTermination::FirstBlankRow
| sheetport_spec::LayoutTermination::UntilMarker => {
let blank_row = bounds.start_row + height;
for &col in &bounds.columns {
writes.push(PreparedWrite::new(
bounds.sheet.clone(),
blank_row,
col,
LiteralValue::Empty,
));
}
}
sheetport_spec::LayoutTermination::SheetEnd => {}
}
Ok(())
}
crate::location::AreaLocation::Name(name) => {
let addr = self.named_range_address(&binding.id, name)?;
let expected_width = addr.width();
if grid.first().map(|row| row.len() as u32).unwrap_or(0) != expected_width {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!(
"range update width does not match named range `{name}` width"
),
});
}
let height = grid.len() as u32;
if height != addr.height() {
return Err(SheetPortError::InvariantViolation {
port: binding.id.clone(),
message: format!(
"range update height does not match named range `{name}` height"
),
});
}
self.write_grid(
GridWrite {
port_id: binding.id.as_str(),
sheet: &addr.sheet,
start_row: addr.start_row,
start_col: addr.start_col,
height,
width: expected_width,
grid,
},
writes,
)
}
other => Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!("unsupported area selector `{other:?}` for range port"),
}),
}
}
fn write_grid(
&mut self,
params: GridWrite<'_>,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
let GridWrite {
port_id,
sheet,
start_row,
start_col,
height,
width,
grid,
} = params;
if grid.len() as u32 != height {
return Err(SheetPortError::InvariantViolation {
port: port_id.to_string(),
message: "range update height mismatch".to_string(),
});
}
for (row_offset, row) in grid.into_iter().enumerate() {
if row.len() as u32 != width {
return Err(SheetPortError::InvariantViolation {
port: port_id.to_string(),
message: "range row width mismatch".to_string(),
});
}
let row_idx = start_row + row_offset as u32;
for (col_offset, value) in row.into_iter().enumerate() {
let col_idx = start_col + col_offset as u32;
writes.push(PreparedWrite::new(
sheet.to_string(),
row_idx,
col_idx,
value,
));
}
}
Ok(())
}
fn named_range_address(
&self,
port_id: &str,
name: &str,
) -> Result<RangeAddress, SheetPortError> {
self.workbook
.named_range_address(name)
.ok_or_else(|| SheetPortError::InvariantViolation {
port: port_id.to_string(),
message: format!("named range `{name}` was not found in the workbook"),
})
}
fn write_table(
&mut self,
binding: &PortBinding,
table: &TableBinding,
value: TableValue,
writes: &mut Vec<PreparedWrite>,
) -> Result<(), SheetPortError> {
match &table.location {
crate::location::TableLocation::Layout(layout) => {
let column_hints: Vec<Option<String>> = table
.columns
.iter()
.map(|c| c.column_hint.clone())
.collect();
let bounds =
resolve_table_layout(&binding.id, self.workbook, layout, &column_hints)?;
let existing_row_count = if bounds.data_end_row >= bounds.data_start_row {
bounds.data_end_row - bounds.data_start_row + 1
} else {
0
};
let rows = value.rows;
let new_row_count = rows.len() as u32;
for (row_offset, row) in rows.into_iter().enumerate() {
let row_idx = bounds.data_start_row + row_offset as u32;
for (col_binding, &col_index) in
table.columns.iter().zip(bounds.column_indices.iter())
{
let cell_value = row
.values
.get(&col_binding.name)
.cloned()
.unwrap_or(LiteralValue::Empty);
writes.push(PreparedWrite::new(
bounds.sheet.clone(),
row_idx,
col_index,
cell_value,
));
}
}
if new_row_count < existing_row_count {
for row in (bounds.data_start_row + new_row_count)..=bounds.data_end_row {
for &col_index in &bounds.column_indices {
writes.push(PreparedWrite::new(
bounds.sheet.clone(),
row,
col_index,
LiteralValue::Empty,
));
}
}
}
match layout.terminate {
sheetport_spec::LayoutTermination::FirstBlankRow
| sheetport_spec::LayoutTermination::UntilMarker => {
let blank_row = bounds.data_start_row + new_row_count;
for &col_index in &bounds.column_indices {
writes.push(PreparedWrite::new(
bounds.sheet.clone(),
blank_row,
col_index,
LiteralValue::Empty,
));
}
}
sheetport_spec::LayoutTermination::SheetEnd => {}
}
Ok(())
}
crate::location::TableLocation::Table(selector) => {
Err(SheetPortError::UnsupportedSelector {
port: binding.id.clone(),
reason: format!(
"native table `{}` selectors are not supported yet",
selector.name
),
})
}
}
}
}
fn apply_defaults(binding: &PortBinding, value: PortValue) -> PortValue {
if let Some(default) = &binding.resolved_default {
normalize_port_value(merge_with_default(value, default))
} else {
normalize_port_value(value)
}
}
fn normalize_literal(lit: LiteralValue) -> LiteralValue {
match lit {
LiteralValue::Int(i) => LiteralValue::Number(i as f64),
other => other,
}
}
fn normalize_port_value(mut value: PortValue) -> PortValue {
match &mut value {
PortValue::Scalar(lit) => {
let old = std::mem::replace(lit, LiteralValue::Empty);
*lit = normalize_literal(old);
}
PortValue::Record(map) => {
for v in map.values_mut() {
let old = std::mem::replace(v, LiteralValue::Empty);
*v = normalize_literal(old);
}
}
PortValue::Range(rows) => {
for row in rows.iter_mut() {
for cell in row.iter_mut() {
let old = std::mem::replace(cell, LiteralValue::Empty);
*cell = normalize_literal(old);
}
}
}
PortValue::Table(table) => {
for row in table.rows.iter_mut() {
for v in row.values.values_mut() {
let old = std::mem::replace(v, LiteralValue::Empty);
*v = normalize_literal(old);
}
}
}
}
value
}
fn merge_with_default(mut current: PortValue, default: &PortValue) -> PortValue {
match (&mut current, default) {
(PortValue::Scalar(current_lit), PortValue::Scalar(default_lit)) => {
if matches!(current_lit, LiteralValue::Empty) {
*current_lit = default_lit.clone();
}
}
(PortValue::Record(current_fields), PortValue::Record(default_fields)) => {
for (field, default_value) in default_fields {
let entry = current_fields
.entry(field.clone())
.or_insert(LiteralValue::Empty);
if matches!(entry, LiteralValue::Empty) {
*entry = default_value.clone();
}
}
}
(PortValue::Range(current_rows), PortValue::Range(default_rows)) => {
let is_empty = current_rows.is_empty()
|| current_rows
.iter()
.all(|row| row.iter().all(|cell| matches!(cell, LiteralValue::Empty)));
if is_empty {
*current_rows = default_rows.clone();
}
}
(PortValue::Table(current_table), PortValue::Table(default_table)) => {
if current_table.is_empty() {
*current_table = default_table.clone();
}
}
_ => {}
}
current
}
struct EvalConfigRestore {
seed: u64,
volatile_level: VolatileLevel,
deterministic_mode: formualizer_eval::engine::DeterministicMode,
budgets: EvaluationBudgets,
seed_overridden: bool,
volatile_overridden: bool,
deterministic_overridden: bool,
budgets_overridden: bool,
}
impl<'a> SheetPort<'a> {
fn validate_eval_options(&self, options: &EvalOptions) -> Result<(), SheetPortError> {
if options
.budgets
.as_ref()
.and_then(|budgets| budgets.optimization.max_threads)
== Some(0)
{
return Err(SheetPortError::InvariantViolation {
port: "<evaluation>".to_string(),
message: "evaluation max_threads must be greater than zero".to_string(),
});
}
Ok(())
}
fn apply_eval_options(
&mut self,
options: &EvalOptions,
) -> Result<EvalConfigRestore, SheetPortError> {
self.validate_eval_options(options)?;
let seed = self.workbook.engine().config.workbook_seed;
let volatile_level = self.workbook.engine().config.volatile_level;
let deterministic_mode = self.workbook.engine().config.deterministic_mode.clone();
let budgets = self.workbook.engine().evaluation_resource_budgets().clone();
let mut seed_overridden = false;
let mut volatile_overridden = false;
let mut deterministic_overridden = false;
let mut budgets_overridden = false;
let deterministic_override = options
.deterministic_mode
.clone()
.filter(|mode| *mode != deterministic_mode);
if let Some(mode) = deterministic_override {
self.workbook
.engine_mut()
.set_deterministic_mode(mode)
.map_err(SheetPortError::from)?;
deterministic_overridden = true;
}
let effective_budgets = options.budgets.clone().unwrap_or_else(|| budgets.clone());
if effective_budgets != budgets {
self.workbook
.engine_mut()
.set_evaluation_resource_budgets(effective_budgets);
budgets_overridden = true;
}
if let Some(desired_seed) = options.rng_seed
&& desired_seed != seed
{
self.workbook.engine_mut().set_workbook_seed(desired_seed);
seed_overridden = true;
}
if options.freeze_volatile && volatile_level != VolatileLevel::OnOpen {
self.workbook
.engine_mut()
.set_volatile_level(VolatileLevel::OnOpen);
volatile_overridden = true;
}
Ok(EvalConfigRestore {
seed,
volatile_level,
deterministic_mode,
budgets,
seed_overridden,
volatile_overridden,
deterministic_overridden,
budgets_overridden,
})
}
fn restore_eval_options(&mut self, restore: EvalConfigRestore) {
if restore.seed_overridden {
self.workbook.engine_mut().set_workbook_seed(restore.seed);
}
if restore.volatile_overridden {
self.workbook
.engine_mut()
.set_volatile_level(restore.volatile_level);
}
if restore.deterministic_overridden {
let _ = self
.workbook
.engine_mut()
.set_deterministic_mode(restore.deterministic_mode);
}
if restore.budgets_overridden {
self.workbook
.engine_mut()
.set_evaluation_resource_budgets(restore.budgets);
}
}
}