Skip to main content

formualizer_workbook/
workbook.rs

1use crate::error::IoError;
2use crate::traits::{LoadStrategy, SpreadsheetReader, SpreadsheetWriter};
3use chrono::Timelike;
4use formualizer_common::{
5    LiteralValue, RangeAddress,
6    error::{ExcelError, ExcelErrorKind},
7};
8use formualizer_eval::engine::eval::EvalPlan;
9use formualizer_eval::engine::named_range::{NameScope, NamedDefinition};
10use parking_lot::RwLock;
11use std::collections::{BTreeMap, BTreeSet};
12use std::sync::Arc;
13
14#[cfg(feature = "wasm_plugins")]
15use wasmparser::{Parser, Payload};
16
17#[cfg(all(feature = "wasm_runtime_wasmtime", not(target_arch = "wasm32")))]
18use crate::wasm_runtime_wasmtime::new_wasmtime_runtime;
19
20fn normalize_custom_fn_name(name: &str) -> Result<String, ExcelError> {
21    let trimmed = name.trim();
22    if trimmed.is_empty() {
23        return Err(
24            ExcelError::new(ExcelErrorKind::Name).with_message("Function name cannot be empty")
25        );
26    }
27    Ok(trimmed.to_ascii_uppercase())
28}
29
30pub const WASM_MANIFEST_SCHEMA_V1: &str = "formualizer.udf.module/v1";
31pub const WASM_MANIFEST_SECTION_V1: &str = "formualizer.udf.manifest.v1";
32pub const WASM_ABI_VERSION_V1: u32 = 1;
33pub const WASM_CODEC_VERSION_V1: u32 = 1;
34
35fn normalize_wasm_module_id(module_id: &str) -> Result<String, ExcelError> {
36    let trimmed = module_id.trim();
37    if trimmed.is_empty() {
38        return Err(
39            ExcelError::new(ExcelErrorKind::Value).with_message("WASM module_id cannot be empty")
40        );
41    }
42    Ok(trimmed.to_string())
43}
44
45#[cfg(not(target_arch = "wasm32"))]
46fn read_wasm_file_bytes(path: &std::path::Path) -> Result<Vec<u8>, ExcelError> {
47    std::fs::read(path).map_err(|err| {
48        ExcelError::new(ExcelErrorKind::Value).with_message(format!(
49            "Failed to read WASM module file {}: {err}",
50            path.display()
51        ))
52    })
53}
54
55#[cfg(not(target_arch = "wasm32"))]
56fn collect_wasm_files_in_dir(dir: &std::path::Path) -> Result<Vec<std::path::PathBuf>, ExcelError> {
57    if !dir.is_dir() {
58        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
59            "WASM module directory does not exist or is not a directory: {}",
60            dir.display()
61        )));
62    }
63
64    let mut files = Vec::new();
65    let entries = std::fs::read_dir(dir).map_err(|err| {
66        ExcelError::new(ExcelErrorKind::Value).with_message(format!(
67            "Failed to read WASM module directory {}: {err}",
68            dir.display()
69        ))
70    })?;
71
72    for entry in entries {
73        let entry = entry.map_err(|err| {
74            ExcelError::new(ExcelErrorKind::Value).with_message(format!(
75                "Failed to iterate WASM module directory {}: {err}",
76                dir.display()
77            ))
78        })?;
79
80        let path = entry.path();
81        if !path.is_file() {
82            continue;
83        }
84
85        let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
86            continue;
87        };
88
89        if ext.eq_ignore_ascii_case("wasm") {
90            files.push(path);
91        }
92    }
93
94    files.sort();
95    Ok(files)
96}
97
98fn stable_fn_salt(name: &str) -> u64 {
99    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
100    const FNV_PRIME: u64 = 0x100000001b3;
101    let mut hash = FNV_OFFSET;
102    for b in name.as_bytes() {
103        hash ^= u64::from(*b);
104        hash = hash.wrapping_mul(FNV_PRIME);
105    }
106    hash
107}
108
109fn validate_custom_arity(name: &str, options: &CustomFnOptions) -> Result<(), ExcelError> {
110    if let Some(max_args) = options.max_args
111        && max_args < options.min_args
112    {
113        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
114            "Invalid arity for {name}: max_args ({max_args}) < min_args ({})",
115            options.min_args
116        )));
117    }
118    Ok(())
119}
120
121fn validate_wasm_spec(spec: &WasmFunctionSpec) -> Result<(), ExcelError> {
122    if spec.module_id.trim().is_empty() {
123        return Err(ExcelError::new(ExcelErrorKind::Value)
124            .with_message("WASM function module_id cannot be empty"));
125    }
126    if spec.export_name.trim().is_empty() {
127        return Err(ExcelError::new(ExcelErrorKind::Value)
128            .with_message("WASM function export_name cannot be empty"));
129    }
130    Ok(())
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct CustomFnOptions {
135    pub min_args: usize,
136    pub max_args: Option<usize>,
137    pub volatile: bool,
138    pub thread_safe: bool,
139    pub deterministic: bool,
140    pub allow_override_builtin: bool,
141}
142
143impl Default for CustomFnOptions {
144    fn default() -> Self {
145        Self {
146            min_args: 0,
147            max_args: None,
148            volatile: false,
149            thread_safe: false,
150            deterministic: true,
151            allow_override_builtin: false,
152        }
153    }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct CustomFnInfo {
158    pub name: String,
159    pub options: CustomFnOptions,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct WasmFunctionSpec {
164    pub module_id: String,
165    pub export_name: String,
166    pub codec_version: u32,
167    pub runtime_hint: Option<WasmRuntimeHint>,
168    pub reserved: BTreeMap<String, String>,
169}
170
171impl WasmFunctionSpec {
172    pub fn new(
173        module_id: impl Into<String>,
174        export_name: impl Into<String>,
175        codec_version: u32,
176    ) -> Self {
177        Self {
178            module_id: module_id.into(),
179            export_name: export_name.into(),
180            codec_version,
181            runtime_hint: None,
182            reserved: BTreeMap::new(),
183        }
184    }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Default)]
188pub struct WasmRuntimeHint {
189    pub fuel_limit: Option<u64>,
190    pub memory_limit_bytes: Option<u64>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct WasmModuleInfo {
195    pub module_id: String,
196    pub version: String,
197    pub abi_version: u32,
198    pub codec_version: u32,
199    pub function_count: usize,
200    pub module_size_bytes: usize,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
204pub struct WasmModuleManifest {
205    pub schema: String,
206    pub module: WasmManifestModule,
207    pub functions: Vec<WasmManifestFunction>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
211pub struct WasmManifestModule {
212    pub id: String,
213    pub version: String,
214    pub abi: u32,
215    pub codec: u32,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
219pub struct WasmManifestFunction {
220    pub id: u32,
221    pub name: String,
222    #[serde(default)]
223    pub aliases: Vec<String>,
224    #[serde(rename = "export")]
225    pub export_name: String,
226    pub min_args: usize,
227    #[serde(default)]
228    pub max_args: Option<usize>,
229    #[serde(default)]
230    pub volatile: bool,
231    #[serde(default = "default_true")]
232    pub deterministic: bool,
233    #[serde(default)]
234    pub thread_safe: bool,
235    #[serde(default)]
236    pub params: Vec<WasmManifestParam>,
237    #[serde(default)]
238    pub returns: Option<WasmManifestReturn>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
242pub struct WasmManifestParam {
243    pub name: String,
244    #[serde(default)]
245    pub kinds: Vec<String>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
249pub struct WasmManifestReturn {
250    #[serde(default)]
251    pub kinds: Vec<String>,
252}
253
254fn default_true() -> bool {
255    true
256}
257
258pub trait WasmUdfRuntime: Send + Sync {
259    fn can_bind_functions(&self) -> bool {
260        true
261    }
262
263    fn validate_module(
264        &self,
265        _module_id: &str,
266        _wasm_bytes: &[u8],
267        _manifest: &WasmModuleManifest,
268    ) -> Result<(), ExcelError> {
269        Ok(())
270    }
271
272    fn invoke(
273        &self,
274        module_id: &str,
275        export_name: &str,
276        function_name: &str,
277        codec_version: u32,
278        args: &[LiteralValue],
279        runtime_hint: Option<&WasmRuntimeHint>,
280    ) -> Result<LiteralValue, ExcelError>;
281}
282
283#[cfg(feature = "wasm_plugins")]
284#[derive(Default)]
285struct PendingWasmRuntime;
286
287#[cfg(feature = "wasm_plugins")]
288impl WasmUdfRuntime for PendingWasmRuntime {
289    fn can_bind_functions(&self) -> bool {
290        false
291    }
292
293    fn invoke(
294        &self,
295        module_id: &str,
296        export_name: &str,
297        function_name: &str,
298        codec_version: u32,
299        _args: &[LiteralValue],
300        _runtime_hint: Option<&WasmRuntimeHint>,
301    ) -> Result<LiteralValue, ExcelError> {
302        Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
303            "WASM plugin runtime integration is pending for {function_name} (module_id={module_id}, export_name={export_name}, codec_version={codec_version})"
304        )))
305    }
306}
307
308pub fn validate_wasm_manifest(manifest: &WasmModuleManifest) -> Result<(), ExcelError> {
309    if manifest.schema != WASM_MANIFEST_SCHEMA_V1 {
310        return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
311            "Unsupported WASM manifest schema: {}",
312            manifest.schema
313        )));
314    }
315
316    let module_id = normalize_wasm_module_id(&manifest.module.id)?;
317    if module_id != manifest.module.id {
318        return Err(ExcelError::new(ExcelErrorKind::Value)
319            .with_message("WASM manifest module.id must not have leading/trailing whitespace"));
320    }
321
322    if manifest.module.version.trim().is_empty() {
323        return Err(ExcelError::new(ExcelErrorKind::Value)
324            .with_message("WASM manifest module.version cannot be empty"));
325    }
326
327    if manifest.module.abi != WASM_ABI_VERSION_V1 {
328        return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
329            "Unsupported WASM ABI version {} (expected {})",
330            manifest.module.abi, WASM_ABI_VERSION_V1
331        )));
332    }
333
334    if manifest.module.codec != WASM_CODEC_VERSION_V1 {
335        return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
336            "Unsupported WASM codec version {} (expected {})",
337            manifest.module.codec, WASM_CODEC_VERSION_V1
338        )));
339    }
340
341    if manifest.functions.is_empty() {
342        return Err(ExcelError::new(ExcelErrorKind::Value)
343            .with_message("WASM manifest must define at least one function"));
344    }
345
346    let mut function_ids = BTreeSet::new();
347    let mut export_names = BTreeSet::new();
348    let mut names_and_aliases = BTreeSet::new();
349
350    for function in &manifest.functions {
351        if !function_ids.insert(function.id) {
352            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
353                "Duplicate WASM manifest function id {}",
354                function.id
355            )));
356        }
357
358        if function.export_name.trim().is_empty() {
359            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
360                "WASM function {} has empty export name",
361                function.id
362            )));
363        }
364
365        if !export_names.insert(function.export_name.clone()) {
366            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
367                "Duplicate WASM export name: {}",
368                function.export_name
369            )));
370        }
371
372        let canonical_name = normalize_custom_fn_name(&function.name)?;
373        if !names_and_aliases.insert(canonical_name.clone()) {
374            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
375                "Duplicate WASM function name or alias: {}",
376                function.name
377            )));
378        }
379
380        if let Some(max_args) = function.max_args
381            && max_args < function.min_args
382        {
383            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
384                "Invalid WASM function arity for {}: max_args ({max_args}) < min_args ({})",
385                function.name, function.min_args
386            )));
387        }
388
389        for alias in &function.aliases {
390            let canonical_alias = normalize_custom_fn_name(alias)?;
391            if !names_and_aliases.insert(canonical_alias.clone()) {
392                return Err(ExcelError::new(ExcelErrorKind::Value)
393                    .with_message(format!("Duplicate WASM function alias: {alias}")));
394            }
395        }
396    }
397
398    Ok(())
399}
400
401#[cfg(feature = "wasm_plugins")]
402pub fn parse_wasm_manifest_json(bytes: &[u8]) -> Result<WasmModuleManifest, ExcelError> {
403    let manifest = serde_json::from_slice::<WasmModuleManifest>(bytes).map_err(|err| {
404        ExcelError::new(ExcelErrorKind::Value)
405            .with_message(format!("Failed to parse WASM manifest JSON: {err}"))
406    })?;
407    validate_wasm_manifest(&manifest)?;
408    Ok(manifest)
409}
410
411#[cfg(feature = "wasm_plugins")]
412pub fn extract_wasm_manifest_json_from_module(wasm_bytes: &[u8]) -> Result<Vec<u8>, ExcelError> {
413    let mut found: Option<Vec<u8>> = None;
414
415    for payload in Parser::new(0).parse_all(wasm_bytes) {
416        let payload = payload.map_err(|err| {
417            ExcelError::new(ExcelErrorKind::Value)
418                .with_message(format!("Invalid WASM module bytes: {err}"))
419        })?;
420
421        if let Payload::CustomSection(section) = payload
422            && section.name() == WASM_MANIFEST_SECTION_V1
423        {
424            if found.is_some() {
425                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
426                    "WASM module has multiple formualizer manifest custom sections",
427                ));
428            }
429            found = Some(section.data().to_vec());
430        }
431    }
432
433    found.ok_or_else(|| {
434        ExcelError::new(ExcelErrorKind::Value).with_message(format!(
435            "WASM module is missing required custom section: {WASM_MANIFEST_SECTION_V1}"
436        ))
437    })
438}
439
440#[cfg(feature = "wasm_plugins")]
441fn wasm_module_info_from_manifest(
442    module_id: String,
443    module_size_bytes: usize,
444    manifest: &WasmModuleManifest,
445) -> WasmModuleInfo {
446    WasmModuleInfo {
447        module_id,
448        version: manifest.module.version.clone(),
449        abi_version: manifest.module.abi,
450        codec_version: manifest.module.codec,
451        function_count: manifest.functions.len(),
452        module_size_bytes,
453    }
454}
455
456#[derive(Clone)]
457struct RegisteredWasmModule {
458    info: WasmModuleInfo,
459    #[allow(dead_code)]
460    manifest: WasmModuleManifest,
461    wasm_bytes: Arc<Vec<u8>>,
462}
463
464#[cfg_attr(not(feature = "wasm_plugins"), derive(Default))]
465struct WasmPluginManager {
466    modules: BTreeMap<String, RegisteredWasmModule>,
467    #[cfg(feature = "wasm_plugins")]
468    runtime: Arc<dyn WasmUdfRuntime>,
469}
470
471#[cfg(feature = "wasm_plugins")]
472impl Default for WasmPluginManager {
473    fn default() -> Self {
474        Self {
475            modules: BTreeMap::new(),
476            runtime: Arc::new(PendingWasmRuntime),
477        }
478    }
479}
480
481impl WasmPluginManager {
482    #[cfg(feature = "wasm_plugins")]
483    fn set_runtime(&mut self, runtime: Arc<dyn WasmUdfRuntime>) {
484        self.runtime = runtime;
485    }
486
487    #[cfg(feature = "wasm_plugins")]
488    fn runtime(&self) -> Arc<dyn WasmUdfRuntime> {
489        self.runtime.clone()
490    }
491    fn list_module_infos(&self) -> Vec<WasmModuleInfo> {
492        self.modules
493            .values()
494            .map(|registered| {
495                let mut info = registered.info.clone();
496                info.module_size_bytes = registered.wasm_bytes.len();
497                info
498            })
499            .collect()
500    }
501
502    #[cfg(feature = "wasm_plugins")]
503    fn get(&self, module_id: &str) -> Option<&RegisteredWasmModule> {
504        self.modules.get(module_id)
505    }
506
507    #[cfg(feature = "wasm_plugins")]
508    fn unregister_module(&mut self, module_id: &str) -> Result<(), ExcelError> {
509        if self.modules.remove(module_id).is_none() {
510            return Err(ExcelError::new(ExcelErrorKind::Name)
511                .with_message(format!("WASM module {module_id} is not registered")));
512        }
513        Ok(())
514    }
515
516    #[cfg(feature = "wasm_plugins")]
517    fn register_module_bytes(
518        &mut self,
519        requested_module_id: &str,
520        wasm_bytes: &[u8],
521    ) -> Result<WasmModuleInfo, ExcelError> {
522        if self.modules.contains_key(requested_module_id) {
523            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
524                "WASM module {requested_module_id} is already registered"
525            )));
526        }
527
528        let manifest_json = extract_wasm_manifest_json_from_module(wasm_bytes)?;
529        let manifest = parse_wasm_manifest_json(&manifest_json)?;
530
531        if manifest.module.id != requested_module_id {
532            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
533                "WASM manifest module id mismatch: requested {requested_module_id}, manifest {}",
534                manifest.module.id
535            )));
536        }
537
538        self.runtime
539            .validate_module(requested_module_id, wasm_bytes, &manifest)?;
540
541        let info = wasm_module_info_from_manifest(
542            requested_module_id.to_string(),
543            wasm_bytes.len(),
544            &manifest,
545        );
546
547        self.modules.insert(
548            requested_module_id.to_string(),
549            RegisteredWasmModule {
550                info: info.clone(),
551                manifest,
552                wasm_bytes: Arc::new(wasm_bytes.to_vec()),
553            },
554        );
555
556        Ok(info)
557    }
558}
559
560pub trait CustomFnHandler: Send + Sync {
561    fn call(&self, args: &[LiteralValue]) -> Result<LiteralValue, ExcelError>;
562
563    fn call_batch(&self, _rows: &[Vec<LiteralValue>]) -> Option<Result<LiteralValue, ExcelError>> {
564        None
565    }
566}
567
568impl<F> CustomFnHandler for F
569where
570    F: Fn(&[LiteralValue]) -> Result<LiteralValue, ExcelError> + Send + Sync,
571{
572    fn call(&self, args: &[LiteralValue]) -> Result<LiteralValue, ExcelError> {
573        (self)(args)
574    }
575}
576
577#[derive(Clone)]
578struct RegisteredCustomFn {
579    info: CustomFnInfo,
580    function: Arc<dyn formualizer_eval::function::Function>,
581}
582
583type CustomFnRegistry = BTreeMap<String, RegisteredCustomFn>;
584
585struct WorkbookCustomFunction {
586    canonical_name: String,
587    options: CustomFnOptions,
588    handler: Arc<dyn CustomFnHandler>,
589}
590
591impl WorkbookCustomFunction {
592    fn new(name: String, options: CustomFnOptions, handler: Arc<dyn CustomFnHandler>) -> Self {
593        Self {
594            canonical_name: name,
595            options,
596            handler,
597        }
598    }
599
600    fn validate_arity(&self, provided: usize) -> Result<(), ExcelError> {
601        if provided < self.options.min_args {
602            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
603                "{} expects at least {} argument(s), got {}",
604                self.canonical_name, self.options.min_args, provided
605            )));
606        }
607        if let Some(max) = self.options.max_args
608            && provided > max
609        {
610            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
611                "{} expects at most {} argument(s), got {}",
612                self.canonical_name, max, provided
613            )));
614        }
615        Ok(())
616    }
617
618    fn materialize_arg<'a, 'b>(
619        arg: &formualizer_eval::traits::ArgumentHandle<'a, 'b>,
620    ) -> Result<LiteralValue, ExcelError> {
621        match arg.value_or_range()? {
622            formualizer_eval::traits::EvaluatedArg::LiteralValue(v) => Ok(v.into_owned()),
623            formualizer_eval::traits::EvaluatedArg::Range(r) => {
624                Ok(LiteralValue::Array(r.materialise().into_owned()))
625            }
626        }
627    }
628}
629
630impl formualizer_eval::function::Function for WorkbookCustomFunction {
631    fn caps(&self) -> formualizer_eval::function::FnCaps {
632        let mut caps = formualizer_eval::function::FnCaps::empty();
633        if self.options.volatile {
634            caps |= formualizer_eval::function::FnCaps::VOLATILE;
635        } else if self.options.deterministic {
636            caps |= formualizer_eval::function::FnCaps::PURE;
637        }
638        caps
639    }
640
641    fn name(&self) -> &'static str {
642        "__WORKBOOK_CUSTOM__"
643    }
644
645    fn function_salt(&self) -> u64 {
646        stable_fn_salt(&self.canonical_name)
647    }
648
649    fn eval<'a, 'b, 'c>(
650        &self,
651        args: &'c [formualizer_eval::traits::ArgumentHandle<'a, 'b>],
652        _ctx: &dyn formualizer_eval::traits::FunctionContext<'b>,
653    ) -> Result<formualizer_eval::traits::CalcValue<'b>, ExcelError> {
654        self.validate_arity(args.len())?;
655
656        let mut materialized = Vec::with_capacity(args.len());
657        for arg in args {
658            materialized.push(Self::materialize_arg(arg)?);
659        }
660
661        let callback_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
662            self.handler.call(&materialized)
663        }));
664
665        match callback_result {
666            Ok(Ok(value)) => Ok(formualizer_eval::traits::CalcValue::Scalar(value)),
667            Ok(Err(err)) => Err(err),
668            Err(_) => Err(ExcelError::new(ExcelErrorKind::Value)
669                .with_message("Custom function callback panicked")),
670        }
671    }
672}
673
674#[cfg(feature = "wasm_plugins")]
675struct WorkbookWasmFunction {
676    canonical_name: String,
677    options: CustomFnOptions,
678    module_id: String,
679    export_name: String,
680    codec_version: u32,
681    runtime_hint: Option<WasmRuntimeHint>,
682    runtime: Arc<dyn WasmUdfRuntime>,
683}
684
685#[cfg(feature = "wasm_plugins")]
686impl WorkbookWasmFunction {
687    fn validate_arity(&self, provided: usize) -> Result<(), ExcelError> {
688        if provided < self.options.min_args {
689            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
690                "{} expects at least {} argument(s), got {}",
691                self.canonical_name, self.options.min_args, provided
692            )));
693        }
694        if let Some(max) = self.options.max_args
695            && provided > max
696        {
697            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
698                "{} expects at most {} argument(s), got {}",
699                self.canonical_name, max, provided
700            )));
701        }
702        Ok(())
703    }
704}
705
706#[cfg(feature = "wasm_plugins")]
707impl formualizer_eval::function::Function for WorkbookWasmFunction {
708    fn caps(&self) -> formualizer_eval::function::FnCaps {
709        let mut caps = formualizer_eval::function::FnCaps::empty();
710        if self.options.volatile {
711            caps |= formualizer_eval::function::FnCaps::VOLATILE;
712        } else if self.options.deterministic {
713            caps |= formualizer_eval::function::FnCaps::PURE;
714        }
715        caps
716    }
717
718    fn name(&self) -> &'static str {
719        "__WORKBOOK_WASM__"
720    }
721
722    fn function_salt(&self) -> u64 {
723        stable_fn_salt(&self.canonical_name)
724    }
725
726    fn eval<'a, 'b, 'c>(
727        &self,
728        args: &'c [formualizer_eval::traits::ArgumentHandle<'a, 'b>],
729        _ctx: &dyn formualizer_eval::traits::FunctionContext<'b>,
730    ) -> Result<formualizer_eval::traits::CalcValue<'b>, ExcelError> {
731        self.validate_arity(args.len())?;
732
733        let mut materialized = Vec::with_capacity(args.len());
734        for arg in args {
735            materialized.push(WorkbookCustomFunction::materialize_arg(arg)?);
736        }
737
738        let runtime_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
739            self.runtime.invoke(
740                &self.module_id,
741                &self.export_name,
742                &self.canonical_name,
743                self.codec_version,
744                &materialized,
745                self.runtime_hint.as_ref(),
746            )
747        }));
748
749        match runtime_result {
750            Ok(Ok(value)) => Ok(formualizer_eval::traits::CalcValue::Scalar(value)),
751            Ok(Err(err)) => Err(err),
752            Err(_) => Err(ExcelError::new(ExcelErrorKind::Value)
753                .with_message("WASM function runtime panicked")),
754        }
755    }
756}
757
758/// Minimal resolver for engine-backed workbook (cells/ranges via graph/arrow; functions via registry).
759#[derive(Clone)]
760pub struct WBResolver {
761    custom_functions: Arc<RwLock<CustomFnRegistry>>,
762}
763
764impl Default for WBResolver {
765    fn default() -> Self {
766        Self {
767            custom_functions: Arc::new(RwLock::new(BTreeMap::new())),
768        }
769    }
770}
771
772impl WBResolver {
773    fn new(custom_functions: Arc<RwLock<CustomFnRegistry>>) -> Self {
774        Self { custom_functions }
775    }
776}
777
778impl formualizer_eval::traits::ReferenceResolver for WBResolver {
779    fn resolve_cell_reference(
780        &self,
781        _sheet: Option<&str>,
782        _row: u32,
783        _col: u32,
784    ) -> Result<LiteralValue, formualizer_common::error::ExcelError> {
785        Err(formualizer_common::error::ExcelError::from(
786            formualizer_common::error::ExcelErrorKind::NImpl,
787        ))
788    }
789}
790impl formualizer_eval::traits::RangeResolver for WBResolver {
791    fn resolve_range_reference(
792        &self,
793        _sheet: Option<&str>,
794        _sr: Option<u32>,
795        _sc: Option<u32>,
796        _er: Option<u32>,
797        _ec: Option<u32>,
798    ) -> Result<Box<dyn formualizer_eval::traits::Range>, formualizer_common::error::ExcelError>
799    {
800        Err(formualizer_common::error::ExcelError::from(
801            formualizer_common::error::ExcelErrorKind::NImpl,
802        ))
803    }
804}
805impl formualizer_eval::traits::NamedRangeResolver for WBResolver {
806    fn resolve_named_range_reference(
807        &self,
808        _name: &str,
809    ) -> Result<Vec<Vec<LiteralValue>>, formualizer_common::error::ExcelError> {
810        Err(ExcelError::new(ExcelErrorKind::Name)
811            .with_message(format!("Undefined name: {}", _name)))
812    }
813}
814impl formualizer_eval::traits::TableResolver for WBResolver {
815    fn resolve_table_reference(
816        &self,
817        _tref: &formualizer_parse::parser::TableReference,
818    ) -> Result<Box<dyn formualizer_eval::traits::Table>, formualizer_common::error::ExcelError>
819    {
820        Err(formualizer_common::error::ExcelError::from(
821            formualizer_common::error::ExcelErrorKind::NImpl,
822        ))
823    }
824}
825impl formualizer_eval::traits::SourceResolver for WBResolver {}
826impl formualizer_eval::traits::FunctionProvider for WBResolver {
827    fn get_function(
828        &self,
829        ns: &str,
830        name: &str,
831    ) -> Option<std::sync::Arc<dyn formualizer_eval::function::Function>> {
832        if ns.is_empty() {
833            let key = name.to_ascii_uppercase();
834            if let Some(local) = self.custom_functions.read().get(&key) {
835                return Some(local.function.clone());
836            }
837        }
838        formualizer_eval::function_registry::get(ns, name)
839    }
840}
841impl formualizer_eval::traits::Resolver for WBResolver {}
842impl formualizer_eval::traits::EvaluationContext for WBResolver {}
843
844/// Engine-backed workbook facade.
845pub struct Workbook {
846    engine: formualizer_eval::engine::Engine<WBResolver>,
847    custom_functions: Arc<RwLock<CustomFnRegistry>>,
848    wasm_plugins: WasmPluginManager,
849    enable_changelog: bool,
850    log: formualizer_eval::engine::ChangeLog,
851    undo: formualizer_eval::engine::graph::editor::undo_engine::UndoEngine,
852}
853
854trait WorkbookActionOps {
855    fn set_value(
856        &mut self,
857        sheet: &str,
858        row: u32,
859        col: u32,
860        value: LiteralValue,
861    ) -> Result<(), IoError>;
862
863    fn set_formula(
864        &mut self,
865        sheet: &str,
866        row: u32,
867        col: u32,
868        formula: &str,
869    ) -> Result<(), IoError>;
870
871    fn set_values(
872        &mut self,
873        sheet: &str,
874        start_row: u32,
875        start_col: u32,
876        rows: &[Vec<LiteralValue>],
877    ) -> Result<(), IoError>;
878
879    fn write_range(
880        &mut self,
881        sheet: &str,
882        start: (u32, u32),
883        cells: BTreeMap<(u32, u32), crate::traits::CellData>,
884    ) -> Result<(), IoError>;
885}
886
887/// Transactional edit surface for `Workbook::action`.
888///
889/// This wrapper exists to avoid aliasing `&mut Workbook` while an Engine transaction is active.
890/// It intentionally exposes only valueful edit operations that can participate in rollback.
891pub struct WorkbookAction<'a> {
892    ops: &'a mut dyn WorkbookActionOps,
893}
894
895impl WorkbookAction<'_> {
896    #[inline]
897    pub fn set_value(
898        &mut self,
899        sheet: &str,
900        row: u32,
901        col: u32,
902        value: LiteralValue,
903    ) -> Result<(), IoError> {
904        self.ops.set_value(sheet, row, col, value)
905    }
906
907    #[inline]
908    pub fn set_formula(
909        &mut self,
910        sheet: &str,
911        row: u32,
912        col: u32,
913        formula: &str,
914    ) -> Result<(), IoError> {
915        self.ops.set_formula(sheet, row, col, formula)
916    }
917
918    #[inline]
919    pub fn set_values(
920        &mut self,
921        sheet: &str,
922        start_row: u32,
923        start_col: u32,
924        rows: &[Vec<LiteralValue>],
925    ) -> Result<(), IoError> {
926        self.ops.set_values(sheet, start_row, start_col, rows)
927    }
928
929    #[inline]
930    pub fn write_range(
931        &mut self,
932        sheet: &str,
933        start: (u32, u32),
934        cells: BTreeMap<(u32, u32), crate::traits::CellData>,
935    ) -> Result<(), IoError> {
936        self.ops.write_range(sheet, start, cells)
937    }
938}
939
940#[derive(Clone, Copy, Debug, PartialEq, Eq)]
941pub enum WorkbookMode {
942    /// Fastpath parity with direct Engine usage.
943    Ephemeral,
944    /// Default workbook behavior (changelog + deferred graph build).
945    Interactive,
946}
947
948#[derive(Clone, Debug)]
949pub struct WorkbookConfig {
950    pub eval: formualizer_eval::engine::EvalConfig,
951    pub enable_changelog: bool,
952}
953
954impl WorkbookConfig {
955    pub fn ephemeral() -> Self {
956        Self {
957            eval: formualizer_eval::engine::EvalConfig::default(),
958            enable_changelog: false,
959        }
960    }
961
962    pub fn interactive() -> Self {
963        let eval = formualizer_eval::engine::EvalConfig {
964            defer_graph_building: true,
965            formula_parse_policy: formualizer_eval::engine::FormulaParsePolicy::CoerceToError,
966            ..Default::default()
967        };
968        Self {
969            eval,
970            enable_changelog: true,
971        }
972    }
973}
974
975impl Default for Workbook {
976    fn default() -> Self {
977        Self::new()
978    }
979}
980
981impl Workbook {
982    pub fn new_with_config(mut config: WorkbookConfig) -> Self {
983        config.eval.arrow_storage_enabled = true;
984        config.eval.delta_overlay_enabled = true;
985        config.eval.write_formula_overlay_enabled = true;
986
987        let custom_functions = Arc::new(RwLock::new(BTreeMap::new()));
988        let resolver = WBResolver::new(custom_functions.clone());
989        let engine = formualizer_eval::engine::Engine::new(resolver, config.eval);
990
991        let mut log = formualizer_eval::engine::ChangeLog::new();
992        log.set_enabled(config.enable_changelog);
993        Self {
994            engine,
995            custom_functions,
996            wasm_plugins: WasmPluginManager::default(),
997            enable_changelog: config.enable_changelog,
998            log,
999            undo: formualizer_eval::engine::graph::editor::undo_engine::UndoEngine::new(),
1000        }
1001    }
1002    pub fn new_with_mode(mode: WorkbookMode) -> Self {
1003        let config = match mode {
1004            WorkbookMode::Ephemeral => WorkbookConfig::ephemeral(),
1005            WorkbookMode::Interactive => WorkbookConfig::interactive(),
1006        };
1007        Self::new_with_config(config)
1008    }
1009    pub fn new() -> Self {
1010        Self::new_with_mode(WorkbookMode::Interactive)
1011    }
1012
1013    pub fn register_custom_function(
1014        &mut self,
1015        name: &str,
1016        options: CustomFnOptions,
1017        handler: Arc<dyn CustomFnHandler>,
1018    ) -> Result<(), ExcelError> {
1019        let canonical_name = normalize_custom_fn_name(name)?;
1020
1021        validate_custom_arity(&canonical_name, &options)?;
1022
1023        if self.custom_functions.read().contains_key(&canonical_name) {
1024            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1025                "Custom function {canonical_name} is already registered"
1026            )));
1027        }
1028
1029        if !options.allow_override_builtin
1030            && formualizer_eval::function_registry::get("", &canonical_name).is_some()
1031        {
1032            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1033                "Custom function {canonical_name} conflicts with a global function; set allow_override_builtin=true to override"
1034            )));
1035        }
1036
1037        let info = CustomFnInfo {
1038            name: canonical_name.clone(),
1039            options: options.clone(),
1040        };
1041        let function = Arc::new(WorkbookCustomFunction::new(
1042            canonical_name.clone(),
1043            options,
1044            handler,
1045        ));
1046
1047        self.custom_functions
1048            .write()
1049            .insert(canonical_name, RegisteredCustomFn { info, function });
1050        Ok(())
1051    }
1052
1053    /// Inspect a WASM module manifest and return module metadata without mutating workbook state.
1054    pub fn inspect_wasm_module_bytes(
1055        &self,
1056        wasm_bytes: &[u8],
1057    ) -> Result<WasmModuleInfo, ExcelError> {
1058        #[cfg(feature = "wasm_plugins")]
1059        {
1060            let manifest_json = extract_wasm_manifest_json_from_module(wasm_bytes)?;
1061            let manifest = parse_wasm_manifest_json(&manifest_json)?;
1062            let canonical_module_id = normalize_wasm_module_id(&manifest.module.id)?;
1063            Ok(wasm_module_info_from_manifest(
1064                canonical_module_id,
1065                wasm_bytes.len(),
1066                &manifest,
1067            ))
1068        }
1069
1070        #[cfg(not(feature = "wasm_plugins"))]
1071        {
1072            let _ = wasm_bytes;
1073            Err(ExcelError::new(ExcelErrorKind::NImpl)
1074                .with_message("WASM module inspection requires the `wasm_plugins` feature"))
1075        }
1076    }
1077
1078    pub fn register_wasm_module_bytes(
1079        &mut self,
1080        module_id: &str,
1081        wasm_bytes: &[u8],
1082    ) -> Result<WasmModuleInfo, ExcelError> {
1083        let canonical_module_id = normalize_wasm_module_id(module_id)?;
1084
1085        #[cfg(feature = "wasm_plugins")]
1086        {
1087            self.wasm_plugins
1088                .register_module_bytes(&canonical_module_id, wasm_bytes)
1089        }
1090
1091        #[cfg(not(feature = "wasm_plugins"))]
1092        {
1093            let _ = wasm_bytes;
1094            Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
1095                "WASM module registration for {canonical_module_id} requires the `wasm_plugins` feature"
1096            )))
1097        }
1098    }
1099
1100    /// Inspect a WASM module file without mutating workbook state.
1101    pub fn inspect_wasm_module_file(
1102        &self,
1103        path: impl AsRef<std::path::Path>,
1104    ) -> Result<WasmModuleInfo, ExcelError> {
1105        #[cfg(not(target_arch = "wasm32"))]
1106        {
1107            let bytes = read_wasm_file_bytes(path.as_ref())?;
1108            self.inspect_wasm_module_bytes(&bytes)
1109        }
1110
1111        #[cfg(target_arch = "wasm32")]
1112        {
1113            let _ = path;
1114            Err(ExcelError::new(ExcelErrorKind::NImpl)
1115                .with_message("WASM module file inspection is not available on wasm32 hosts"))
1116        }
1117    }
1118
1119    /// Inspect all `*.wasm` files in a directory without mutating workbook state.
1120    pub fn inspect_wasm_modules_dir(
1121        &self,
1122        dir: impl AsRef<std::path::Path>,
1123    ) -> Result<Vec<WasmModuleInfo>, ExcelError> {
1124        #[cfg(not(target_arch = "wasm32"))]
1125        {
1126            let mut infos = Vec::new();
1127            for path in collect_wasm_files_in_dir(dir.as_ref())? {
1128                let bytes = read_wasm_file_bytes(&path)?;
1129                infos.push(self.inspect_wasm_module_bytes(&bytes)?);
1130            }
1131            Ok(infos)
1132        }
1133
1134        #[cfg(target_arch = "wasm32")]
1135        {
1136            let _ = dir;
1137            Err(ExcelError::new(ExcelErrorKind::NImpl)
1138                .with_message("WASM module directory inspection is not available on wasm32 hosts"))
1139        }
1140    }
1141
1142    /// Alias for clearer workbook-local terminology.
1143    pub fn attach_wasm_module_bytes(
1144        &mut self,
1145        module_id: &str,
1146        wasm_bytes: &[u8],
1147    ) -> Result<WasmModuleInfo, ExcelError> {
1148        self.register_wasm_module_bytes(module_id, wasm_bytes)
1149    }
1150
1151    /// Attach a WASM module from a file path using the module id from its manifest.
1152    pub fn attach_wasm_module_file(
1153        &mut self,
1154        path: impl AsRef<std::path::Path>,
1155    ) -> Result<WasmModuleInfo, ExcelError> {
1156        #[cfg(not(target_arch = "wasm32"))]
1157        {
1158            let bytes = read_wasm_file_bytes(path.as_ref())?;
1159            let info = self.inspect_wasm_module_bytes(&bytes)?;
1160            self.attach_wasm_module_bytes(&info.module_id, &bytes)
1161        }
1162
1163        #[cfg(target_arch = "wasm32")]
1164        {
1165            let _ = path;
1166            Err(ExcelError::new(ExcelErrorKind::NImpl)
1167                .with_message("WASM module file attachment is not available on wasm32 hosts"))
1168        }
1169    }
1170
1171    /// Attach all `*.wasm` modules found in a directory.
1172    pub fn attach_wasm_modules_dir(
1173        &mut self,
1174        dir: impl AsRef<std::path::Path>,
1175    ) -> Result<Vec<WasmModuleInfo>, ExcelError> {
1176        #[cfg(not(target_arch = "wasm32"))]
1177        {
1178            let mut attached = Vec::new();
1179            for path in collect_wasm_files_in_dir(dir.as_ref())? {
1180                attached.push(self.attach_wasm_module_file(path)?);
1181            }
1182            Ok(attached)
1183        }
1184
1185        #[cfg(target_arch = "wasm32")]
1186        {
1187            let _ = dir;
1188            Err(ExcelError::new(ExcelErrorKind::NImpl)
1189                .with_message("WASM module directory attachment is not available on wasm32 hosts"))
1190        }
1191    }
1192
1193    pub fn list_wasm_modules(&self) -> Vec<WasmModuleInfo> {
1194        self.wasm_plugins.list_module_infos()
1195    }
1196
1197    pub fn unregister_wasm_module(&mut self, module_id: &str) -> Result<(), ExcelError> {
1198        let canonical_module_id = normalize_wasm_module_id(module_id)?;
1199
1200        #[cfg(feature = "wasm_plugins")]
1201        {
1202            self.wasm_plugins.unregister_module(&canonical_module_id)
1203        }
1204
1205        #[cfg(not(feature = "wasm_plugins"))]
1206        {
1207            Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
1208                "WASM module unregistration for {canonical_module_id} requires the `wasm_plugins` feature"
1209            )))
1210        }
1211    }
1212
1213    #[cfg(feature = "wasm_plugins")]
1214    #[doc(hidden)]
1215    pub fn set_wasm_runtime(&mut self, runtime: Arc<dyn WasmUdfRuntime>) {
1216        self.wasm_plugins.set_runtime(runtime);
1217    }
1218
1219    #[cfg(all(feature = "wasm_runtime_wasmtime", not(target_arch = "wasm32")))]
1220    pub fn use_wasmtime_runtime(&mut self) {
1221        self.wasm_plugins
1222            .set_runtime(Arc::new(new_wasmtime_runtime()));
1223    }
1224
1225    pub fn register_wasm_function(
1226        &mut self,
1227        name: &str,
1228        options: CustomFnOptions,
1229        spec: WasmFunctionSpec,
1230    ) -> Result<(), ExcelError> {
1231        let canonical_name = normalize_custom_fn_name(name)?;
1232        validate_custom_arity(&canonical_name, &options)?;
1233        validate_wasm_spec(&spec)?;
1234
1235        #[cfg(feature = "wasm_plugins")]
1236        {
1237            let module_id = normalize_wasm_module_id(&spec.module_id)?;
1238            let module = self.wasm_plugins.get(&module_id).ok_or_else(|| {
1239                ExcelError::new(ExcelErrorKind::Name)
1240                    .with_message(format!("WASM module {module_id} is not registered"))
1241            })?;
1242
1243            if module.manifest.module.codec != spec.codec_version {
1244                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
1245                    "WASM codec mismatch for {canonical_name}: spec codec {} != module codec {}",
1246                    spec.codec_version, module.manifest.module.codec
1247                )));
1248            }
1249
1250            if !module
1251                .manifest
1252                .functions
1253                .iter()
1254                .any(|function| function.export_name == spec.export_name)
1255            {
1256                return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1257                    "WASM export {} is not declared in module {}",
1258                    spec.export_name, module_id
1259                )));
1260            }
1261
1262            if self.custom_functions.read().contains_key(&canonical_name) {
1263                return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1264                    "Custom function {canonical_name} is already registered"
1265                )));
1266            }
1267
1268            if !options.allow_override_builtin
1269                && formualizer_eval::function_registry::get("", &canonical_name).is_some()
1270            {
1271                return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1272                    "Custom function {canonical_name} conflicts with a global function; set allow_override_builtin=true to override"
1273                )));
1274            }
1275
1276            let runtime = self.wasm_plugins.runtime();
1277            if !runtime.can_bind_functions() {
1278                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
1279                    "WASM plugin runtime integration is pending for {canonical_name} (module_id={}, export_name={}, codec_version={})",
1280                    module_id, spec.export_name, spec.codec_version
1281                )));
1282            }
1283
1284            let info = CustomFnInfo {
1285                name: canonical_name.clone(),
1286                options: options.clone(),
1287            };
1288            let function = Arc::new(WorkbookWasmFunction {
1289                canonical_name: canonical_name.clone(),
1290                options,
1291                module_id,
1292                export_name: spec.export_name,
1293                codec_version: spec.codec_version,
1294                runtime_hint: spec.runtime_hint,
1295                runtime,
1296            });
1297
1298            self.custom_functions
1299                .write()
1300                .insert(canonical_name, RegisteredCustomFn { info, function });
1301            Ok(())
1302        }
1303
1304        #[cfg(not(feature = "wasm_plugins"))]
1305        {
1306            Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
1307                "WASM plugin registration for {canonical_name} requires the `wasm_plugins` feature (module_id={}, export_name={}, codec_version={})",
1308                spec.module_id, spec.export_name, spec.codec_version
1309            )))
1310        }
1311    }
1312
1313    /// Alias for clearer workbook-local terminology.
1314    pub fn bind_wasm_function(
1315        &mut self,
1316        name: &str,
1317        options: CustomFnOptions,
1318        spec: WasmFunctionSpec,
1319    ) -> Result<(), ExcelError> {
1320        self.register_wasm_function(name, options, spec)
1321    }
1322
1323    pub fn unregister_custom_function(&mut self, name: &str) -> Result<(), ExcelError> {
1324        let canonical_name = normalize_custom_fn_name(name)?;
1325        if self
1326            .custom_functions
1327            .write()
1328            .remove(&canonical_name)
1329            .is_none()
1330        {
1331            return Err(ExcelError::new(ExcelErrorKind::Name).with_message(format!(
1332                "Custom function {canonical_name} is not registered"
1333            )));
1334        }
1335        Ok(())
1336    }
1337
1338    pub fn list_custom_functions(&self) -> Vec<CustomFnInfo> {
1339        self.custom_functions
1340            .read()
1341            .values()
1342            .map(|registered| registered.info.clone())
1343            .collect()
1344    }
1345
1346    pub fn engine(&self) -> &formualizer_eval::engine::Engine<WBResolver> {
1347        &self.engine
1348    }
1349    pub fn engine_mut(&mut self) -> &mut formualizer_eval::engine::Engine<WBResolver> {
1350        &mut self.engine
1351    }
1352    pub fn eval_config(&self) -> &formualizer_eval::engine::EvalConfig {
1353        &self.engine.config
1354    }
1355
1356    pub fn deterministic_mode(&self) -> &formualizer_eval::engine::DeterministicMode {
1357        &self.engine.config.deterministic_mode
1358    }
1359
1360    pub fn set_deterministic_mode(
1361        &mut self,
1362        mode: formualizer_eval::engine::DeterministicMode,
1363    ) -> Result<(), IoError> {
1364        self.engine
1365            .set_deterministic_mode(mode)
1366            .map_err(IoError::Engine)
1367    }
1368
1369    // Changelog controls
1370    pub fn set_changelog_enabled(&mut self, enabled: bool) {
1371        self.enable_changelog = enabled;
1372        self.log.set_enabled(enabled);
1373    }
1374
1375    // Changelog metadata
1376    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
1377        self.log.set_actor_id(actor_id);
1378    }
1379
1380    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
1381        self.log.set_correlation_id(correlation_id);
1382    }
1383
1384    pub fn set_reason(&mut self, reason: Option<String>) {
1385        self.log.set_reason(reason);
1386    }
1387    pub fn begin_action(&mut self, description: impl Into<String>) {
1388        if self.enable_changelog {
1389            self.log.begin_compound(description.into());
1390        }
1391    }
1392    pub fn end_action(&mut self) {
1393        if self.enable_changelog {
1394            self.log.end_compound();
1395        }
1396    }
1397
1398    /// Execute an atomic workbook action.
1399    ///
1400    /// When changelog is enabled, this delegates to `Engine::action_with_logger` and therefore:
1401    /// - logs changes into the changelog as a compound
1402    /// - rolls back graph + Arrow-truth value changes on error
1403    /// - truncates the changelog on rollback
1404    ///
1405    /// The closure receives a `WorkbookAction` rather than `&mut Workbook` to avoid aliasing
1406    /// `&mut Workbook` while the Engine transaction is active.
1407    pub fn action<T>(
1408        &mut self,
1409        name: &str,
1410        f: impl FnOnce(&mut WorkbookAction<'_>) -> Result<T, IoError>,
1411    ) -> Result<T, IoError> {
1412        let mut user_err: Option<IoError> = None;
1413
1414        if self.enable_changelog {
1415            let res = self.engine.action_with_logger(&mut self.log, name, |tx| {
1416                struct TxOps<'a, 'e> {
1417                    tx: &'a mut formualizer_eval::engine::EngineAction<'e, WBResolver>,
1418                }
1419                impl WorkbookActionOps for TxOps<'_, '_> {
1420                    fn set_value(
1421                        &mut self,
1422                        sheet: &str,
1423                        row: u32,
1424                        col: u32,
1425                        value: LiteralValue,
1426                    ) -> Result<(), IoError> {
1427                        self.tx
1428                            .set_cell_value(sheet, row, col, value)
1429                            .map_err(|e| match e {
1430                                formualizer_eval::engine::EditorError::Excel(excel) => {
1431                                    IoError::Engine(excel)
1432                                }
1433                                other => IoError::from_backend("editor", other),
1434                            })
1435                    }
1436
1437                    fn set_formula(
1438                        &mut self,
1439                        sheet: &str,
1440                        row: u32,
1441                        col: u32,
1442                        formula: &str,
1443                    ) -> Result<(), IoError> {
1444                        let with_eq = if formula.starts_with('=') {
1445                            formula.to_string()
1446                        } else {
1447                            format!("={formula}")
1448                        };
1449                        let ast = formualizer_parse::parser::parse(&with_eq)
1450                            .map_err(|e| IoError::from_backend("parser", e))?;
1451                        self.tx
1452                            .set_cell_formula(sheet, row, col, ast)
1453                            .map_err(|e| match e {
1454                                formualizer_eval::engine::EditorError::Excel(excel) => {
1455                                    IoError::Engine(excel)
1456                                }
1457                                other => IoError::from_backend("editor", other),
1458                            })
1459                    }
1460
1461                    fn set_values(
1462                        &mut self,
1463                        sheet: &str,
1464                        start_row: u32,
1465                        start_col: u32,
1466                        rows: &[Vec<LiteralValue>],
1467                    ) -> Result<(), IoError> {
1468                        for (ri, rvals) in rows.iter().enumerate() {
1469                            let r = start_row + ri as u32;
1470                            for (ci, v) in rvals.iter().enumerate() {
1471                                let c = start_col + ci as u32;
1472                                self.set_value(sheet, r, c, v.clone())?;
1473                            }
1474                        }
1475                        Ok(())
1476                    }
1477
1478                    fn write_range(
1479                        &mut self,
1480                        sheet: &str,
1481                        _start: (u32, u32),
1482                        cells: BTreeMap<(u32, u32), crate::traits::CellData>,
1483                    ) -> Result<(), IoError> {
1484                        for ((r, c), d) in cells.into_iter() {
1485                            if let Some(v) = d.value {
1486                                self.set_value(sheet, r, c, v)?;
1487                            }
1488                            if let Some(f) = d.formula.as_ref() {
1489                                self.set_formula(sheet, r, c, f)?;
1490                            }
1491                        }
1492                        Ok(())
1493                    }
1494                }
1495
1496                let mut ops = TxOps { tx };
1497                let mut wtx = WorkbookAction { ops: &mut ops };
1498                match f(&mut wtx) {
1499                    Ok(v) => Ok(v),
1500                    Err(e) => {
1501                        user_err = Some(e);
1502                        Err(formualizer_eval::engine::EditorError::TransactionFailed {
1503                            reason: "Workbook::action aborted".to_string(),
1504                        })
1505                    }
1506                }
1507            });
1508
1509            if let Some(e) = user_err {
1510                return Err(e);
1511            }
1512            return res.map_err(|e| match e {
1513                formualizer_eval::engine::EditorError::Excel(excel) => IoError::Engine(excel),
1514                other => IoError::from_backend("editor", other),
1515            });
1516        }
1517
1518        let res = self.engine.action_atomic_journal(name.to_string(), |tx| {
1519            struct TxOps<'a, 'e> {
1520                tx: &'a mut formualizer_eval::engine::EngineAction<'e, WBResolver>,
1521            }
1522            impl WorkbookActionOps for TxOps<'_, '_> {
1523                fn set_value(
1524                    &mut self,
1525                    sheet: &str,
1526                    row: u32,
1527                    col: u32,
1528                    value: LiteralValue,
1529                ) -> Result<(), IoError> {
1530                    self.tx
1531                        .set_cell_value(sheet, row, col, value)
1532                        .map_err(|e| match e {
1533                            formualizer_eval::engine::EditorError::Excel(excel) => {
1534                                IoError::Engine(excel)
1535                            }
1536                            other => IoError::from_backend("editor", other),
1537                        })
1538                }
1539
1540                fn set_formula(
1541                    &mut self,
1542                    sheet: &str,
1543                    row: u32,
1544                    col: u32,
1545                    formula: &str,
1546                ) -> Result<(), IoError> {
1547                    let with_eq = if formula.starts_with('=') {
1548                        formula.to_string()
1549                    } else {
1550                        format!("={formula}")
1551                    };
1552                    let ast = formualizer_parse::parser::parse(&with_eq)
1553                        .map_err(|e| IoError::from_backend("parser", e))?;
1554                    self.tx
1555                        .set_cell_formula(sheet, row, col, ast)
1556                        .map_err(|e| match e {
1557                            formualizer_eval::engine::EditorError::Excel(excel) => {
1558                                IoError::Engine(excel)
1559                            }
1560                            other => IoError::from_backend("editor", other),
1561                        })
1562                }
1563
1564                fn set_values(
1565                    &mut self,
1566                    sheet: &str,
1567                    start_row: u32,
1568                    start_col: u32,
1569                    rows: &[Vec<LiteralValue>],
1570                ) -> Result<(), IoError> {
1571                    for (ri, rvals) in rows.iter().enumerate() {
1572                        let r = start_row + ri as u32;
1573                        for (ci, v) in rvals.iter().enumerate() {
1574                            let c = start_col + ci as u32;
1575                            self.set_value(sheet, r, c, v.clone())?;
1576                        }
1577                    }
1578                    Ok(())
1579                }
1580
1581                fn write_range(
1582                    &mut self,
1583                    sheet: &str,
1584                    _start: (u32, u32),
1585                    cells: BTreeMap<(u32, u32), crate::traits::CellData>,
1586                ) -> Result<(), IoError> {
1587                    for ((r, c), d) in cells.into_iter() {
1588                        if let Some(v) = d.value {
1589                            self.set_value(sheet, r, c, v)?;
1590                        }
1591                        if let Some(f) = d.formula.as_ref() {
1592                            self.set_formula(sheet, r, c, f)?;
1593                        }
1594                    }
1595                    Ok(())
1596                }
1597            }
1598
1599            let mut ops = TxOps { tx };
1600            let mut wtx = WorkbookAction { ops: &mut ops };
1601            match f(&mut wtx) {
1602                Ok(v) => Ok(v),
1603                Err(e) => {
1604                    user_err = Some(e);
1605                    Err(formualizer_eval::engine::EditorError::TransactionFailed {
1606                        reason: "Workbook::action aborted".to_string(),
1607                    })
1608                }
1609            }
1610        });
1611
1612        if let Some(e) = user_err {
1613            return Err(e);
1614        }
1615        let (v, journal) = res.map_err(|e| match e {
1616            formualizer_eval::engine::EditorError::Excel(excel) => IoError::Engine(excel),
1617            other => IoError::from_backend("editor", other),
1618        })?;
1619        self.undo.push_action(journal);
1620        Ok(v)
1621    }
1622    pub fn undo(&mut self) -> Result<(), IoError> {
1623        if self.enable_changelog {
1624            self.engine
1625                .undo_logged(&mut self.undo, &mut self.log)
1626                .map_err(|e| IoError::from_backend("editor", e))?;
1627        } else {
1628            self.engine
1629                .undo_action(&mut self.undo)
1630                .map_err(|e| IoError::from_backend("editor", e))?;
1631        }
1632        Ok(())
1633    }
1634    pub fn redo(&mut self) -> Result<(), IoError> {
1635        if self.enable_changelog {
1636            self.engine
1637                .redo_logged(&mut self.undo, &mut self.log)
1638                .map_err(|e| IoError::from_backend("editor", e))?;
1639        } else {
1640            self.engine
1641                .redo_action(&mut self.undo)
1642                .map_err(|e| IoError::from_backend("editor", e))?;
1643        }
1644        Ok(())
1645    }
1646
1647    fn ensure_arrow_sheet_capacity(&mut self, sheet: &str, min_rows: usize, min_cols: usize) {
1648        use formualizer_eval::arrow_store::ArrowSheet;
1649
1650        if self.engine.sheet_store().sheet(sheet).is_none() {
1651            self.engine.sheet_store_mut().sheets.push(ArrowSheet {
1652                name: std::sync::Arc::<str>::from(sheet),
1653                columns: Vec::new(),
1654                nrows: 0,
1655                chunk_starts: Vec::new(),
1656                chunk_rows: 32 * 1024,
1657            });
1658        }
1659
1660        let asheet = self
1661            .engine
1662            .sheet_store_mut()
1663            .sheet_mut(sheet)
1664            .expect("ArrowSheet must exist");
1665
1666        // Ensure rows first so nrows is set before inserting columns
1667        if min_rows > asheet.nrows as usize {
1668            asheet.ensure_row_capacity(min_rows);
1669        }
1670
1671        // Then ensure columns - they will get properly sized chunks since nrows is set
1672        let cur_cols = asheet.columns.len();
1673        if min_cols > cur_cols {
1674            asheet.insert_columns(cur_cols, min_cols - cur_cols);
1675        }
1676    }
1677
1678    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
1679        use formualizer_eval::arrow_store::OverlayValue;
1680        if !(self.engine.config.arrow_storage_enabled && self.engine.config.delta_overlay_enabled) {
1681            return;
1682        }
1683        let date_system = self.engine.config.date_system;
1684        let row0 = row.saturating_sub(1) as usize;
1685        let col0 = col.saturating_sub(1) as usize;
1686        self.ensure_arrow_sheet_capacity(sheet, row0 + 1, col0 + 1);
1687        let asheet = self
1688            .engine
1689            .sheet_store_mut()
1690            .sheet_mut(sheet)
1691            .expect("ArrowSheet must exist");
1692        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
1693            let ov = match value {
1694                LiteralValue::Empty => OverlayValue::Empty,
1695                LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
1696                LiteralValue::Number(n) => OverlayValue::Number(*n),
1697                LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
1698                LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
1699                LiteralValue::Error(e) => {
1700                    OverlayValue::Error(formualizer_eval::arrow_store::map_error_code(e.kind))
1701                }
1702                LiteralValue::Date(d) => {
1703                    let dt = d.and_hms_opt(0, 0, 0).unwrap();
1704                    let serial = formualizer_eval::builtins::datetime::datetime_to_serial_for(
1705                        date_system,
1706                        &dt,
1707                    );
1708                    OverlayValue::DateTime(serial)
1709                }
1710                LiteralValue::DateTime(dt) => {
1711                    let serial = formualizer_eval::builtins::datetime::datetime_to_serial_for(
1712                        date_system,
1713                        dt,
1714                    );
1715                    OverlayValue::DateTime(serial)
1716                }
1717                LiteralValue::Time(t) => {
1718                    let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
1719                    OverlayValue::DateTime(serial)
1720                }
1721                LiteralValue::Duration(d) => {
1722                    let serial = d.num_seconds() as f64 / 86_400.0;
1723                    OverlayValue::Duration(serial)
1724                }
1725                LiteralValue::Pending => OverlayValue::Pending,
1726                LiteralValue::Array(_) => {
1727                    OverlayValue::Error(formualizer_eval::arrow_store::map_error_code(
1728                        formualizer_common::ExcelErrorKind::Value,
1729                    ))
1730                }
1731            };
1732            // Use ensure_column_chunk_mut to lazily create chunk if needed
1733            if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
1734                ch.overlay.set(in_off, ov);
1735            }
1736        }
1737    }
1738
1739    // Sheets
1740    pub fn sheet_names(&self) -> Vec<String> {
1741        self.engine
1742            .sheet_store()
1743            .sheets
1744            .iter()
1745            .map(|s| s.name.as_ref().to_string())
1746            .collect()
1747    }
1748    /// Return (rows, cols) for a sheet if present in the Arrow store
1749    pub fn sheet_dimensions(&self, name: &str) -> Option<(u32, u32)> {
1750        self.engine
1751            .sheet_store()
1752            .sheet(name)
1753            .map(|s| (s.nrows, s.columns.len() as u32))
1754    }
1755    pub fn has_sheet(&self, name: &str) -> bool {
1756        self.engine.sheet_id(name).is_some()
1757    }
1758    pub fn add_sheet(&mut self, name: &str) -> Result<(), ExcelError> {
1759        self.engine.add_sheet(name)?;
1760        self.ensure_arrow_sheet_capacity(name, 0, 0);
1761        Ok(())
1762    }
1763    pub fn delete_sheet(&mut self, name: &str) -> Result<(), ExcelError> {
1764        if let Some(id) = self.engine.sheet_id(name) {
1765            self.engine.remove_sheet(id)?;
1766        }
1767        // Remove from Arrow store as well
1768        self.engine
1769            .sheet_store_mut()
1770            .sheets
1771            .retain(|s| s.name.as_ref() != name);
1772        Ok(())
1773    }
1774    pub fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), ExcelError> {
1775        if let Some(id) = self.engine.sheet_id(old) {
1776            self.engine.rename_sheet(id, new)?;
1777        }
1778        if let Some(asheet) = self.engine.sheet_store_mut().sheet_mut(old) {
1779            asheet.name = std::sync::Arc::<str>::from(new);
1780        }
1781        Ok(())
1782    }
1783
1784    // Cells
1785    pub fn set_value(
1786        &mut self,
1787        sheet: &str,
1788        row: u32,
1789        col: u32,
1790        value: LiteralValue,
1791    ) -> Result<(), IoError> {
1792        self.ensure_arrow_sheet_capacity(sheet, row as usize, col as usize);
1793        if self.enable_changelog {
1794            // Use VertexEditor with logging for graph, then mirror overlay and mark edited
1795            let sheet_id = self
1796                .engine
1797                .sheet_id(sheet)
1798                .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
1799            let cell = formualizer_eval::reference::CellRef::new(
1800                sheet_id,
1801                formualizer_eval::reference::Coord::from_excel(row, col, true, true),
1802            );
1803
1804            // In Arrow-canonical mode, the graph value cache is disabled, so we must capture
1805            // the old state from Arrow truth for undo/redo.
1806            let old_value = self.engine.get_cell_value(sheet, row, col);
1807            let old_formula = self
1808                .engine
1809                .get_cell(sheet, row, col)
1810                .and_then(|(ast, _)| ast);
1811
1812            self.engine.edit_with_logger(&mut self.log, |editor| {
1813                editor.set_cell_value(cell, value.clone());
1814            });
1815
1816            self.log
1817                .patch_last_cell_event_old_state(cell, old_value, old_formula);
1818            self.mirror_value_to_overlay(sheet, row, col, &value);
1819            self.engine.mark_data_edited();
1820            Ok(())
1821        } else {
1822            self.engine
1823                .set_cell_value(sheet, row, col, value)
1824                .map_err(IoError::Engine)
1825        }
1826    }
1827
1828    pub fn set_formula(
1829        &mut self,
1830        sheet: &str,
1831        row: u32,
1832        col: u32,
1833        formula: &str,
1834    ) -> Result<(), IoError> {
1835        self.ensure_arrow_sheet_capacity(sheet, row as usize, col as usize);
1836        if self.engine.config.defer_graph_building {
1837            if self.engine.get_cell(sheet, row, col).is_some() {
1838                let with_eq = if formula.starts_with('=') {
1839                    formula.to_string()
1840                } else {
1841                    format!("={formula}")
1842                };
1843                let ast = formualizer_parse::parser::parse(&with_eq)
1844                    .map_err(|e| IoError::from_backend("parser", e))?;
1845                if self.enable_changelog {
1846                    let sheet_id = self
1847                        .engine
1848                        .sheet_id(sheet)
1849                        .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
1850                    let cell = formualizer_eval::reference::CellRef::new(
1851                        sheet_id,
1852                        formualizer_eval::reference::Coord::from_excel(row, col, true, true),
1853                    );
1854
1855                    let old_value = self.engine.get_cell_value(sheet, row, col);
1856                    let old_formula = self.engine.get_cell(sheet, row, col).and_then(|(a, _)| a);
1857
1858                    self.engine.edit_with_logger(&mut self.log, |editor| {
1859                        editor.set_cell_formula(cell, ast);
1860                    });
1861
1862                    self.log
1863                        .patch_last_cell_event_old_state(cell, old_value, old_formula);
1864                    self.engine.mark_data_edited();
1865                    Ok(())
1866                } else {
1867                    self.engine
1868                        .set_cell_formula(sheet, row, col, ast)
1869                        .map_err(IoError::Engine)
1870                }
1871            } else {
1872                self.engine
1873                    .stage_formula_text(sheet, row, col, formula.to_string());
1874                Ok(())
1875            }
1876        } else {
1877            let with_eq = if formula.starts_with('=') {
1878                formula.to_string()
1879            } else {
1880                format!("={formula}")
1881            };
1882            let ast = formualizer_parse::parser::parse(&with_eq)
1883                .map_err(|e| IoError::from_backend("parser", e))?;
1884            if self.enable_changelog {
1885                let sheet_id = self
1886                    .engine
1887                    .sheet_id(sheet)
1888                    .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
1889                let cell = formualizer_eval::reference::CellRef::new(
1890                    sheet_id,
1891                    formualizer_eval::reference::Coord::from_excel(row, col, true, true),
1892                );
1893                self.engine.edit_with_logger(&mut self.log, |editor| {
1894                    editor.set_cell_formula(cell, ast);
1895                });
1896                self.engine.mark_data_edited();
1897                Ok(())
1898            } else {
1899                self.engine
1900                    .set_cell_formula(sheet, row, col, ast)
1901                    .map_err(IoError::Engine)
1902            }
1903        }
1904    }
1905
1906    pub fn get_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
1907        self.engine.get_cell_value(sheet, row, col)
1908    }
1909    pub fn get_formula(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
1910        if let Some(s) = self.engine.get_staged_formula_text(sheet, row, col) {
1911            return Some(s);
1912        }
1913        self.engine
1914            .get_cell(sheet, row, col)
1915            .and_then(|(ast, _)| ast.map(|a| formualizer_parse::pretty::canonical_formula(&a)))
1916    }
1917
1918    // Ranges
1919    pub fn read_range(&self, addr: &RangeAddress) -> Vec<Vec<LiteralValue>> {
1920        let mut out = Vec::with_capacity(addr.height() as usize);
1921        if let Some(asheet) = self.engine.sheet_store().sheet(&addr.sheet) {
1922            let sr0 = addr.start_row.saturating_sub(1) as usize;
1923            let sc0 = addr.start_col.saturating_sub(1) as usize;
1924            let er0 = addr.end_row.saturating_sub(1) as usize;
1925            let ec0 = addr.end_col.saturating_sub(1) as usize;
1926            let view = asheet.range_view(sr0, sc0, er0, ec0);
1927            let (h, w) = view.dims();
1928            for rr in 0..h {
1929                let mut row = Vec::with_capacity(w);
1930                for cc in 0..w {
1931                    row.push(view.get_cell(rr, cc));
1932                }
1933                out.push(row);
1934            }
1935        } else {
1936            // Fallback: materialize via graph stored values
1937            for r in addr.start_row..=addr.end_row {
1938                let mut row = Vec::with_capacity(addr.width() as usize);
1939                for c in addr.start_col..=addr.end_col {
1940                    row.push(
1941                        self.engine
1942                            .get_cell_value(&addr.sheet, r, c)
1943                            .unwrap_or(LiteralValue::Empty),
1944                    );
1945                }
1946                out.push(row);
1947            }
1948        }
1949        out
1950    }
1951    pub fn write_range(
1952        &mut self,
1953        sheet: &str,
1954        _start: (u32, u32),
1955        cells: BTreeMap<(u32, u32), crate::traits::CellData>,
1956    ) -> Result<(), IoError> {
1957        if self.enable_changelog {
1958            let sheet_id = self
1959                .engine
1960                .sheet_id(sheet)
1961                .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
1962            let defer_graph_building = self.engine.config.defer_graph_building;
1963
1964            // Capture per-cell old state from Arrow truth BEFORE applying the bulk edit.
1965            // In canonical mode the graph value cache is empty, so ChangeLog old_value must be patched.
1966            #[allow(clippy::type_complexity)]
1967            let mut items: Vec<(
1968                u32,
1969                u32,
1970                crate::traits::CellData,
1971                formualizer_eval::reference::CellRef,
1972                Option<LiteralValue>,
1973                Option<formualizer_parse::ASTNode>,
1974            )> = Vec::with_capacity(cells.len());
1975            for ((r, c), d) in cells.into_iter() {
1976                let cell = formualizer_eval::reference::CellRef::new(
1977                    sheet_id,
1978                    formualizer_eval::reference::Coord::from_excel(r, c, true, true),
1979                );
1980                let old_value = self.engine.get_cell_value(sheet, r, c);
1981                let old_formula = self.engine.get_cell(sheet, r, c).and_then(|(ast, _)| ast);
1982                items.push((r, c, d, cell, old_value, old_formula));
1983            }
1984
1985            let mut overlay_ops: Vec<(u32, u32, LiteralValue)> = Vec::new();
1986            let mut staged_forms: Vec<(u32, u32, String)> = Vec::new();
1987
1988            self.engine
1989                .edit_with_logger(&mut self.log, |editor| -> Result<(), IoError> {
1990                    for (r, c, d, cell, _old_value, _old_formula) in items.iter() {
1991                        if let Some(v) = d.value.clone() {
1992                            editor.set_cell_value(*cell, v.clone());
1993                            // If a formula is also being set for this cell, do not mirror the
1994                            // provided value into the delta overlay. In Arrow-truth mode that
1995                            // would mask the computed formula result.
1996                            if d.formula.is_none() {
1997                                overlay_ops.push((*r, *c, v));
1998                            }
1999                        }
2000                        if let Some(f) = d.formula.as_ref() {
2001                            if defer_graph_building {
2002                                staged_forms.push((*r, *c, f.clone()));
2003                            } else {
2004                                let with_eq = if f.starts_with('=') {
2005                                    f.clone()
2006                                } else {
2007                                    format!("={f}")
2008                                };
2009                                let ast = formualizer_parse::parser::parse(&with_eq)
2010                                    .map_err(|e| IoError::from_backend("parser", e))?;
2011                                editor.set_cell_formula(*cell, ast);
2012                            }
2013                        }
2014                    }
2015                    Ok(())
2016                })?;
2017
2018            // Patch old_value/old_formula for each cell's last SetValue/SetFormula event.
2019            for (_r, _c, _d, cell, old_value, old_formula) in items.iter().rev() {
2020                self.log.patch_last_cell_event_old_state(
2021                    *cell,
2022                    old_value.clone(),
2023                    old_formula.clone(),
2024                );
2025            }
2026
2027            for (r, c, v) in overlay_ops {
2028                self.mirror_value_to_overlay(sheet, r, c, &v);
2029            }
2030            for (r, c, f) in staged_forms {
2031                self.engine.stage_formula_text(sheet, r, c, f);
2032            }
2033            self.engine.mark_data_edited();
2034            Ok(())
2035        } else {
2036            for ((r, c), d) in cells.into_iter() {
2037                if let Some(v) = d.value.clone() {
2038                    self.engine
2039                        .set_cell_value(sheet, r, c, v)
2040                        .map_err(IoError::Engine)?;
2041                }
2042                if let Some(f) = d.formula.as_ref() {
2043                    if self.engine.config.defer_graph_building {
2044                        self.engine.stage_formula_text(sheet, r, c, f.clone());
2045                    } else {
2046                        let with_eq = if f.starts_with('=') {
2047                            f.clone()
2048                        } else {
2049                            format!("={f}")
2050                        };
2051                        let ast = formualizer_parse::parser::parse(&with_eq)
2052                            .map_err(|e| IoError::from_backend("parser", e))?;
2053                        self.engine
2054                            .set_cell_formula(sheet, r, c, ast)
2055                            .map_err(IoError::Engine)?;
2056                    }
2057                }
2058            }
2059            Ok(())
2060        }
2061    }
2062
2063    // Batch set values in a rectangle starting at (start_row,start_col)
2064    pub fn set_values(
2065        &mut self,
2066        sheet: &str,
2067        start_row: u32,
2068        start_col: u32,
2069        rows: &[Vec<LiteralValue>],
2070    ) -> Result<(), IoError> {
2071        if self.enable_changelog {
2072            let sheet_id = self
2073                .engine
2074                .sheet_id(sheet)
2075                .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
2076
2077            // Capture old state from Arrow truth BEFORE applying the batch.
2078            #[allow(clippy::type_complexity)]
2079            let mut items: Vec<(
2080                u32,
2081                u32,
2082                LiteralValue,
2083                formualizer_eval::reference::CellRef,
2084                Option<LiteralValue>,
2085                Option<formualizer_parse::ASTNode>,
2086            )> = Vec::new();
2087            for (ri, rvals) in rows.iter().enumerate() {
2088                let r = start_row + ri as u32;
2089                for (ci, v) in rvals.iter().enumerate() {
2090                    let c = start_col + ci as u32;
2091                    let cell = formualizer_eval::reference::CellRef::new(
2092                        sheet_id,
2093                        formualizer_eval::reference::Coord::from_excel(r, c, true, true),
2094                    );
2095                    let old_value = self.engine.get_cell_value(sheet, r, c);
2096                    let old_formula = self.engine.get_cell(sheet, r, c).and_then(|(ast, _)| ast);
2097                    items.push((r, c, v.clone(), cell, old_value, old_formula));
2098                }
2099            }
2100
2101            self.engine.edit_with_logger(&mut self.log, |editor| {
2102                for (_r, _c, v, cell, _old_value, _old_formula) in items.iter() {
2103                    editor.set_cell_value(*cell, v.clone());
2104                }
2105            });
2106
2107            for (_r, _c, _v, cell, old_value, old_formula) in items.iter().rev() {
2108                self.log.patch_last_cell_event_old_state(
2109                    *cell,
2110                    old_value.clone(),
2111                    old_formula.clone(),
2112                );
2113            }
2114
2115            for (r, c, v, _cell, _old_value, _old_formula) in items {
2116                self.mirror_value_to_overlay(sheet, r, c, &v);
2117            }
2118            self.engine.mark_data_edited();
2119            Ok(())
2120        } else {
2121            for (ri, rvals) in rows.iter().enumerate() {
2122                let r = start_row + ri as u32;
2123                for (ci, v) in rvals.iter().enumerate() {
2124                    let c = start_col + ci as u32;
2125                    self.engine
2126                        .set_cell_value(sheet, r, c, v.clone())
2127                        .map_err(IoError::Engine)?;
2128                }
2129            }
2130            Ok(())
2131        }
2132    }
2133
2134    // Batch set formulas in a rectangle starting at (start_row,start_col)
2135    pub fn set_formulas(
2136        &mut self,
2137        sheet: &str,
2138        start_row: u32,
2139        start_col: u32,
2140        rows: &[Vec<String>],
2141    ) -> Result<(), IoError> {
2142        let height = rows.len();
2143        let width = rows.iter().map(|r| r.len()).max().unwrap_or(0);
2144        if height == 0 || width == 0 {
2145            return Ok(());
2146        }
2147        let end_row = start_row.saturating_add((height - 1) as u32);
2148        let end_col = start_col.saturating_add((width - 1) as u32);
2149        self.ensure_arrow_sheet_capacity(sheet, end_row as usize, end_col as usize);
2150
2151        if self.engine.config.defer_graph_building {
2152            for (ri, rforms) in rows.iter().enumerate() {
2153                let r = start_row + ri as u32;
2154                for (ci, f) in rforms.iter().enumerate() {
2155                    let c = start_col + ci as u32;
2156                    self.engine.stage_formula_text(sheet, r, c, f.clone());
2157                }
2158            }
2159            Ok(())
2160        } else if self.enable_changelog {
2161            let sheet_id = self
2162                .engine
2163                .sheet_id(sheet)
2164                .unwrap_or_else(|| self.engine.add_sheet(sheet).expect("add sheet"));
2165
2166            self.engine
2167                .edit_with_logger(&mut self.log, |editor| -> Result<(), IoError> {
2168                    for (ri, rforms) in rows.iter().enumerate() {
2169                        let r = start_row + ri as u32;
2170                        for (ci, f) in rforms.iter().enumerate() {
2171                            let c = start_col + ci as u32;
2172                            let cell = formualizer_eval::reference::CellRef::new(
2173                                sheet_id,
2174                                formualizer_eval::reference::Coord::from_excel(r, c, true, true),
2175                            );
2176                            let with_eq = if f.starts_with('=') {
2177                                f.clone()
2178                            } else {
2179                                format!("={f}")
2180                            };
2181                            let ast = formualizer_parse::parser::parse(&with_eq)
2182                                .map_err(|e| IoError::from_backend("parser", e))?;
2183                            editor.set_cell_formula(cell, ast);
2184                        }
2185                    }
2186                    Ok(())
2187                })?;
2188
2189            self.engine.mark_data_edited();
2190            Ok(())
2191        } else {
2192            for (ri, rforms) in rows.iter().enumerate() {
2193                let r = start_row + ri as u32;
2194                for (ci, f) in rforms.iter().enumerate() {
2195                    let c = start_col + ci as u32;
2196                    let with_eq = if f.starts_with('=') {
2197                        f.clone()
2198                    } else {
2199                        format!("={f}")
2200                    };
2201                    let ast = formualizer_parse::parser::parse(&with_eq)
2202                        .map_err(|e| IoError::from_backend("parser", e))?;
2203                    self.engine
2204                        .set_cell_formula(sheet, r, c, ast)
2205                        .map_err(IoError::Engine)?;
2206                }
2207            }
2208            Ok(())
2209        }
2210    }
2211
2212    // Evaluation
2213    pub fn prepare_graph_all(&mut self) -> Result<(), IoError> {
2214        self.engine
2215            .build_graph_all()
2216            .map_err(|e| IoError::from_backend("parser", e))
2217    }
2218    pub fn prepare_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
2219        &mut self,
2220        sheets: I,
2221    ) -> Result<(), IoError> {
2222        self.engine
2223            .build_graph_for_sheets(sheets)
2224            .map_err(|e| IoError::from_backend("parser", e))
2225    }
2226    pub fn evaluate_cell(
2227        &mut self,
2228        sheet: &str,
2229        row: u32,
2230        col: u32,
2231    ) -> Result<LiteralValue, IoError> {
2232        self.engine
2233            .evaluate_cell(sheet, row, col)
2234            .map_err(IoError::Engine)
2235            .map(|value| value.unwrap_or(LiteralValue::Empty))
2236    }
2237    pub fn evaluate_cells(
2238        &mut self,
2239        targets: &[(&str, u32, u32)],
2240    ) -> Result<Vec<LiteralValue>, IoError> {
2241        self.engine
2242            .evaluate_cells(targets)
2243            .map_err(IoError::Engine)
2244            .map(|values| {
2245                values
2246                    .into_iter()
2247                    .map(|v| v.unwrap_or(LiteralValue::Empty))
2248                    .collect()
2249            })
2250    }
2251
2252    pub fn evaluate_cells_cancellable(
2253        &mut self,
2254        targets: &[(&str, u32, u32)],
2255        cancel_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
2256    ) -> Result<Vec<LiteralValue>, IoError> {
2257        self.engine
2258            .evaluate_cells_cancellable(targets, cancel_flag)
2259            .map_err(IoError::Engine)
2260            .map(|values| {
2261                values
2262                    .into_iter()
2263                    .map(|v| v.unwrap_or(LiteralValue::Empty))
2264                    .collect()
2265            })
2266    }
2267    pub fn evaluate_all(&mut self) -> Result<formualizer_eval::engine::EvalResult, IoError> {
2268        self.engine.evaluate_all().map_err(IoError::Engine)
2269    }
2270
2271    pub fn evaluate_all_cancellable(
2272        &mut self,
2273        cancel_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
2274    ) -> Result<formualizer_eval::engine::EvalResult, IoError> {
2275        self.engine
2276            .evaluate_all_cancellable(cancel_flag)
2277            .map_err(IoError::Engine)
2278    }
2279
2280    pub fn evaluate_with_plan(
2281        &mut self,
2282        plan: &formualizer_eval::engine::RecalcPlan,
2283    ) -> Result<formualizer_eval::engine::EvalResult, IoError> {
2284        self.engine
2285            .evaluate_recalc_plan(plan)
2286            .map_err(IoError::Engine)
2287    }
2288
2289    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, IoError> {
2290        self.engine.get_eval_plan(targets).map_err(IoError::Engine)
2291    }
2292
2293    // Named ranges
2294    pub fn define_named_range(
2295        &mut self,
2296        name: &str,
2297        address: &RangeAddress,
2298        scope: crate::traits::NamedRangeScope,
2299    ) -> Result<(), IoError> {
2300        let (definition, scope) = self.named_definition_with_scope(address, scope)?;
2301        if self.enable_changelog {
2302            let result = self.engine.edit_with_logger(&mut self.log, |editor| {
2303                editor.define_name(name, definition, scope)
2304            });
2305            result.map_err(|e| IoError::from_backend("editor", e))
2306        } else {
2307            self.engine
2308                .define_name(name, definition, scope)
2309                .map_err(IoError::Engine)
2310        }
2311    }
2312
2313    pub fn update_named_range(
2314        &mut self,
2315        name: &str,
2316        address: &RangeAddress,
2317        scope: crate::traits::NamedRangeScope,
2318    ) -> Result<(), IoError> {
2319        let (definition, scope) = self.named_definition_with_scope(address, scope)?;
2320        if self.enable_changelog {
2321            let result = self.engine.edit_with_logger(&mut self.log, |editor| {
2322                editor.update_name(name, definition, scope)
2323            });
2324            result.map_err(|e| IoError::from_backend("editor", e))
2325        } else {
2326            self.engine
2327                .update_name(name, definition, scope)
2328                .map_err(IoError::Engine)
2329        }
2330    }
2331
2332    pub fn delete_named_range(
2333        &mut self,
2334        name: &str,
2335        scope: crate::traits::NamedRangeScope,
2336        sheet: Option<&str>,
2337    ) -> Result<(), IoError> {
2338        let scope = self.name_scope_from_hint(scope, sheet)?;
2339        if self.enable_changelog {
2340            let result = self
2341                .engine
2342                .edit_with_logger(&mut self.log, |editor| editor.delete_name(name, scope));
2343            result.map_err(|e| IoError::from_backend("editor", e))
2344        } else {
2345            self.engine
2346                .delete_name(name, scope)
2347                .map_err(IoError::Engine)
2348        }
2349    }
2350
2351    /// Resolve a named range (workbook-scoped or unique sheet-scoped) to an absolute address.
2352    pub fn named_range_address(&self, name: &str) -> Option<RangeAddress> {
2353        if let Some((_, named)) = self
2354            .engine
2355            .named_ranges_iter()
2356            .find(|(n, _)| n.as_str() == name)
2357        {
2358            return self.named_definition_to_address(&named.definition);
2359        }
2360
2361        let mut resolved: Option<RangeAddress> = None;
2362        for ((_sheet_id, candidate), named) in self.engine.sheet_named_ranges_iter() {
2363            if candidate == name
2364                && let Some(address) = self.named_definition_to_address(&named.definition)
2365            {
2366                if resolved.is_some() {
2367                    return None; // ambiguous sheet-scoped name
2368                }
2369                resolved = Some(address);
2370            }
2371        }
2372        resolved
2373    }
2374
2375    fn named_definition_with_scope(
2376        &mut self,
2377        address: &RangeAddress,
2378        scope: crate::traits::NamedRangeScope,
2379    ) -> Result<(NamedDefinition, NameScope), IoError> {
2380        let sheet_id = self.ensure_sheet_for_address(address)?;
2381        let scope = match scope {
2382            crate::traits::NamedRangeScope::Workbook => NameScope::Workbook,
2383            crate::traits::NamedRangeScope::Sheet => NameScope::Sheet(sheet_id),
2384        };
2385        let sr0 = address.start_row.saturating_sub(1);
2386        let sc0 = address.start_col.saturating_sub(1);
2387        let er0 = address.end_row.saturating_sub(1);
2388        let ec0 = address.end_col.saturating_sub(1);
2389        let start_ref = formualizer_eval::reference::CellRef::new(
2390            sheet_id,
2391            formualizer_eval::reference::Coord::new(sr0, sc0, true, true),
2392        );
2393        if sr0 == er0 && sc0 == ec0 {
2394            Ok((NamedDefinition::Cell(start_ref), scope))
2395        } else {
2396            let end_ref = formualizer_eval::reference::CellRef::new(
2397                sheet_id,
2398                formualizer_eval::reference::Coord::new(er0, ec0, true, true),
2399            );
2400            let range_ref = formualizer_eval::reference::RangeRef::new(start_ref, end_ref);
2401            Ok((NamedDefinition::Range(range_ref), scope))
2402        }
2403    }
2404
2405    fn name_scope_from_hint(
2406        &mut self,
2407        scope: crate::traits::NamedRangeScope,
2408        sheet: Option<&str>,
2409    ) -> Result<NameScope, IoError> {
2410        match scope {
2411            crate::traits::NamedRangeScope::Workbook => Ok(NameScope::Workbook),
2412            crate::traits::NamedRangeScope::Sheet => {
2413                let sheet = sheet.ok_or_else(|| IoError::Backend {
2414                    backend: "workbook".to_string(),
2415                    message: "Sheet scope requires a sheet name".to_string(),
2416                })?;
2417                let sheet_id = self
2418                    .engine
2419                    .sheet_id(sheet)
2420                    .ok_or_else(|| IoError::Backend {
2421                        backend: "workbook".to_string(),
2422                        message: "Sheet not found".to_string(),
2423                    })?;
2424                Ok(NameScope::Sheet(sheet_id))
2425            }
2426        }
2427    }
2428
2429    fn ensure_sheet_for_address(
2430        &mut self,
2431        address: &RangeAddress,
2432    ) -> Result<formualizer_eval::SheetId, IoError> {
2433        let sheet_id = self
2434            .engine
2435            .sheet_id(&address.sheet)
2436            .or_else(|| self.engine.add_sheet(&address.sheet).ok())
2437            .ok_or_else(|| IoError::Backend {
2438                backend: "workbook".to_string(),
2439                message: "Sheet not found".to_string(),
2440            })?;
2441        self.ensure_arrow_sheet_capacity(
2442            &address.sheet,
2443            address.end_row as usize,
2444            address.end_col as usize,
2445        );
2446        Ok(sheet_id)
2447    }
2448
2449    fn named_definition_to_address(&self, definition: &NamedDefinition) -> Option<RangeAddress> {
2450        match definition {
2451            NamedDefinition::Cell(cell) => {
2452                let sheet = self.engine.sheet_name(cell.sheet_id).to_string();
2453                let row = cell.coord.row() + 1;
2454                let col = cell.coord.col() + 1;
2455                RangeAddress::new(sheet, row, col, row, col).ok()
2456            }
2457            NamedDefinition::Range(range) => {
2458                if range.start.sheet_id != range.end.sheet_id {
2459                    return None;
2460                }
2461                let sheet = self.engine.sheet_name(range.start.sheet_id).to_string();
2462                let start_row = range.start.coord.row() + 1;
2463                let start_col = range.start.coord.col() + 1;
2464                let end_row = range.end.coord.row() + 1;
2465                let end_col = range.end.coord.col() + 1;
2466                RangeAddress::new(sheet, start_row, start_col, end_row, end_col).ok()
2467            }
2468            NamedDefinition::Literal(_) => None,
2469            NamedDefinition::Formula { .. } => {
2470                #[cfg(feature = "tracing")]
2471                tracing::debug!("formula-backed named ranges are not yet supported");
2472                None
2473            }
2474        }
2475    }
2476
2477    // Persistence/transactions via SpreadsheetWriter (self implements writer)
2478    pub fn begin_tx<'a, W: SpreadsheetWriter>(
2479        &'a mut self,
2480        writer: &'a mut W,
2481    ) -> crate::transaction::WriteTransaction<'a, W> {
2482        crate::transaction::WriteTransaction::new(writer)
2483    }
2484
2485    // Loading via streaming ingest (Arrow base + graph formulas)
2486    pub fn from_reader<B>(
2487        mut backend: B,
2488        _strategy: LoadStrategy,
2489        config: WorkbookConfig,
2490    ) -> Result<Self, IoError>
2491    where
2492        B: SpreadsheetReader + formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>,
2493        IoError: From<<B as formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>>::Error>,
2494    {
2495        let mut wb = Self::new_with_config(config);
2496        backend
2497            .stream_into_engine(&mut wb.engine)
2498            .map_err(IoError::from)?;
2499        Ok(wb)
2500    }
2501
2502    pub fn from_reader_with_config<B>(
2503        backend: B,
2504        strategy: LoadStrategy,
2505        config: WorkbookConfig,
2506    ) -> Result<Self, IoError>
2507    where
2508        B: SpreadsheetReader + formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>,
2509        IoError: From<<B as formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>>::Error>,
2510    {
2511        Self::from_reader(backend, strategy, config)
2512    }
2513
2514    pub fn from_reader_with_mode<B>(
2515        backend: B,
2516        strategy: LoadStrategy,
2517        mode: WorkbookMode,
2518    ) -> Result<Self, IoError>
2519    where
2520        B: SpreadsheetReader + formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>,
2521        IoError: From<<B as formualizer_eval::engine::ingest::EngineLoadStream<WBResolver>>::Error>,
2522    {
2523        let config = match mode {
2524            WorkbookMode::Ephemeral => WorkbookConfig::ephemeral(),
2525            WorkbookMode::Interactive => WorkbookConfig::interactive(),
2526        };
2527        Self::from_reader(backend, strategy, config)
2528    }
2529}
2530
2531// Implement SpreadsheetWriter so external transactions can target Workbook
2532impl SpreadsheetWriter for Workbook {
2533    type Error = IoError;
2534
2535    fn write_cell(
2536        &mut self,
2537        sheet: &str,
2538        row: u32,
2539        col: u32,
2540        data: crate::traits::CellData,
2541    ) -> Result<(), Self::Error> {
2542        if let Some(v) = data.value {
2543            self.set_value(sheet, row, col, v)?;
2544        }
2545        if let Some(f) = data.formula {
2546            self.set_formula(sheet, row, col, &f)?;
2547        }
2548        Ok(())
2549    }
2550    fn write_range(
2551        &mut self,
2552        sheet: &str,
2553        cells: BTreeMap<(u32, u32), crate::traits::CellData>,
2554    ) -> Result<(), Self::Error> {
2555        for ((r, c), d) in cells {
2556            self.write_cell(sheet, r, c, d)?;
2557        }
2558        Ok(())
2559    }
2560    fn clear_range(
2561        &mut self,
2562        sheet: &str,
2563        start: (u32, u32),
2564        end: (u32, u32),
2565    ) -> Result<(), Self::Error> {
2566        for r in start.0..=end.0 {
2567            for c in start.1..=end.1 {
2568                self.set_value(sheet, r, c, LiteralValue::Empty)?;
2569            }
2570        }
2571        Ok(())
2572    }
2573    fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
2574        self.add_sheet(name).map_err(IoError::Engine)
2575    }
2576    fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
2577        self.delete_sheet(name).map_err(IoError::Engine)
2578    }
2579    fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error> {
2580        self.rename_sheet(old, new).map_err(IoError::Engine)
2581    }
2582    fn flush(&mut self) -> Result<(), Self::Error> {
2583        Ok(())
2584    }
2585}