Skip to main content

formualizer_eval/
function_registry.rs

1use crate::function::{FnCaps, Function};
2use crate::function_contract::{
3    FunctionDependencySemantics, FunctionEnvironmentSemantics, FunctionEvaluationSemantics,
4    FunctionResultSemantics, FunctionSemanticContract, FunctionSemanticIdentity,
5};
6use once_cell::sync::Lazy;
7use std::collections::{HashMap, VecDeque};
8use std::panic::{AssertUnwindSafe, catch_unwind};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::{Arc, RwLock};
11
12type RegistryKey = (String, String);
13
14#[derive(Clone)]
15struct RegistryEntry {
16    function: Arc<dyn Function>,
17    generation: u64,
18    trusted_builtin: bool,
19    semantics: SemanticContractResolution,
20    semantics_by_arity: Arc<RwLock<HashMap<usize, SemanticContractResolution>>>,
21}
22
23#[derive(Clone)]
24struct AliasEntry {
25    target: RegistryKey,
26    owner: Option<(RegistryKey, u64)>,
27}
28
29struct RegistryState {
30    registrations: HashMap<RegistryKey, RegistryEntry>,
31    aliases: HashMap<RegistryKey, AliasEntry>,
32    semantic_epoch: u64,
33    semantic_changes: VecDeque<(u64, Vec<RegistryKey>)>,
34}
35
36impl Default for RegistryState {
37    fn default() -> Self {
38        Self {
39            registrations: HashMap::new(),
40            aliases: HashMap::new(),
41            semantic_epoch: 1,
42            semantic_changes: VecDeque::new(),
43        }
44    }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum SemanticConformanceIssue {
50    CapabilityPanicked,
51    DependencyContractPanicked,
52    SemanticContractPanicked,
53    ArityMetadataPanicked,
54    VariadicMetadataPanicked,
55    AliasMetadataPanicked,
56    ArgumentSchemaPanicked,
57    AritySchemaMismatch,
58    DynamicDependencyMismatch,
59    ShortCircuitMismatch,
60    ReferenceResultMismatch,
61    LocalEnvironmentMismatch,
62    SpillResultMismatch,
63    PrecisionContractMismatch,
64    PrecisionContractInvalid,
65}
66
67#[derive(Clone, Debug)]
68#[non_exhaustive]
69pub struct SemanticContractResolution {
70    pub contract: Option<FunctionSemanticContract>,
71    pub generation: u64,
72    pub trusted_builtin: bool,
73    pub issues: Vec<SemanticConformanceIssue>,
74}
75
76impl SemanticContractResolution {
77    pub fn conforms(&self) -> bool {
78        self.contract.is_some() && self.issues.is_empty()
79    }
80}
81
82#[derive(Clone)]
83#[non_exhaustive]
84pub struct ResolvedFunction {
85    pub namespace: String,
86    pub canonical_name: String,
87    pub function: Arc<dyn Function>,
88    pub semantics: SemanticContractResolution,
89}
90
91static REGISTRY: Lazy<RwLock<RegistryState>> = Lazy::new(|| RwLock::new(RegistryState::default()));
92static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
93
94#[inline]
95fn norm<S: AsRef<str>>(s: S) -> String {
96    s.as_ref().to_uppercase()
97}
98
99pub fn semantic_epoch() -> u64 {
100    REGISTRY
101        .read()
102        .unwrap_or_else(|poisoned| poisoned.into_inner())
103        .semantic_epoch
104}
105
106pub(crate) struct SemanticEpochReadGuard(std::sync::RwLockReadGuard<'static, RegistryState>);
107
108impl SemanticEpochReadGuard {
109    pub(crate) fn epoch(&self) -> u64 {
110        self.0.semantic_epoch
111    }
112
113    pub(crate) fn semantic_changes_affect_requests_since(
114        &self,
115        epoch: u64,
116        requests: impl IntoIterator<Item = (String, String, usize)>,
117    ) -> bool {
118        semantic_changes_affect_requests_in_state(&self.0, epoch, requests)
119    }
120}
121
122pub(crate) fn semantic_epoch_read_guard() -> SemanticEpochReadGuard {
123    SemanticEpochReadGuard(
124        REGISTRY
125            .read()
126            .unwrap_or_else(|poisoned| poisoned.into_inner()),
127    )
128}
129
130pub(crate) struct SemanticChanges {
131    pub(crate) epoch: u64,
132    pub(crate) complete: bool,
133    pub(crate) keys: Vec<(String, String)>,
134}
135
136fn publish_semantic_change(state: &mut RegistryState, keys: impl IntoIterator<Item = RegistryKey>) {
137    state.semantic_epoch = state.semantic_epoch.saturating_add(1);
138    let epoch = state.semantic_epoch;
139    state
140        .semantic_changes
141        .push_back((epoch, keys.into_iter().collect()));
142    if state.semantic_changes.len() > 1_024 {
143        state.semantic_changes.pop_front();
144    }
145}
146
147pub(crate) fn semantic_changes_since(epoch: u64) -> SemanticChanges {
148    let state = REGISTRY
149        .read()
150        .unwrap_or_else(|poisoned| poisoned.into_inner());
151    semantic_changes_since_in_state(&state, epoch)
152}
153
154fn semantic_changes_since_in_state(state: &RegistryState, epoch: u64) -> SemanticChanges {
155    let complete = state
156        .semantic_changes
157        .front()
158        .is_none_or(|(oldest, _)| epoch.saturating_add(1) >= *oldest);
159    let keys = state
160        .semantic_changes
161        .iter()
162        .filter(|(changed_epoch, _)| *changed_epoch > epoch)
163        .flat_map(|(_, keys)| keys.iter().cloned())
164        .collect();
165    SemanticChanges {
166        epoch: state.semantic_epoch,
167        complete,
168        keys,
169    }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
173#[non_exhaustive]
174pub enum RegistrationError {
175    NameMetadataPanicked,
176    NamespaceMetadataPanicked,
177}
178
179pub fn try_register_function(function: Arc<dyn Function>) -> Result<(), RegistrationError> {
180    register(function, false)
181}
182
183pub fn register_function(function: Arc<dyn Function>) {
184    let _ = try_register_function(function);
185}
186pub(crate) fn register_builtin(function: Arc<dyn Function>) {
187    register(function, true).expect("builtin name and namespace metadata must not panic");
188}
189
190fn register(function: Arc<dyn Function>, trusted_builtin: bool) -> Result<(), RegistrationError> {
191    let namespace = catch_unwind(AssertUnwindSafe(|| function.namespace()))
192        .map_err(|_| RegistrationError::NamespaceMetadataPanicked)?;
193    let name = catch_unwind(AssertUnwindSafe(|| function.name()))
194        .map_err(|_| RegistrationError::NameMetadataPanicked)?;
195    let key = (norm(namespace), norm(name));
196    let generation = NEXT_GENERATION.fetch_add(1, Ordering::Relaxed);
197    let aliases = catch_unwind(AssertUnwindSafe(|| function.aliases().to_vec()));
198    let min_args = catch_unwind(AssertUnwindSafe(|| function.min_args()));
199    let initial_arity = min_args.as_ref().copied().unwrap_or(0);
200    let mut semantics = match min_args {
201        Ok(arity) => inspect_semantics(&function, trusted_builtin, generation, arity),
202        Err(_) => failed_resolution(
203            generation,
204            trusted_builtin,
205            SemanticConformanceIssue::ArityMetadataPanicked,
206        ),
207    };
208    let aliases = match aliases {
209        Ok(aliases) => aliases,
210        Err(_) => {
211            semantics
212                .issues
213                .push(SemanticConformanceIssue::AliasMetadataPanicked);
214            semantics.contract = None;
215            Vec::new()
216        }
217    };
218
219    let mut state = REGISTRY
220        .write()
221        .unwrap_or_else(|poisoned| poisoned.into_inner());
222    if trusted_builtin
223        && state
224            .registrations
225            .get(&key)
226            .is_some_and(|entry| entry.trusted_builtin)
227    {
228        return Ok(());
229    }
230    let previous = state
231        .registrations
232        .get(&key)
233        .map(|entry| (entry.generation, entry.trusted_builtin));
234    let mut changed_spellings = Vec::new();
235    if let Some((previous_generation, _)) = previous {
236        changed_spellings.extend(
237            state
238                .aliases
239                .iter()
240                .filter(|(_, alias)| {
241                    alias.owner.as_ref() == Some(&(key.clone(), previous_generation))
242                })
243                .map(|(alias_key, _)| alias_key.clone()),
244        );
245        state
246            .aliases
247            .retain(|_, alias| alias.owner.as_ref() != Some(&(key.clone(), previous_generation)));
248    }
249    state.registrations.insert(
250        key.clone(),
251        RegistryEntry {
252            function: Arc::clone(&function),
253            generation,
254            trusted_builtin,
255            semantics: semantics.clone(),
256            semantics_by_arity: Arc::new(RwLock::new(HashMap::from([(initial_arity, semantics)]))),
257        },
258    );
259    for alias in aliases {
260        if !alias.eq_ignore_ascii_case(&key.1) {
261            let alias_key = (key.0.clone(), norm(alias));
262            changed_spellings.push(alias_key.clone());
263            state.aliases.insert(
264                alias_key,
265                AliasEntry {
266                    target: key.clone(),
267                    owner: Some((key.clone(), generation)),
268                },
269            );
270        }
271    }
272    changed_spellings.push(key);
273    publish_semantic_change(&mut state, changed_spellings);
274    Ok(())
275}
276
277fn failed_resolution(
278    generation: u64,
279    trusted_builtin: bool,
280    issue: SemanticConformanceIssue,
281) -> SemanticContractResolution {
282    SemanticContractResolution {
283        contract: None,
284        generation,
285        trusted_builtin,
286        issues: vec![issue],
287    }
288}
289
290fn inspect_semantics(
291    function: &Arc<dyn Function>,
292    trusted_builtin: bool,
293    generation: u64,
294    arity: usize,
295) -> SemanticContractResolution {
296    inspect_semantics_with_identity_metadata(function, trusted_builtin, generation, arity).0
297}
298
299fn inspect_semantics_with_identity_metadata(
300    function: &Arc<dyn Function>,
301    trusted_builtin: bool,
302    generation: u64,
303    arity: usize,
304) -> (SemanticContractResolution, Option<(FnCaps, Vec<bool>)>) {
305    let mut issues = Vec::new();
306    let inspected_caps = inspected(
307        &mut issues,
308        SemanticConformanceIssue::CapabilityPanicked,
309        || function.caps(),
310    );
311    let caps = inspected_caps.unwrap_or_else(FnCaps::empty);
312    let precision = inspected(
313        &mut issues,
314        SemanticConformanceIssue::DependencyContractPanicked,
315        || function.dependency_contract(arity),
316    )
317    .flatten();
318    let explicit = inspected(
319        &mut issues,
320        SemanticConformanceIssue::SemanticContractPanicked,
321        || function.semantic_contract(arity),
322    )
323    .flatten();
324    let schema = inspected(
325        &mut issues,
326        SemanticConformanceIssue::ArgumentSchemaPanicked,
327        || function.arg_schema(),
328    );
329    let min_args = inspected(
330        &mut issues,
331        SemanticConformanceIssue::ArityMetadataPanicked,
332        || function.min_args(),
333    );
334    let variadic = inspected(
335        &mut issues,
336        SemanticConformanceIssue::VariadicMetadataPanicked,
337        || function.variadic(),
338    );
339    if let (Some(schema), Some(min_args), Some(variadic)) = (schema, min_args, variadic)
340        && !schema_allows_arity(schema, min_args, variadic, arity, !trusted_builtin)
341    {
342        issues.push(SemanticConformanceIssue::AritySchemaMismatch);
343    }
344    let contract =
345        explicit.or_else(|| trusted_builtin.then(|| trusted_contract_from_caps(caps, precision)));
346    if let Some(contract) = contract {
347        if contract.precision != precision {
348            issues.push(SemanticConformanceIssue::PrecisionContractMismatch);
349        }
350        if !precision_is_valid(contract, arity) {
351            issues.push(SemanticConformanceIssue::PrecisionContractInvalid);
352        }
353        check_capability(
354            &mut issues,
355            caps.contains(FnCaps::DYNAMIC_DEPENDENCY),
356            contract.dependency == FunctionDependencySemantics::Dynamic,
357            SemanticConformanceIssue::DynamicDependencyMismatch,
358        );
359        check_capability(
360            &mut issues,
361            caps.contains(FnCaps::SHORT_CIRCUIT),
362            contract.evaluation == FunctionEvaluationSemantics::ShortCircuit,
363            SemanticConformanceIssue::ShortCircuitMismatch,
364        );
365        check_capability(
366            &mut issues,
367            caps.contains(FnCaps::RETURNS_REFERENCE),
368            contract.result.may_return_reference(),
369            SemanticConformanceIssue::ReferenceResultMismatch,
370        );
371        check_capability(
372            &mut issues,
373            caps.contains(FnCaps::LOCAL_ENVIRONMENT),
374            contract.environment == FunctionEnvironmentSemantics::LocalBindings,
375            SemanticConformanceIssue::LocalEnvironmentMismatch,
376        );
377        check_capability(
378            &mut issues,
379            caps.contains(FnCaps::MAY_SPILL),
380            contract.result.may_spill(),
381            SemanticConformanceIssue::SpillResultMismatch,
382        );
383    }
384    let identity_metadata = inspected_caps.zip(schema).map(|(caps, schema)| {
385        let repeating = schema.iter().find(|argument| argument.repeating.is_some());
386        let argument_by_ref = (0..arity)
387            .map(|index| {
388                schema
389                    .get(index)
390                    .or(repeating)
391                    .is_some_and(|argument| argument.by_ref)
392            })
393            .collect();
394        (caps, argument_by_ref)
395    });
396    (
397        SemanticContractResolution {
398            contract: issues.is_empty().then_some(contract).flatten(),
399            generation,
400            trusted_builtin,
401            issues,
402        },
403        identity_metadata,
404    )
405}
406
407fn precision_is_valid(contract: FunctionSemanticContract, arity: usize) -> bool {
408    use crate::function_contract::{
409        CriteriaValueRange, FunctionArgumentDependencyContract as Arguments,
410        FunctionArgumentDependencyRole as Role,
411    };
412    let Some(precision) = contract.precision else {
413        return true;
414    };
415    if !precision.arity.allows(arity)
416        || contract.dependency != FunctionDependencySemantics::RecursiveSyntacticArgs
417    {
418        return false;
419    }
420    match precision.arguments {
421        Arguments::AllArgs(role) | Arguments::Variadic(role) => {
422            !matches!(role, Role::IgnoredLiteral | Role::Unsupported)
423        }
424        Arguments::CriteriaPairs(criteria) => {
425            let value_valid = match criteria.value_range {
426                CriteriaValueRange::None => true,
427                CriteriaValueRange::Fixed(index) => index < arity,
428                CriteriaValueRange::Optional {
429                    provided_index,
430                    fallback_criteria_range_index,
431                } => provided_index <= arity && fallback_criteria_range_index < arity,
432            };
433            let pair_end = match criteria.value_range {
434                CriteriaValueRange::Fixed(index) if index >= criteria.first_criteria_pair => index,
435                CriteriaValueRange::Optional { provided_index, .. }
436                    if provided_index >= criteria.first_criteria_pair =>
437                {
438                    provided_index
439                }
440                _ => arity,
441            };
442            value_valid
443                && criteria.first_criteria_pair < pair_end
444                && (pair_end - criteria.first_criteria_pair).is_multiple_of(2)
445        }
446        Arguments::LocalBindingPairs => {
447            contract.environment == FunctionEnvironmentSemantics::LocalBindings
448                && arity >= 3
449                && !arity.is_multiple_of(2)
450        }
451        Arguments::LambdaParameters => {
452            contract.environment == FunctionEnvironmentSemantics::LocalBindings && arity >= 1
453        }
454    }
455}
456
457fn schema_allows_arity(
458    schema: &[crate::args::ArgSchema],
459    min_args: usize,
460    variadic: bool,
461    arity: usize,
462    strict_required_count: bool,
463) -> bool {
464    if schema.is_empty() {
465        return min_args == 0 && arity == 0;
466    }
467
468    let mut optional_seen = false;
469    let mut required_count = 0usize;
470    let mut repeating = None;
471    for (index, argument) in schema.iter().enumerate() {
472        if argument.required {
473            if optional_seen {
474                return false;
475            }
476            required_count += 1;
477        } else {
478            optional_seen = true;
479        }
480        if let Some(width) = argument.repeating {
481            if width == 0 || repeating.is_some() || index + 1 != schema.len() {
482                return false;
483            }
484            repeating = Some(width);
485        }
486    }
487    let represented_minimum = min_args.min(schema.len());
488    if (strict_required_count && required_count != min_args)
489        || (!strict_required_count && required_count < represented_minimum)
490        || schema
491            .iter()
492            .take(represented_minimum)
493            .any(|argument| !argument.required)
494        || (!variadic && schema.len() > 1 && min_args > schema.len())
495        || arity < min_args
496    {
497        return false;
498    }
499    if let Some(width) = repeating {
500        if width > schema.len() {
501            return false;
502        }
503        let fixed_prefix = schema.len() - width;
504        return arity >= schema.len() && (arity - fixed_prefix).is_multiple_of(width);
505    }
506    if variadic {
507        return true;
508    }
509    arity <= schema.len().max(min_args)
510}
511
512fn inspected<T>(
513    issues: &mut Vec<SemanticConformanceIssue>,
514    issue: SemanticConformanceIssue,
515    inspect: impl FnOnce() -> T,
516) -> Option<T> {
517    match catch_unwind(AssertUnwindSafe(inspect)) {
518        Ok(value) => Some(value),
519        Err(_) => {
520            issues.push(issue);
521            None
522        }
523    }
524}
525
526fn check_capability(
527    issues: &mut Vec<SemanticConformanceIssue>,
528    capability: bool,
529    semantic: bool,
530    issue: SemanticConformanceIssue,
531) {
532    if capability != semantic {
533        issues.push(issue);
534    }
535}
536
537fn trusted_contract_from_caps(
538    caps: FnCaps,
539    precision: Option<crate::function_contract::FunctionDependencyContract>,
540) -> FunctionSemanticContract {
541    let mut contract = FunctionSemanticContract::trusted_builtin_default(precision);
542    if caps.contains(FnCaps::DYNAMIC_DEPENDENCY) {
543        contract.dependency = FunctionDependencySemantics::Dynamic;
544    }
545    if caps.contains(FnCaps::SHORT_CIRCUIT) {
546        contract.evaluation = FunctionEvaluationSemantics::ShortCircuit;
547    }
548    contract.result = FunctionResultSemantics::from_capabilities(
549        caps.contains(FnCaps::RETURNS_REFERENCE),
550        caps.contains(FnCaps::MAY_SPILL),
551    );
552    if caps.contains(FnCaps::LOCAL_ENVIRONMENT) {
553        contract.environment = FunctionEnvironmentSemantics::LocalBindings;
554    }
555    contract
556}
557
558const EXCEL_PREFIXES: &[&str] = &["_XLFN.", "_XLL.", "_XLWS."];
559
560fn resolve_registered(
561    state: &RegistryState,
562    key: &RegistryKey,
563) -> Option<(RegistryKey, RegistryEntry)> {
564    if let Some(entry) = state.registrations.get(key) {
565        return Some((key.clone(), entry.clone()));
566    }
567    let alias = state.aliases.get(key)?;
568    state
569        .registrations
570        .get(&alias.target)
571        .map(|entry| (alias.target.clone(), entry.clone()))
572}
573
574fn resolve_entry(ns: &str, name: &str) -> Option<(RegistryKey, RegistryEntry)> {
575    let ns = norm(ns);
576    let normalized_name = norm(name);
577    let key = (ns.clone(), normalized_name.clone());
578    let mut state = REGISTRY
579        .write()
580        .unwrap_or_else(|poisoned| poisoned.into_inner());
581    if let Some(entry) = resolve_registered(&state, &key) {
582        return Some(entry);
583    }
584    let mut candidate = normalized_name.as_str();
585    loop {
586        let mut stripped_any = false;
587        for prefix in EXCEL_PREFIXES {
588            if let Some(rest) = candidate.strip_prefix(prefix) {
589                candidate = rest;
590                stripped_any = true;
591                let stripped_key = (ns.clone(), candidate.to_string());
592                if let Some((canonical, entry)) = resolve_registered(&state, &stripped_key) {
593                    state.aliases.insert(
594                        key.clone(),
595                        AliasEntry {
596                            target: canonical.clone(),
597                            owner: Some((canonical.clone(), entry.generation)),
598                        },
599                    );
600                    return Some((canonical, entry));
601                }
602                break;
603            }
604        }
605        if !stripped_any {
606            break;
607        }
608    }
609    None
610}
611
612fn resolve_key_read_only(
613    state: &RegistryState,
614    key: &RegistryKey,
615) -> Option<(RegistryKey, RegistryEntry)> {
616    if let Some(entry) = resolve_registered(state, key) {
617        return Some(entry);
618    }
619    let mut candidate = key.1.as_str();
620    loop {
621        let rest = EXCEL_PREFIXES
622            .iter()
623            .find_map(|prefix| candidate.strip_prefix(prefix))?;
624        candidate = rest;
625        let stripped = (key.0.clone(), candidate.to_string());
626        if let Some(entry) = resolve_registered(state, &stripped) {
627            return Some(entry);
628        }
629    }
630}
631
632fn resolve_entry_read_only(ns: &str, name: &str) -> Option<(RegistryKey, RegistryEntry)> {
633    let state = REGISTRY
634        .read()
635        .unwrap_or_else(|poisoned| poisoned.into_inner());
636    resolve_key_read_only(&state, &(norm(ns), norm(name)))
637}
638
639pub fn get(ns: &str, name: &str) -> Option<Arc<dyn Function>> {
640    resolve_entry(ns, name).map(|(_, entry)| entry.function)
641}
642
643/// Read-only registry lookup for planning providers. Unlike [`get`], this does
644/// not populate the global Excel-prefix alias cache.
645#[doc(hidden)]
646pub fn get_for_planning(ns: &str, name: &str) -> Option<Arc<dyn Function>> {
647    resolve_entry_read_only(ns, name).map(|(_, entry)| entry.function)
648}
649
650pub(crate) struct GlobalRegistryFunctionProvider;
651
652impl crate::traits::FunctionProvider for GlobalRegistryFunctionProvider {
653    fn planning_semantic_revision(&self) -> Option<u64> {
654        Some(0)
655    }
656
657    fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
658        get(ns, name)
659    }
660
661    fn get_function_for_planning(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
662        get_for_planning(ns, name)
663    }
664}
665
666#[derive(Clone)]
667struct PlanningRegistration {
668    canonical: RegistryKey,
669    function: Arc<dyn Function>,
670    generation: u64,
671    trusted_builtin: bool,
672}
673
674/// Immutable registry view used by one formula-planning operation.
675///
676/// Function metadata is inspected before publication of the snapshot. Resolution
677/// afterward is lock-free and never populates the global prefix-alias cache.
678#[derive(Clone)]
679pub(crate) struct RegistryPlanningSnapshot {
680    epoch: u64,
681    provider_revision: Option<u64>,
682    requests: Arc<Vec<(String, String, usize)>>,
683    functions: Arc<HashMap<RegistryKey, Arc<dyn Function>>>,
684    capabilities: Arc<HashMap<RegistryKey, FnCaps>>,
685    identities: Arc<HashMap<(String, String, usize), FunctionSemanticIdentity>>,
686}
687
688#[derive(Clone, Copy, Debug, PartialEq, Eq)]
689pub(crate) enum PlanningSnapshotError {
690    RegistryChangedDuringCapture,
691    ProviderRevisionUnavailable,
692}
693
694impl RegistryPlanningSnapshot {
695    const CAPTURE_ATTEMPTS: usize = 16;
696
697    pub(crate) fn capture_for_requests(
698        runtime_provider: &dyn crate::traits::FunctionProvider,
699        requests: impl IntoIterator<Item = (String, String, usize)>,
700    ) -> Result<Self, PlanningSnapshotError> {
701        crate::builtins::load_builtins();
702        let mut requests: Vec<_> = requests.into_iter().collect();
703        requests.sort();
704        requests.dedup();
705        Self::capture_with_hook(runtime_provider, &requests, Self::CAPTURE_ATTEMPTS, |_| {})
706    }
707
708    fn capture_with_hook(
709        runtime_provider: &dyn crate::traits::FunctionProvider,
710        requests: &[(String, String, usize)],
711        attempts: usize,
712        mut after_registry_copy: impl FnMut(usize),
713    ) -> Result<Self, PlanningSnapshotError> {
714        for attempt in 0..attempts {
715            let provider_revision = if requests.is_empty() {
716                None
717            } else {
718                Some(
719                    runtime_provider
720                        .planning_semantic_revision()
721                        .ok_or(PlanningSnapshotError::ProviderRevisionUnavailable)?,
722                )
723            };
724            let start_epoch = REGISTRY
725                .read()
726                .unwrap_or_else(|poisoned| poisoned.into_inner())
727                .semantic_epoch;
728            let runtime_functions: HashMap<_, _> = requests
729                .iter()
730                .filter_map(|(namespace, name, _)| {
731                    runtime_provider
732                        .get_function_for_planning(namespace, name)
733                        .map(|function| ((norm(namespace), norm(name)), function))
734                })
735                .collect();
736            let (epoch, registrations) = {
737                let state = REGISTRY
738                    .read()
739                    .unwrap_or_else(|poisoned| poisoned.into_inner());
740                let registrations = requests
741                    .iter()
742                    .filter_map(|(namespace, name, arity)| {
743                        let request_key = (norm(namespace), norm(name));
744                        resolve_key_read_only(&state, &request_key).map(|(canonical, entry)| {
745                            (
746                                (request_key.0, request_key.1, *arity),
747                                PlanningRegistration {
748                                    canonical,
749                                    function: entry.function,
750                                    generation: entry.generation,
751                                    trusted_builtin: entry.trusted_builtin,
752                                },
753                            )
754                        })
755                    })
756                    .collect::<HashMap<_, _>>();
757                (state.semantic_epoch, registrations)
758            };
759
760            after_registry_copy(attempt);
761
762            let mut capabilities = HashMap::new();
763            let mut identities = HashMap::new();
764            for (namespace, name, arity) in requests {
765                let request_key = (norm(namespace), norm(name));
766                let Some(runtime) = runtime_functions.get(&request_key) else {
767                    continue;
768                };
769                let Some(registration) =
770                    registrations.get(&(request_key.0.clone(), request_key.1.clone(), *arity))
771                else {
772                    continue;
773                };
774                if !Arc::ptr_eq(runtime, &registration.function) {
775                    continue;
776                }
777                let (semantics, identity_metadata) = inspect_semantics_with_identity_metadata(
778                    &registration.function,
779                    registration.trusted_builtin,
780                    registration.generation,
781                    *arity,
782                );
783                let Some(contract) = semantics.contract else {
784                    continue;
785                };
786                let Some((caps, argument_by_ref)) = identity_metadata else {
787                    continue;
788                };
789                capabilities.insert(request_key.clone(), caps);
790                identities.insert(
791                    (request_key.0, request_key.1, *arity),
792                    FunctionSemanticIdentity {
793                        namespace: registration.canonical.0.clone(),
794                        canonical_name: registration.canonical.1.clone(),
795                        generation: registration.generation,
796                        caps,
797                        contract,
798                        argument_by_ref,
799                    },
800                );
801            }
802
803            for (key, function) in &runtime_functions {
804                if !capabilities.contains_key(key)
805                    && let Ok(caps) = catch_unwind(AssertUnwindSafe(|| function.caps()))
806                {
807                    capabilities.insert(key.clone(), caps);
808                }
809            }
810
811            let provider_unchanged = provider_revision.is_none_or(|revision| {
812                runtime_provider.planning_semantic_revision() == Some(revision)
813            });
814            // Validate the capture against the functions this snapshot actually
815            // requested, not against the global registry epoch.
816            //
817            // Registering any function anywhere bumps `semantic_epoch`, so a
818            // global equality check treated a completely unrelated registration
819            // -- a different namespace, a different name -- as "the registry
820            // changed underneath us". Under concurrent registration that
821            // produced spurious `RegistryChangedDuringCapture` results and, in
822            // authoritative mode, spurious FormulaPlane family fallbacks whose
823            // reported reason blamed the provider.
824            //
825            // `semantic_changes_affect_requests_in_state` consults the change
826            // log across the whole capture window [start_epoch, now] and only
827            // reports a conflict when a *requested* key changed. It stays
828            // conservative when the bounded change log has been truncated past
829            // `start_epoch`, so precision never costs correctness.
830            let requests_unchanged = {
831                let state = REGISTRY
832                    .read()
833                    .unwrap_or_else(|poisoned| poisoned.into_inner());
834                !semantic_changes_affect_requests_in_state(
835                    &state,
836                    start_epoch,
837                    requests.iter().cloned(),
838                )
839            };
840            let unchanged = requests_unchanged && provider_unchanged;
841            if unchanged {
842                return Ok(Self {
843                    epoch,
844                    provider_revision,
845                    requests: Arc::new(requests.to_vec()),
846                    functions: Arc::new(runtime_functions),
847                    capabilities: Arc::new(capabilities),
848                    identities: Arc::new(identities),
849                });
850            }
851        }
852        Err(PlanningSnapshotError::RegistryChangedDuringCapture)
853    }
854
855    pub(crate) fn epoch(&self) -> u64 {
856        self.epoch
857    }
858
859    pub(crate) fn provider_revision(&self) -> Option<u64> {
860        self.provider_revision
861    }
862
863    pub(crate) fn semantic_changes_affect_requests_since(&self, epoch: u64) -> bool {
864        semantic_changes_affect_requests_since(epoch, self.requests.iter().cloned())
865    }
866
867    pub(crate) fn semantic_changes_affect_requests_since_guarded(
868        &self,
869        guard: &SemanticEpochReadGuard,
870        epoch: u64,
871    ) -> bool {
872        guard.semantic_changes_affect_requests_since(epoch, self.requests.iter().cloned())
873    }
874}
875
876pub(crate) fn semantic_changes_affect_requests_since(
877    epoch: u64,
878    requests: impl IntoIterator<Item = (String, String, usize)>,
879) -> bool {
880    let state = REGISTRY
881        .read()
882        .unwrap_or_else(|poisoned| poisoned.into_inner());
883    semantic_changes_affect_requests_in_state(&state, epoch, requests)
884}
885
886fn semantic_changes_affect_requests_in_state(
887    state: &RegistryState,
888    epoch: u64,
889    requests: impl IntoIterator<Item = (String, String, usize)>,
890) -> bool {
891    let changes = semantic_changes_since_in_state(state, epoch);
892    if changes.epoch == epoch {
893        return false;
894    }
895    if !changes.complete {
896        return true;
897    }
898    let requests = requests
899        .into_iter()
900        .flat_map(|(namespace, name, _)| {
901            let namespace = norm(namespace);
902            let normalized = norm(name);
903            let mut spellings = vec![(namespace.clone(), normalized.clone())];
904            let mut stripped = normalized.as_str();
905            while let Some(rest) = EXCEL_PREFIXES
906                .iter()
907                .find_map(|prefix| stripped.strip_prefix(prefix))
908            {
909                stripped = rest;
910                spellings.push((namespace.clone(), stripped.to_string()));
911            }
912            spellings
913        })
914        .collect::<std::collections::BTreeSet<_>>();
915    changes.keys.into_iter().any(|key| requests.contains(&key))
916}
917
918impl crate::traits::FunctionProvider for RegistryPlanningSnapshot {
919    fn planning_semantic_revision(&self) -> Option<u64> {
920        Some(self.provider_revision.unwrap_or(0))
921    }
922
923    fn get_function(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
924        self.functions.get(&(norm(ns), norm(name))).cloned()
925    }
926
927    fn get_function_for_planning(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
928        self.get_function(ns, name)
929    }
930
931    fn function_capabilities(&self, ns: &str, name: &str) -> Option<FnCaps> {
932        self.capabilities.get(&(norm(ns), norm(name))).copied()
933    }
934
935    fn function_semantic_identity(
936        &self,
937        ns: &str,
938        name: &str,
939        arity: usize,
940    ) -> Option<FunctionSemanticIdentity> {
941        self.identities.get(&(norm(ns), norm(name), arity)).cloned()
942    }
943}
944
945pub fn resolve(ns: &str, name: &str) -> Option<ResolvedFunction> {
946    resolve_entry(ns, name).map(to_resolved)
947}
948
949pub fn resolve_with_epoch(ns: &str, name: &str) -> Option<(u64, ResolvedFunction)> {
950    let key = (norm(ns), norm(name));
951    let state = REGISTRY
952        .read()
953        .unwrap_or_else(|poisoned| poisoned.into_inner());
954    resolve_registered(&state, &key).map(|entry| (state.semantic_epoch, to_resolved(entry)))
955}
956pub fn resolve_for_arity(ns: &str, name: &str, arity: usize) -> Option<ResolvedFunction> {
957    resolve_entry(ns, name).map(|((namespace, canonical_name), entry)| {
958        let semantics = {
959            let cached = entry
960                .semantics_by_arity
961                .read()
962                .unwrap_or_else(|poisoned| poisoned.into_inner())
963                .get(&arity)
964                .cloned();
965            cached.unwrap_or_else(|| {
966                let inspected = inspect_semantics(
967                    &entry.function,
968                    entry.trusted_builtin,
969                    entry.generation,
970                    arity,
971                );
972                entry
973                    .semantics_by_arity
974                    .write()
975                    .unwrap_or_else(|poisoned| poisoned.into_inner())
976                    .entry(arity)
977                    .or_insert_with(|| inspected.clone())
978                    .clone()
979            })
980        };
981        ResolvedFunction {
982            semantics,
983            namespace,
984            canonical_name,
985            function: entry.function,
986        }
987    })
988}
989
990pub(crate) fn resolve_semantic_identity<P: crate::traits::FunctionProvider + ?Sized>(
991    provider: &P,
992    ns: &str,
993    name: &str,
994    arity: usize,
995) -> Option<FunctionSemanticIdentity> {
996    let runtime = provider.get_function(ns, name)?;
997    let resolved = resolve_for_arity(ns, name, arity)?;
998    if !Arc::ptr_eq(&runtime, &resolved.function) {
999        return None;
1000    }
1001    let contract = resolved.semantics.contract?;
1002    let argument_by_ref = catch_unwind(AssertUnwindSafe(|| {
1003        let schema = runtime.arg_schema();
1004        let repeating = schema.iter().find(|argument| argument.repeating.is_some());
1005        (0..arity)
1006            .map(|index| {
1007                schema
1008                    .get(index)
1009                    .or(repeating)
1010                    .is_some_and(|argument| argument.by_ref)
1011            })
1012            .collect()
1013    }))
1014    .ok()?;
1015    Some(FunctionSemanticIdentity {
1016        namespace: resolved.namespace,
1017        canonical_name: resolved.canonical_name,
1018        generation: resolved.semantics.generation,
1019        caps: runtime.caps(),
1020        contract,
1021        argument_by_ref,
1022    })
1023}
1024
1025fn to_resolved(
1026    ((namespace, canonical_name), entry): (RegistryKey, RegistryEntry),
1027) -> ResolvedFunction {
1028    ResolvedFunction {
1029        namespace,
1030        canonical_name,
1031        function: entry.function,
1032        semantics: entry.semantics,
1033    }
1034}
1035
1036pub fn register_alias(ns: &str, alias: &str, target_ns: &str, target_name: &str) {
1037    let mut state = REGISTRY
1038        .write()
1039        .unwrap_or_else(|poisoned| poisoned.into_inner());
1040    let alias_key = (norm(ns), norm(alias));
1041    let target = (norm(target_ns), norm(target_name));
1042    let old_target = state
1043        .aliases
1044        .get(&alias_key)
1045        .map(|entry| entry.target.clone());
1046    if old_target.as_ref() == Some(&target) {
1047        return;
1048    }
1049    state.aliases.insert(
1050        alias_key.clone(),
1051        AliasEntry {
1052            target: target.clone(),
1053            owner: None,
1054        },
1055    );
1056    // Retargeting changes resolution through the alias spelling, not resolution
1057    // of either directly named target.
1058    publish_semantic_change(&mut state, [alias_key]);
1059}
1060
1061pub fn snapshot_registered() -> Vec<(String, String, Arc<dyn Function>)> {
1062    let state = REGISTRY
1063        .read()
1064        .unwrap_or_else(|poisoned| poisoned.into_inner());
1065    state
1066        .registrations
1067        .iter()
1068        .map(|((ns, name), entry)| (ns.clone(), name.clone(), Arc::clone(&entry.function)))
1069        .collect()
1070}
1071pub fn snapshot_semantics() -> Vec<ResolvedFunction> {
1072    let state = REGISTRY
1073        .read()
1074        .unwrap_or_else(|poisoned| poisoned.into_inner());
1075    state
1076        .registrations
1077        .iter()
1078        .map(|((namespace, canonical_name), entry)| ResolvedFunction {
1079            namespace: namespace.clone(),
1080            canonical_name: canonical_name.clone(),
1081            function: Arc::clone(&entry.function),
1082            semantics: entry.semantics.clone(),
1083        })
1084        .collect()
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::traits::FunctionProvider;
1091
1092    struct TestFn {
1093        ns: &'static str,
1094        name: &'static str,
1095        aliases: &'static [&'static str],
1096    }
1097
1098    impl Function for TestFn {
1099        fn name(&self) -> &'static str {
1100            self.name
1101        }
1102        fn namespace(&self) -> &'static str {
1103            self.ns
1104        }
1105        fn aliases(&self) -> &'static [&'static str] {
1106            self.aliases
1107        }
1108        fn eval<'a, 'b, 'c>(
1109            &self,
1110            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
1111            _ctx: &dyn crate::traits::FunctionContext<'b>,
1112        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
1113            Ok(crate::traits::CalcValue::Scalar(
1114                formualizer_common::LiteralValue::Number(1.0),
1115            ))
1116        }
1117    }
1118
1119    struct PlanningFn {
1120        ns: &'static str,
1121        name: &'static str,
1122        aliases: &'static [&'static str],
1123        caps: FnCaps,
1124    }
1125
1126    impl Function for PlanningFn {
1127        fn name(&self) -> &'static str {
1128            self.name
1129        }
1130        fn namespace(&self) -> &'static str {
1131            self.ns
1132        }
1133        fn aliases(&self) -> &'static [&'static str] {
1134            self.aliases
1135        }
1136        fn caps(&self) -> FnCaps {
1137            self.caps
1138        }
1139        fn min_args(&self) -> usize {
1140            1
1141        }
1142        fn variadic(&self) -> bool {
1143            true
1144        }
1145        fn arg_schema(&self) -> &'static [crate::args::ArgSchema] {
1146            static SCHEMA: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
1147                std::sync::LazyLock::new(|| {
1148                    let mut argument = crate::args::ArgSchema::any();
1149                    argument.repeating = Some(1);
1150                    vec![argument]
1151                });
1152            &SCHEMA
1153        }
1154        fn eval<'a, 'b, 'c>(
1155            &self,
1156            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
1157            _ctx: &dyn crate::traits::FunctionContext<'b>,
1158        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
1159            unreachable!()
1160        }
1161    }
1162
1163    fn planning_fn(
1164        ns: &'static str,
1165        name: &'static str,
1166        aliases: &'static [&'static str],
1167        caps: FnCaps,
1168    ) -> Arc<dyn Function> {
1169        Arc::new(PlanningFn {
1170            ns,
1171            name,
1172            aliases,
1173            caps,
1174        })
1175    }
1176
1177    #[test]
1178    fn planning_snapshot_resolves_direct_alias_namespace_and_prefix_without_cache_mutation() {
1179        let ns = "__PLANNING_PARITY__";
1180        register_builtin(planning_fn(ns, "TARGET", &["ALIAS"], FnCaps::empty()));
1181        let requests = [
1182            (ns.to_string(), "TARGET".to_string(), 1),
1183            (ns.to_string(), "alias".to_string(), 1),
1184            (ns.to_string(), "_xlfn._xlws.alias".to_string(), 1),
1185        ];
1186        let prefixed_key = (ns.to_string(), "_XLFN._XLWS.ALIAS".to_string());
1187        assert!(!REGISTRY.read().unwrap().aliases.contains_key(&prefixed_key));
1188        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1189            &GlobalRegistryFunctionProvider,
1190            requests,
1191        )
1192        .unwrap();
1193        assert!(!REGISTRY.read().unwrap().aliases.contains_key(&prefixed_key));
1194
1195        let direct = snapshot
1196            .function_semantic_identity(ns, "TARGET", 1)
1197            .unwrap();
1198        for spelling in ["alias", "_xlfn._xlws.alias"] {
1199            let resolved = snapshot
1200                .function_semantic_identity(ns, spelling, 1)
1201                .unwrap();
1202            assert_eq!(resolved.namespace, ns);
1203            assert_eq!(resolved.canonical_name, "TARGET");
1204            assert_eq!(resolved.generation, direct.generation);
1205            assert!(Arc::ptr_eq(
1206                &snapshot.get_function(ns, spelling).unwrap(),
1207                &snapshot.get_function(ns, "TARGET").unwrap(),
1208            ));
1209        }
1210    }
1211
1212    #[test]
1213    fn guarded_request_change_check_does_not_relock_behind_queued_writer() {
1214        let ns = "__GUARDED_REQUEST_CHANGE__";
1215        register_function(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1216        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1217            &GlobalRegistryFunctionProvider,
1218            [(ns.to_string(), "TARGET".to_string(), 1)],
1219        )
1220        .unwrap();
1221        register_function(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1222
1223        let guard = semantic_epoch_read_guard();
1224        let (queued_tx, queued_rx) = std::sync::mpsc::sync_channel(0);
1225        let writer = std::thread::spawn(move || {
1226            assert!(REGISTRY.try_write().is_err());
1227            queued_tx.send(()).unwrap();
1228            let mut state = REGISTRY
1229                .write()
1230                .unwrap_or_else(|poisoned| poisoned.into_inner());
1231            publish_semantic_change(&mut state, [(ns.to_string(), "QUEUED_WRITER".to_string())]);
1232        });
1233        queued_rx.recv().unwrap();
1234        std::thread::yield_now();
1235
1236        assert!(snapshot.semantic_changes_affect_requests_since_guarded(&guard, snapshot.epoch(),));
1237        drop(guard);
1238        writer.join().unwrap();
1239    }
1240
1241    #[test]
1242    fn workbook_planning_fallback_does_not_populate_prefix_alias_cache() {
1243        let ns = "__PLANNING_WORKBOOK_PREFIX__";
1244        register_builtin(planning_fn(ns, "TARGET", &["ALIAS"], FnCaps::empty()));
1245        let prefixed_key = (ns.to_string(), "_XLFN.ALIAS".to_string());
1246        assert!(!REGISTRY.read().unwrap().aliases.contains_key(&prefixed_key));
1247
1248        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1249            &crate::test_workbook::TestWorkbook::default(),
1250            [(ns.to_string(), "_xlfn.alias".to_string(), 1)],
1251        )
1252        .unwrap();
1253
1254        assert!(
1255            snapshot
1256                .function_semantic_identity(ns, "_xlfn.alias", 1)
1257                .is_some()
1258        );
1259        assert!(!REGISTRY.read().unwrap().aliases.contains_key(&prefixed_key));
1260    }
1261
1262    #[test]
1263    fn planning_snapshot_is_immutable_across_replacement() {
1264        let ns = "__PLANNING_IMMUTABLE__";
1265        register_builtin(planning_fn(ns, "TARGET", &["OLD_ALIAS"], FnCaps::empty()));
1266        let requests = [
1267            (ns.to_string(), "TARGET".to_string(), 1),
1268            (ns.to_string(), "OLD_ALIAS".to_string(), 1),
1269        ];
1270        let snapshot = RegistryPlanningSnapshot::capture_with_hook(
1271            &GlobalRegistryFunctionProvider,
1272            &requests,
1273            10_000,
1274            |_| {},
1275        )
1276        .unwrap();
1277        let old_function = snapshot.get_function(ns, "TARGET").unwrap();
1278        let old_identity = snapshot
1279            .function_semantic_identity(ns, "TARGET", 1)
1280            .unwrap();
1281
1282        register_function(planning_fn(ns, "TARGET", &[], FnCaps::MAY_SPILL));
1283        let current = resolve_for_arity(ns, "TARGET", 1).unwrap();
1284        assert!(current.semantics.generation > old_identity.generation);
1285        assert!(!Arc::ptr_eq(&old_function, &current.function));
1286        assert_eq!(
1287            snapshot
1288                .function_semantic_identity(ns, "TARGET", 1)
1289                .unwrap(),
1290            old_identity
1291        );
1292        assert!(Arc::ptr_eq(
1293            &old_function,
1294            &snapshot.get_function(ns, "TARGET").unwrap(),
1295        ));
1296        assert!(get(ns, "OLD_ALIAS").is_none());
1297        assert_eq!(
1298            snapshot
1299                .function_semantic_identity(ns, "OLD_ALIAS", 1)
1300                .unwrap(),
1301            old_identity
1302        );
1303        assert!(Arc::ptr_eq(
1304            &old_function,
1305            &snapshot.get_function(ns, "OLD_ALIAS").unwrap(),
1306        ));
1307    }
1308
1309    #[test]
1310    fn planning_snapshot_requires_explicit_side_effect_free_provider_opt_in() {
1311        struct RuntimeOnlyProvider(Arc<dyn Function>);
1312        impl FunctionProvider for RuntimeOnlyProvider {
1313            fn get_function(&self, _ns: &str, _name: &str) -> Option<Arc<dyn Function>> {
1314                Some(Arc::clone(&self.0))
1315            }
1316        }
1317
1318        let ns = "__PLANNING_FAIL_CLOSED__";
1319        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1320        let result = RegistryPlanningSnapshot::capture_for_requests(
1321            &RuntimeOnlyProvider(planning_fn(ns, "TARGET", &[], FnCaps::empty())),
1322            [(ns.to_string(), "TARGET".to_string(), 1)],
1323        );
1324        assert_eq!(
1325            result.err(),
1326            Some(PlanningSnapshotError::ProviderRevisionUnavailable)
1327        );
1328    }
1329
1330    #[test]
1331    fn planning_snapshot_preserves_runtime_override_without_global_semantics() {
1332        struct OverrideProvider(Arc<dyn Function>);
1333        impl FunctionProvider for OverrideProvider {
1334            fn planning_semantic_revision(&self) -> Option<u64> {
1335                Some(0)
1336            }
1337            fn get_function(&self, _ns: &str, _name: &str) -> Option<Arc<dyn Function>> {
1338                Some(Arc::clone(&self.0))
1339            }
1340            fn get_function_for_planning(
1341                &self,
1342                _ns: &str,
1343                _name: &str,
1344            ) -> Option<Arc<dyn Function>> {
1345                Some(Arc::clone(&self.0))
1346            }
1347        }
1348
1349        let ns = "__PLANNING_OVERRIDE__";
1350        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1351        let global = get(ns, "TARGET").unwrap();
1352        let runtime = planning_fn(ns, "TARGET", &[], FnCaps::MAY_SPILL);
1353        let provider = OverrideProvider(Arc::clone(&runtime));
1354        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1355            &provider,
1356            [(ns.to_string(), "TARGET".to_string(), 1)],
1357        )
1358        .unwrap();
1359
1360        let captured = snapshot.get_function(ns, "TARGET").unwrap();
1361        assert!(Arc::ptr_eq(&captured, &runtime));
1362        assert!(!Arc::ptr_eq(&captured, &global));
1363        assert!(
1364            snapshot
1365                .function_semantic_identity(ns, "TARGET", 1)
1366                .is_none()
1367        );
1368        assert_eq!(snapshot.functions.len(), 1);
1369        assert_eq!(
1370            snapshot.function_capabilities(ns, "TARGET"),
1371            Some(FnCaps::MAY_SPILL)
1372        );
1373        assert_eq!(snapshot.capabilities.len(), 1);
1374        assert!(snapshot.identities.is_empty());
1375        assert!(snapshot.get_function(ns, "UNREQUESTED").is_none());
1376    }
1377
1378    #[test]
1379    fn planning_snapshot_retries_provider_revision_flip_to_runtime_override() {
1380        struct FlippingProvider {
1381            function: Arc<RwLock<Arc<dyn Function>>>,
1382            revision: Arc<AtomicU64>,
1383        }
1384        impl FunctionProvider for FlippingProvider {
1385            fn planning_semantic_revision(&self) -> Option<u64> {
1386                Some(self.revision.load(Ordering::Acquire))
1387            }
1388            fn get_function(&self, _ns: &str, _name: &str) -> Option<Arc<dyn Function>> {
1389                Some(Arc::clone(&self.function.read().unwrap()))
1390            }
1391            fn get_function_for_planning(&self, ns: &str, name: &str) -> Option<Arc<dyn Function>> {
1392                self.get_function(ns, name)
1393            }
1394        }
1395
1396        let ns = "__PLANNING_PROVIDER_FLIP__";
1397        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1398        let global = get(ns, "TARGET").unwrap();
1399        let override_function = planning_fn(ns, "TARGET", &[], FnCaps::MAY_SPILL);
1400        let function = Arc::new(RwLock::new(global));
1401        let revision = Arc::new(AtomicU64::new(0));
1402        let provider = FlippingProvider {
1403            function: Arc::clone(&function),
1404            revision: Arc::clone(&revision),
1405        };
1406        let requests = [(ns.to_string(), "TARGET".to_string(), 1)];
1407        let snapshot =
1408            RegistryPlanningSnapshot::capture_with_hook(&provider, &requests, 2, |attempt| {
1409                if attempt == 0 {
1410                    *function.write().unwrap() = Arc::clone(&override_function);
1411                    revision.fetch_add(1, Ordering::AcqRel);
1412                }
1413            })
1414            .unwrap();
1415
1416        assert_eq!(snapshot.provider_revision(), Some(1));
1417        assert!(Arc::ptr_eq(
1418            &snapshot.get_function(ns, "TARGET").unwrap(),
1419            &override_function
1420        ));
1421        assert!(
1422            snapshot
1423                .function_semantic_identity(ns, "TARGET", 1)
1424                .is_none()
1425        );
1426    }
1427
1428    #[test]
1429    fn planning_snapshot_capture_tolerates_unrelated_concurrent_registrations() {
1430        // Regression test for the flake in the provider-revision test family.
1431        //
1432        // A capture used to be validated against the global `semantic_epoch`,
1433        // so registering *any* function on another thread invalidated it. In
1434        // the test suite that surfaced as an intermittent failure that moved
1435        // between tests depending on which happened to be running in parallel;
1436        // in production it would surface as spurious FormulaPlane fallbacks in
1437        // any application that registers custom functions while evaluating.
1438        //
1439        // The churn below never touches the requested key, so a correct capture
1440        // must succeed even with the tightest useful attempt budget.
1441        // The capture hook fires between the registry copy and the validation
1442        // step -- exactly where a concurrent registration would land -- so this
1443        // reproduces the race deterministically rather than relying on thread
1444        // interleaving.
1445        let ns = "__PLANNING_UNRELATED_CHURN__";
1446        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1447        let requests = [(ns.to_string(), "TARGET".to_string(), 1)];
1448
1449        let mut unrelated_registrations = 0usize;
1450        let snapshot = RegistryPlanningSnapshot::capture_with_hook(
1451            &GlobalRegistryFunctionProvider,
1452            &requests,
1453            2,
1454            |_| {
1455                // `register_function` is untrusted, so unlike a repeated builtin
1456                // registration it always publishes a semantic change. Firing on
1457                // every attempt means a global-epoch check can never converge.
1458                register_function(planning_fn(
1459                    "__PLANNING_UNRELATED_CHURN_OTHER__",
1460                    "OTHER",
1461                    &[],
1462                    FnCaps::empty(),
1463                ));
1464                unrelated_registrations += 1;
1465            },
1466        )
1467        .expect("unrelated registrations must not invalidate a planning snapshot capture");
1468
1469        assert!(unrelated_registrations > 0, "hook must have registered");
1470        assert!(Arc::ptr_eq(
1471            &snapshot.get_function(ns, "TARGET").unwrap(),
1472            &get(ns, "TARGET").unwrap(),
1473        ));
1474    }
1475
1476    #[test]
1477    fn planning_snapshot_capture_retries_and_fails_deterministically() {
1478        let ns = "__PLANNING_RACE__";
1479        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1480        let requests = [(ns.to_string(), "TARGET".to_string(), 1)];
1481        let before = semantic_epoch();
1482        let retried = RegistryPlanningSnapshot::capture_with_hook(
1483            &GlobalRegistryFunctionProvider,
1484            &requests,
1485            100,
1486            |attempt| {
1487                if attempt == 0 {
1488                    register_function(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1489                }
1490            },
1491        )
1492        .unwrap();
1493        assert!(retried.epoch() > before);
1494
1495        let failed = RegistryPlanningSnapshot::capture_with_hook(
1496            &GlobalRegistryFunctionProvider,
1497            &requests,
1498            2,
1499            |_| register_function(planning_fn(ns, "TARGET", &[], FnCaps::empty())),
1500        );
1501        assert_eq!(
1502            failed.err(),
1503            Some(PlanningSnapshotError::RegistryChangedDuringCapture)
1504        );
1505    }
1506
1507    #[test]
1508    fn planning_snapshot_nested_function_authority_matches_global_registry() {
1509        let ns = "";
1510        register_builtin(planning_fn(ns, "__PLAN_OUTER__", &[], FnCaps::empty()));
1511        register_builtin(planning_fn(ns, "__PLAN_INNER__", &[], FnCaps::empty()));
1512        let requests = [
1513            (String::new(), "__PLAN_OUTER__".to_string(), 1),
1514            (String::new(), "_xlfn.__PLAN_INNER__".to_string(), 1),
1515        ];
1516        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1517            &GlobalRegistryFunctionProvider,
1518            requests,
1519        )
1520        .unwrap();
1521        let ast =
1522            formualizer_parse::parser::parse("=__PLAN_OUTER__(_xlfn.__PLAN_INNER__(A1))").unwrap();
1523        let frozen = crate::formula_plane::template_canonical::canonicalize_template_with_provider(
1524            &ast,
1525            2,
1526            2,
1527            Some(&snapshot),
1528        );
1529        let global = crate::formula_plane::template_canonical::canonicalize_template_with_provider(
1530            &ast,
1531            2,
1532            2,
1533            Some(&GlobalRegistryFunctionProvider),
1534        );
1535        assert_eq!(frozen, global);
1536        assert!(frozen.labels.is_authority_supported());
1537    }
1538
1539    #[test]
1540    fn parallel_snapshot_capture_and_prefix_resolution_does_not_deadlock() {
1541        let ns = "__PLANNING_PARALLEL__";
1542        register_builtin(planning_fn(ns, "TARGET", &["ALIAS"], FnCaps::empty()));
1543        let (send, receive) = std::sync::mpsc::channel();
1544        std::thread::spawn(move || {
1545            let mut workers = Vec::new();
1546            for worker in 0..4 {
1547                workers.push(std::thread::spawn(move || {
1548                    for iteration in 0..100 {
1549                        if worker == 0 && iteration % 10 == 0 {
1550                            register_function(planning_fn(
1551                                ns,
1552                                "TARGET",
1553                                &["ALIAS"],
1554                                FnCaps::empty(),
1555                            ));
1556                        }
1557                        let _ = RegistryPlanningSnapshot::capture_for_requests(
1558                            &GlobalRegistryFunctionProvider,
1559                            [(ns.to_string(), "_xlfn.alias".to_string(), 1)],
1560                        );
1561                        let _ = get(ns, "_xlfn.alias");
1562                    }
1563                }));
1564            }
1565            for worker in workers {
1566                worker.join().unwrap();
1567            }
1568            send.send(()).unwrap();
1569        });
1570        receive
1571            .recv_timeout(std::time::Duration::from_secs(10))
1572            .expect("parallel registry planning timed out (possible lock inversion)");
1573    }
1574
1575    #[test]
1576    fn resolves_prefixes_aliases_and_direct_registration() {
1577        let ns = "__REG_PREFIX__";
1578        register_function(Arc::new(TestFn {
1579            ns,
1580            name: "FILTER",
1581            aliases: &["LEGACY"],
1582        }));
1583        assert_eq!(get(ns, "_xlfn._xlws.legacy").unwrap().name(), "FILTER");
1584        register_function(Arc::new(TestFn {
1585            ns,
1586            name: "_XLFN.FILTER",
1587            aliases: &[],
1588        }));
1589        assert_eq!(get(ns, "_xlfn.filter").unwrap().name(), "_XLFN.FILTER");
1590    }
1591
1592    #[test]
1593    fn trusted_replacement_records_removed_owned_alias_spelling() {
1594        let namespace = "__REG_STALE_ALIAS__";
1595        register_builtin(Arc::new(TestFn {
1596            ns: namespace,
1597            name: "TARGET",
1598            aliases: &["STALE_OWNED_ALIAS"],
1599        }));
1600        let before = semantic_epoch();
1601        register_function(Arc::new(TestFn {
1602            ns: namespace,
1603            name: "TARGET",
1604            aliases: &["NEW_OWNED_ALIAS"],
1605        }));
1606        let changes = semantic_changes_since(before);
1607        assert!(
1608            changes
1609                .keys
1610                .contains(&(namespace.to_string(), "STALE_OWNED_ALIAS".to_string()))
1611        );
1612        assert!(
1613            changes
1614                .keys
1615                .contains(&(namespace.to_string(), "NEW_OWNED_ALIAS".to_string()))
1616        );
1617    }
1618
1619    #[test]
1620    fn alias_requests_keep_the_spelling_used_by_the_formula() {
1621        let ns = "__ALIAS_REQUEST_SPELLING__";
1622        register_builtin(planning_fn(ns, "TARGET", &[], FnCaps::empty()));
1623        register_alias(ns, "FORMULA_ALIAS", ns, "TARGET");
1624        let request = (ns.to_string(), "FORMULA_ALIAS".to_string(), 1);
1625        let snapshot = RegistryPlanningSnapshot::capture_for_requests(
1626            &GlobalRegistryFunctionProvider,
1627            [request.clone()],
1628        )
1629        .unwrap();
1630
1631        assert_eq!(snapshot.requests.as_ref(), &[request]);
1632        assert_eq!(
1633            snapshot
1634                .function_semantic_identity(ns, "FORMULA_ALIAS", 1)
1635                .unwrap()
1636                .canonical_name,
1637            "TARGET"
1638        );
1639    }
1640
1641    #[test]
1642    fn alias_mutations_affect_alias_requests_but_not_direct_target_requests() {
1643        let ns = "__ALIAS_CHANGE_SCOPE__";
1644        register_builtin(planning_fn(ns, "OLD_TARGET", &[], FnCaps::empty()));
1645        register_builtin(planning_fn(ns, "NEW_TARGET", &[], FnCaps::empty()));
1646
1647        let add_epoch = semantic_epoch();
1648        register_alias(ns, "ADDED_ALIAS", ns, "OLD_TARGET");
1649        assert!(semantic_changes_affect_requests_since(
1650            add_epoch,
1651            [(ns.to_string(), "ADDED_ALIAS".to_string(), 1)]
1652        ));
1653        assert!(!semantic_changes_affect_requests_since(
1654            add_epoch,
1655            [(ns.to_string(), "OLD_TARGET".to_string(), 1)]
1656        ));
1657
1658        register_alias(ns, "RETARGETED_ALIAS", ns, "OLD_TARGET");
1659        let retarget_epoch = semantic_epoch();
1660        register_alias(ns, "RETARGETED_ALIAS", ns, "NEW_TARGET");
1661        assert!(semantic_changes_affect_requests_since(
1662            retarget_epoch,
1663            [(ns.to_string(), "RETARGETED_ALIAS".to_string(), 1)]
1664        ));
1665        assert!(semantic_changes_affect_requests_since(
1666            retarget_epoch,
1667            [(ns.to_string(), "_xlfn.RETARGETED_ALIAS".to_string(), 1)]
1668        ));
1669        for target in ["OLD_TARGET", "NEW_TARGET"] {
1670            assert!(!semantic_changes_affect_requests_since(
1671                retarget_epoch,
1672                [(ns.to_string(), target.to_string(), 1)]
1673            ));
1674        }
1675
1676        register_alias(ns, "REMOVED_ALIAS", ns, "OLD_TARGET");
1677        let remove_epoch = semantic_epoch();
1678        {
1679            let mut state = REGISTRY
1680                .write()
1681                .unwrap_or_else(|poisoned| poisoned.into_inner());
1682            let alias_key = (ns.to_string(), "REMOVED_ALIAS".to_string());
1683            assert!(state.aliases.remove(&alias_key).is_some());
1684            publish_semantic_change(&mut state, [alias_key]);
1685        }
1686        assert!(semantic_changes_affect_requests_since(
1687            remove_epoch,
1688            [(ns.to_string(), "REMOVED_ALIAS".to_string(), 1)]
1689        ));
1690        assert!(!semantic_changes_affect_requests_since(
1691            remove_epoch,
1692            [(ns.to_string(), "OLD_TARGET".to_string(), 1)]
1693        ));
1694    }
1695
1696    #[test]
1697    fn request_change_check_stays_conservative_after_log_truncation() {
1698        let mut state = RegistryState::default();
1699        let before = state.semantic_epoch;
1700        for index in 0..=1_024 {
1701            publish_semantic_change(&mut state, [(String::new(), format!("UNRELATED_{index}"))]);
1702        }
1703
1704        assert!(semantic_changes_affect_requests_in_state(
1705            &state,
1706            before,
1707            [(String::new(), "TARGET".to_string(), 1)]
1708        ));
1709    }
1710
1711    #[test]
1712    fn replacement_advances_semantic_generation() {
1713        let ns = "__REG_GENERATION__";
1714        register_function(Arc::new(TestFn {
1715            ns,
1716            name: "F",
1717            aliases: &[],
1718        }));
1719        let first = resolve(ns, "F").unwrap().semantics.generation;
1720        let epoch = semantic_epoch();
1721        register_function(Arc::new(TestFn {
1722            ns,
1723            name: "F",
1724            aliases: &[],
1725        }));
1726        let second = resolve(ns, "F").unwrap().semantics.generation;
1727        assert!(second > first);
1728        let changes = semantic_changes_since(epoch);
1729        assert!(changes.epoch > epoch);
1730        assert!(changes.keys.contains(&(ns.to_string(), "F".to_string())));
1731    }
1732
1733    struct PanickingSchemaFn;
1734
1735    impl Function for PanickingSchemaFn {
1736        fn name(&self) -> &'static str {
1737            "PANICKING_SCHEMA"
1738        }
1739        fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
1740            Some(FunctionSemanticContract::trusted_builtin_default(None))
1741        }
1742        fn arg_schema(&self) -> &'static [crate::args::ArgSchema] {
1743            panic!("bad schema")
1744        }
1745        fn eval<'a, 'b, 'c>(
1746            &self,
1747            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
1748            _ctx: &dyn crate::traits::FunctionContext<'b>,
1749        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
1750            unreachable!()
1751        }
1752    }
1753
1754    #[test]
1755    fn schema_panic_as_sole_defect_is_non_panicking_and_fails_closed() {
1756        register_function(Arc::new(PanickingSchemaFn));
1757        let semantics = resolve("", "PANICKING_SCHEMA").unwrap().semantics;
1758        assert!(semantics.contract.is_none());
1759        assert!(
1760            semantics
1761                .issues
1762                .contains(&SemanticConformanceIssue::ArgumentSchemaPanicked)
1763        );
1764        assert_eq!(
1765            semantics.issues,
1766            vec![SemanticConformanceIssue::ArgumentSchemaPanicked]
1767        );
1768        assert!(!semantics.conforms());
1769    }
1770
1771    #[test]
1772    fn every_registered_builtin_has_a_conforming_semantic_contract() {
1773        crate::builtins::load_builtins();
1774        let builtins: Vec<_> = snapshot_semantics()
1775            .into_iter()
1776            .filter(|entry| entry.semantics.trusted_builtin)
1777            .collect();
1778        assert!(builtins.len() > 100);
1779        let rejected: Vec<_> = builtins
1780            .iter()
1781            .filter(|entry| !entry.semantics.conforms())
1782            .map(|entry| {
1783                (
1784                    &entry.namespace,
1785                    &entry.canonical_name,
1786                    &entry.semantics.issues,
1787                )
1788            })
1789            .collect();
1790        assert!(rejected.is_empty(), "non-conforming builtins: {rejected:?}");
1791    }
1792
1793    #[test]
1794    fn semantic_contract_is_context_and_arity_aware() {
1795        crate::builtins::lookup::register_builtins();
1796        let row_without_arg = resolve_for_arity("", "ROW", 0).unwrap();
1797        let row_with_arg = resolve_for_arity("", "ROW", 1).unwrap();
1798        assert_eq!(
1799            row_without_arg.semantics.contract.unwrap().context,
1800            crate::function_contract::FunctionContextDependence::PlacementDependent
1801        );
1802        assert_eq!(
1803            row_with_arg.semantics.contract.unwrap().context,
1804            crate::function_contract::FunctionContextDependence::None
1805        );
1806    }
1807
1808    #[test]
1809    fn semantic_identity_encodes_effective_by_reference_roles_for_call_arity() {
1810        crate::builtins::load_builtins();
1811        let provider = GlobalRegistryFunctionProvider;
1812        let sum = resolve_semantic_identity(&provider, "", "SUM", 3).unwrap();
1813        assert_eq!(sum.argument_by_ref, vec![false, false, false]);
1814
1815        let row = resolve_semantic_identity(&provider, "", "ROW", 1).unwrap();
1816        assert_eq!(row.argument_by_ref, vec![true]);
1817    }
1818
1819    struct ExplicitSafeCustomFn;
1820
1821    impl Function for ExplicitSafeCustomFn {
1822        fn name(&self) -> &'static str {
1823            "EXPLICIT_SAFE_CUSTOM"
1824        }
1825        fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
1826            Some(FunctionSemanticContract::trusted_builtin_default(None))
1827        }
1828        fn eval<'a, 'b, 'c>(
1829            &self,
1830            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
1831            _ctx: &dyn crate::traits::FunctionContext<'b>,
1832        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
1833            unreachable!()
1834        }
1835    }
1836
1837    struct MismatchedPrecisionCustomFn;
1838
1839    impl Function for MismatchedPrecisionCustomFn {
1840        fn name(&self) -> &'static str {
1841            "MISMATCHED_PRECISION_CUSTOM"
1842        }
1843        fn dependency_contract(
1844            &self,
1845            arity: usize,
1846        ) -> Option<crate::function_contract::FunctionDependencyContract> {
1847            crate::function_contract::FunctionDependencyContract::static_scalar_all_args(arity)
1848        }
1849        fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
1850            Some(FunctionSemanticContract::trusted_builtin_default(None))
1851        }
1852        fn min_args(&self) -> usize {
1853            1
1854        }
1855        fn arg_schema(&self) -> &'static [crate::args::ArgSchema] {
1856            static SCHEMA: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
1857                std::sync::LazyLock::new(|| vec![crate::args::ArgSchema::any()]);
1858            &SCHEMA
1859        }
1860        fn eval<'a, 'b, 'c>(
1861            &self,
1862            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
1863            _ctx: &dyn crate::traits::FunctionContext<'b>,
1864        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
1865            unreachable!()
1866        }
1867    }
1868
1869    #[test]
1870    fn explicit_precision_must_equal_dependency_contract() {
1871        register_function(Arc::new(MismatchedPrecisionCustomFn));
1872        let semantics = resolve_for_arity("", "MISMATCHED_PRECISION_CUSTOM", 1)
1873            .unwrap()
1874            .semantics;
1875        assert!(semantics.contract.is_none());
1876        assert!(
1877            semantics
1878                .issues
1879                .contains(&SemanticConformanceIssue::PrecisionContractMismatch)
1880        );
1881    }
1882
1883    #[test]
1884    fn explicit_custom_semantics_can_conform_without_becoming_trusted() {
1885        register_function(Arc::new(ExplicitSafeCustomFn));
1886        let semantics = resolve_for_arity("", "EXPLICIT_SAFE_CUSTOM", 0)
1887            .unwrap()
1888            .semantics;
1889        assert!(!semantics.trusted_builtin);
1890        assert!(semantics.conforms());
1891    }
1892
1893    #[test]
1894    fn concurrent_replacements_leave_only_final_owned_alias() {
1895        let ns = "__REG_CONCURRENT__";
1896        register_function(Arc::new(TestFn {
1897            ns,
1898            name: "TARGET",
1899            aliases: &["INITIAL"],
1900        }));
1901        let mut workers = Vec::new();
1902        for alias in ["A", "B", "C", "D"] {
1903            workers.push(std::thread::spawn(move || {
1904                for _ in 0..100 {
1905                    let aliases: &'static [&'static str] = Box::leak(Box::new([alias]));
1906                    register_function(Arc::new(TestFn {
1907                        ns,
1908                        name: "TARGET",
1909                        aliases,
1910                    }));
1911                    assert_eq!(get(ns, "TARGET").unwrap().name(), "TARGET");
1912                }
1913            }));
1914        }
1915        for worker in workers {
1916            worker.join().unwrap();
1917        }
1918        register_function(Arc::new(TestFn {
1919            ns,
1920            name: "TARGET",
1921            aliases: &["FINAL"],
1922        }));
1923        for stale in ["INITIAL", "A", "B", "C", "D"] {
1924            assert!(get(ns, stale).is_none());
1925        }
1926        assert!(get(ns, "FINAL").is_some());
1927    }
1928
1929    #[test]
1930    fn independent_exception_inventory_matches_builtin_caps_and_context() {
1931        crate::builtins::load_builtins();
1932        for name in [
1933            "RAND",
1934            "RANDBETWEEN",
1935            "RANDARRAY",
1936            "TODAY",
1937            "NOW",
1938            "OFFSET",
1939            "INDIRECT",
1940        ] {
1941            assert!(
1942                get("", name).unwrap().caps().contains(FnCaps::VOLATILE),
1943                "{name}"
1944            );
1945        }
1946        for name in ["OFFSET", "INDIRECT"] {
1947            assert!(
1948                get("", name)
1949                    .unwrap()
1950                    .caps()
1951                    .contains(FnCaps::DYNAMIC_DEPENDENCY),
1952                "{name}"
1953            );
1954        }
1955        for name in ["INDEX", "OFFSET", "INDIRECT", "CHOOSE"] {
1956            assert!(
1957                get("", name)
1958                    .unwrap()
1959                    .caps()
1960                    .contains(FnCaps::RETURNS_REFERENCE),
1961                "{name}"
1962            );
1963        }
1964        for name in ["LET", "LAMBDA"] {
1965            assert!(
1966                get("", name)
1967                    .unwrap()
1968                    .caps()
1969                    .contains(FnCaps::LOCAL_ENVIRONMENT),
1970                "{name}"
1971            );
1972        }
1973        for name in [
1974            "IF",
1975            "IFERROR",
1976            "IFNA",
1977            "IFS",
1978            "SWITCH",
1979            "CHOOSE",
1980            "FILTER",
1981            "UNIQUE",
1982            "SEQUENCE",
1983            "TRANSPOSE",
1984            "TAKE",
1985            "DROP",
1986            "SORT",
1987            "SORTBY",
1988            "RANDARRAY",
1989            "HSTACK",
1990            "VSTACK",
1991            "TOCOL",
1992            "TOROW",
1993            "CHOOSECOLS",
1994            "CHOOSEROWS",
1995            "FREQUENCY",
1996            "LINEST",
1997            "TREND",
1998            "GROWTH",
1999            "LOGEST",
2000            "MODE.MULT",
2001            "TEXTSPLIT",
2002        ] {
2003            assert!(
2004                get("", name).unwrap().caps().contains(FnCaps::MAY_SPILL),
2005                "{name}"
2006            );
2007        }
2008        const SHORT_CIRCUIT: &[&str] = &[
2009            "IF", "IFERROR", "IFNA", "IFS", "SWITCH", "CHOOSE", "LET", "LAMBDA", "AND", "OR",
2010        ];
2011        let observed_short_circuit: std::collections::BTreeSet<_> = snapshot_registered()
2012            .into_iter()
2013            .filter(|(namespace, _, function)| {
2014                namespace.is_empty() && function.caps().contains(FnCaps::SHORT_CIRCUIT)
2015            })
2016            .map(|(_, name, _)| name)
2017            .collect();
2018        let expected_short_circuit: std::collections::BTreeSet<_> = SHORT_CIRCUIT
2019            .iter()
2020            .map(|name| (*name).to_string())
2021            .collect();
2022        assert_eq!(observed_short_circuit, expected_short_circuit);
2023        for name in SHORT_CIRCUIT {
2024            let contract = resolve_for_arity("", name, get("", name).unwrap().min_args())
2025                .unwrap()
2026                .semantics
2027                .contract
2028                .unwrap();
2029            assert_eq!(
2030                contract.evaluation,
2031                FunctionEvaluationSemantics::ShortCircuit,
2032                "{name}"
2033            );
2034        }
2035        assert_eq!(
2036            resolve_for_arity("", "CHOOSE", 2)
2037                .unwrap()
2038                .semantics
2039                .contract
2040                .unwrap()
2041                .result,
2042            FunctionResultSemantics::MayReturnReferenceAndSpill
2043        );
2044        for name in ["ROW", "COLUMN"] {
2045            let contract = resolve_for_arity("", name, 0)
2046                .unwrap()
2047                .semantics
2048                .contract
2049                .unwrap();
2050            assert_eq!(
2051                contract.context,
2052                crate::function_contract::FunctionContextDependence::PlacementDependent,
2053                "{name}"
2054            );
2055            assert_eq!(
2056                resolve_for_arity("", name, 1)
2057                    .unwrap()
2058                    .semantics
2059                    .contract
2060                    .unwrap()
2061                    .context,
2062                crate::function_contract::FunctionContextDependence::None,
2063                "{name} with argument"
2064            );
2065        }
2066        for name in ["ISFORMULA", "FORMULATEXT", "SHEET", "SHEETS"] {
2067            let contract = resolve_for_arity("", name, get("", name).unwrap().min_args())
2068                .unwrap()
2069                .semantics
2070                .contract
2071                .unwrap();
2072            assert_eq!(
2073                contract.context,
2074                crate::function_contract::FunctionContextDependence::WorkbookMetadata,
2075                "{name}"
2076            );
2077        }
2078    }
2079
2080    struct NamePanicFn;
2081    impl Function for NamePanicFn {
2082        fn name(&self) -> &'static str {
2083            panic!("name")
2084        }
2085        fn eval<'a, 'b, 'c>(
2086            &self,
2087            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
2088            _ctx: &dyn crate::traits::FunctionContext<'b>,
2089        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
2090            unreachable!()
2091        }
2092    }
2093    struct NamespacePanicFn;
2094    impl Function for NamespacePanicFn {
2095        fn name(&self) -> &'static str {
2096            "NS_PANIC"
2097        }
2098        fn namespace(&self) -> &'static str {
2099            panic!("namespace")
2100        }
2101        fn eval<'a, 'b, 'c>(
2102            &self,
2103            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
2104            _ctx: &dyn crate::traits::FunctionContext<'b>,
2105        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
2106            unreachable!()
2107        }
2108    }
2109
2110    #[test]
2111    fn canonical_metadata_panics_decline_registration_without_unwinding() {
2112        assert_eq!(
2113            try_register_function(Arc::new(NamePanicFn)),
2114            Err(RegistrationError::NameMetadataPanicked)
2115        );
2116        assert_eq!(
2117            try_register_function(Arc::new(NamespacePanicFn)),
2118            Err(RegistrationError::NamespaceMetadataPanicked)
2119        );
2120        register_function(Arc::new(NamePanicFn));
2121        register_function(Arc::new(NamespacePanicFn));
2122        assert!(get("", "NS_PANIC").is_none());
2123    }
2124
2125    #[derive(Clone, Copy)]
2126    enum BadSchemaKind {
2127        TooLarge,
2128        Repeating,
2129        MinDisagreement,
2130        RequiredCount,
2131        TooManyRequired,
2132        RepeatWidth,
2133    }
2134    struct BadSchemaFn {
2135        name: &'static str,
2136        kind: BadSchemaKind,
2137    }
2138    impl Function for BadSchemaFn {
2139        fn name(&self) -> &'static str {
2140            self.name
2141        }
2142        fn min_args(&self) -> usize {
2143            if matches!(
2144                self.kind,
2145                BadSchemaKind::MinDisagreement
2146                    | BadSchemaKind::RequiredCount
2147                    | BadSchemaKind::RepeatWidth
2148            ) {
2149                2
2150            } else {
2151                1
2152            }
2153        }
2154        fn variadic(&self) -> bool {
2155            matches!(self.kind, BadSchemaKind::RepeatWidth)
2156        }
2157        fn semantic_contract(&self, _arity: usize) -> Option<FunctionSemanticContract> {
2158            Some(FunctionSemanticContract::trusted_builtin_default(None))
2159        }
2160        fn arg_schema(&self) -> &'static [crate::args::ArgSchema] {
2161            static ONE: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
2162                std::sync::LazyLock::new(|| vec![crate::args::ArgSchema::any()]);
2163            static BAD_REPEAT: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
2164                std::sync::LazyLock::new(|| {
2165                    let mut arg = crate::args::ArgSchema::any();
2166                    arg.repeating = Some(0);
2167                    vec![arg]
2168                });
2169            static REQUIRED_COUNT: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
2170                std::sync::LazyLock::new(|| {
2171                    let mut optional = crate::args::ArgSchema::any();
2172                    optional.required = false;
2173                    vec![crate::args::ArgSchema::any(), optional]
2174                });
2175            static REPEAT_WIDTH: std::sync::LazyLock<Vec<crate::args::ArgSchema>> =
2176                std::sync::LazyLock::new(|| {
2177                    let first = crate::args::ArgSchema::any();
2178                    let mut second = crate::args::ArgSchema::any();
2179                    second.repeating = Some(2);
2180                    vec![first, second]
2181                });
2182            match self.kind {
2183                BadSchemaKind::TooLarge => &ONE,
2184                BadSchemaKind::Repeating => &BAD_REPEAT,
2185                BadSchemaKind::MinDisagreement => &[],
2186                BadSchemaKind::RequiredCount => &REQUIRED_COUNT,
2187                BadSchemaKind::TooManyRequired => &REPEAT_WIDTH,
2188                BadSchemaKind::RepeatWidth => &REPEAT_WIDTH,
2189            }
2190        }
2191        fn eval<'a, 'b, 'c>(
2192            &self,
2193            _args: &'c [crate::traits::ArgumentHandle<'a, 'b>],
2194            _ctx: &dyn crate::traits::FunctionContext<'b>,
2195        ) -> Result<crate::traits::CalcValue<'b>, formualizer_common::ExcelError> {
2196            unreachable!()
2197        }
2198    }
2199
2200    #[test]
2201    fn malformed_arity_and_schema_contracts_fail_closed() {
2202        for (name, kind, arity) in [
2203            ("TOO_LARGE", BadSchemaKind::TooLarge, 2),
2204            ("BAD_REPEAT", BadSchemaKind::Repeating, 1),
2205            ("MIN_DISAGREEMENT", BadSchemaKind::MinDisagreement, 2),
2206            ("REQUIRED_COUNT", BadSchemaKind::RequiredCount, 2),
2207            ("TOO_MANY_REQUIRED", BadSchemaKind::TooManyRequired, 1),
2208            ("REPEAT_WIDTH", BadSchemaKind::RepeatWidth, 3),
2209        ] {
2210            register_function(Arc::new(BadSchemaFn { name, kind }));
2211            let semantics = resolve_for_arity("", name, arity).unwrap().semantics;
2212            assert!(semantics.contract.is_none(), "{name}");
2213            assert!(
2214                semantics
2215                    .issues
2216                    .contains(&SemanticConformanceIssue::AritySchemaMismatch),
2217                "{name}: {:?}",
2218                semantics.issues
2219            );
2220        }
2221    }
2222
2223    #[test]
2224    fn valid_optional_and_width_n_repeating_schemas_conform() {
2225        let required = crate::args::ArgSchema::any();
2226        let mut optional = crate::args::ArgSchema::any();
2227        optional.required = false;
2228        assert!(schema_allows_arity(
2229            &[required.clone(), optional],
2230            1,
2231            false,
2232            2,
2233            true
2234        ));
2235
2236        let mut repeat_end = crate::args::ArgSchema::any();
2237        repeat_end.repeating = Some(2);
2238        let repeating = [required, repeat_end];
2239        assert!(schema_allows_arity(&repeating, 2, true, 4, true));
2240        assert!(!schema_allows_arity(&repeating, 2, true, 3, true));
2241    }
2242
2243    #[test]
2244    fn replacement_readers_observe_generation_and_epoch_atomically() {
2245        let ns = "__REG_SNAPSHOT_RACE__";
2246        register_builtin(Arc::new(TestFn {
2247            ns,
2248            name: "TARGET",
2249            aliases: &[],
2250        }));
2251        let (initial_epoch, initial) = resolve_with_epoch(ns, "TARGET").unwrap();
2252        let initial_generation = initial.semantics.generation;
2253        let barrier = Arc::new(std::sync::Barrier::new(5));
2254        let mut readers = Vec::new();
2255        for _ in 0..4 {
2256            let barrier = Arc::clone(&barrier);
2257            readers.push(std::thread::spawn(move || {
2258                barrier.wait();
2259                for _ in 0..1_000 {
2260                    let (epoch, resolved) = resolve_with_epoch(ns, "TARGET").unwrap();
2261                    if resolved.semantics.generation != initial_generation {
2262                        assert!(epoch > initial_epoch);
2263                    }
2264                }
2265            }));
2266        }
2267        barrier.wait();
2268        register_function(Arc::new(TestFn {
2269            ns,
2270            name: "TARGET",
2271            aliases: &[],
2272        }));
2273        for reader in readers {
2274            reader.join().unwrap();
2275        }
2276        let (epoch, resolved) = resolve_with_epoch(ns, "TARGET").unwrap();
2277        assert!(epoch > initial_epoch);
2278        assert!(resolved.semantics.generation > initial_generation);
2279    }
2280}