Skip to main content

tensorlogic_adapters/
effects.rs

1//! Effect system for tracking computational effects.
2//!
3//! The effect system allows tracking what side effects a computation may have,
4//! enabling reasoning about purity, IO, state mutation, non-determinism, and more.
5//! This is useful for optimization (pure functions can be cached/memoized) and
6//! for ensuring safety properties.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use tensorlogic_adapters::{Effect, EffectSet, EffectContext, EffectHandler};
12//!
13//! // Create effect sets
14//! let pure = EffectSet::pure();
15//! let io = EffectSet::new().with(Effect::IO);
16//! let stateful = EffectSet::new().with(Effect::State);
17//!
18//! // Combine effects
19//! let combined = io.union(&stateful);
20//! assert!(combined.has(Effect::IO));
21//! assert!(combined.has(Effect::State));
22//!
23//! // Check purity
24//! assert!(pure.is_pure());
25//! assert!(!io.is_pure());
26//! ```
27
28use std::collections::{HashMap, HashSet};
29use std::fmt;
30
31/// A computational effect that can be tracked.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Effect {
34    /// Input/output operations
35    IO,
36    /// State mutation
37    State,
38    /// Non-determinism (random number generation, etc.)
39    NonDet,
40    /// Exceptions/errors
41    Exception,
42    /// Divergence (non-termination)
43    Diverge,
44    /// Memory allocation
45    Alloc,
46    /// Network communication
47    Network,
48    /// File system access
49    FileSystem,
50    /// Logging/tracing
51    Log,
52    /// Time-dependent operations
53    Time,
54    /// GPU operations
55    GPU,
56    /// Concurrency/parallelism
57    Concurrent,
58    /// Environment variable access
59    Env,
60    /// System calls
61    System,
62}
63
64impl Effect {
65    /// Get the effect name.
66    pub fn name(&self) -> &'static str {
67        match self {
68            Effect::IO => "IO",
69            Effect::State => "State",
70            Effect::NonDet => "NonDet",
71            Effect::Exception => "Exception",
72            Effect::Diverge => "Diverge",
73            Effect::Alloc => "Alloc",
74            Effect::Network => "Network",
75            Effect::FileSystem => "FileSystem",
76            Effect::Log => "Log",
77            Effect::Time => "Time",
78            Effect::GPU => "GPU",
79            Effect::Concurrent => "Concurrent",
80            Effect::Env => "Env",
81            Effect::System => "System",
82        }
83    }
84
85    /// Get a description of the effect.
86    pub fn description(&self) -> &'static str {
87        match self {
88            Effect::IO => "Input/output operations",
89            Effect::State => "State mutation",
90            Effect::NonDet => "Non-deterministic computation",
91            Effect::Exception => "May raise exceptions",
92            Effect::Diverge => "May not terminate",
93            Effect::Alloc => "Memory allocation",
94            Effect::Network => "Network communication",
95            Effect::FileSystem => "File system access",
96            Effect::Log => "Logging/tracing",
97            Effect::Time => "Time-dependent operations",
98            Effect::GPU => "GPU computation",
99            Effect::Concurrent => "Concurrent/parallel execution",
100            Effect::Env => "Environment variable access",
101            Effect::System => "System calls",
102        }
103    }
104
105    /// Check if this effect implies another.
106    ///
107    /// For example, FileSystem implies IO.
108    pub fn implies(&self, other: &Effect) -> bool {
109        match (self, other) {
110            (Effect::FileSystem, Effect::IO) => true,
111            (Effect::Network, Effect::IO) => true,
112            (Effect::GPU, Effect::Alloc) => true,
113            _ => self == other,
114        }
115    }
116}
117
118impl fmt::Display for Effect {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{}", self.name())
121    }
122}
123
124/// A set of computational effects.
125#[derive(Debug, Clone, Default, PartialEq, Eq)]
126pub struct EffectSet {
127    /// The effects in this set
128    effects: HashSet<Effect>,
129}
130
131impl EffectSet {
132    /// Create an empty (pure) effect set.
133    pub fn new() -> Self {
134        EffectSet {
135            effects: HashSet::new(),
136        }
137    }
138
139    /// Create a pure effect set (alias for new).
140    pub fn pure() -> Self {
141        Self::new()
142    }
143
144    /// Create an effect set with all effects.
145    pub fn all() -> Self {
146        let mut effects = HashSet::new();
147        effects.insert(Effect::IO);
148        effects.insert(Effect::State);
149        effects.insert(Effect::NonDet);
150        effects.insert(Effect::Exception);
151        effects.insert(Effect::Diverge);
152        effects.insert(Effect::Alloc);
153        effects.insert(Effect::Network);
154        effects.insert(Effect::FileSystem);
155        effects.insert(Effect::Log);
156        effects.insert(Effect::Time);
157        effects.insert(Effect::GPU);
158        effects.insert(Effect::Concurrent);
159        effects.insert(Effect::Env);
160        effects.insert(Effect::System);
161        EffectSet { effects }
162    }
163
164    /// Create an effect set from a single effect.
165    pub fn singleton(effect: Effect) -> Self {
166        let mut effects = HashSet::new();
167        effects.insert(effect);
168        EffectSet { effects }
169    }
170
171    /// Add an effect to the set.
172    pub fn with(mut self, effect: Effect) -> Self {
173        self.effects.insert(effect);
174        self
175    }
176
177    /// Add an effect to the set (mutable).
178    pub fn insert(&mut self, effect: Effect) {
179        self.effects.insert(effect);
180    }
181
182    /// Remove an effect from the set.
183    pub fn remove(&mut self, effect: &Effect) {
184        self.effects.remove(effect);
185    }
186
187    /// Check if the set contains an effect.
188    pub fn has(&self, effect: Effect) -> bool {
189        self.effects.contains(&effect)
190    }
191
192    /// Check if the set is pure (no effects).
193    pub fn is_pure(&self) -> bool {
194        self.effects.is_empty()
195    }
196
197    /// Check if the computation is total (no divergence or exceptions).
198    pub fn is_total(&self) -> bool {
199        !self.has(Effect::Diverge) && !self.has(Effect::Exception)
200    }
201
202    /// Check if the computation is deterministic.
203    pub fn is_deterministic(&self) -> bool {
204        !self.has(Effect::NonDet)
205    }
206
207    /// Get the union of two effect sets.
208    pub fn union(&self, other: &EffectSet) -> EffectSet {
209        let effects: HashSet<_> = self.effects.union(&other.effects).cloned().collect();
210        EffectSet { effects }
211    }
212
213    /// Get the intersection of two effect sets.
214    pub fn intersection(&self, other: &EffectSet) -> EffectSet {
215        let effects: HashSet<_> = self.effects.intersection(&other.effects).cloned().collect();
216        EffectSet { effects }
217    }
218
219    /// Get the difference of two effect sets (effects in self but not in other).
220    pub fn difference(&self, other: &EffectSet) -> EffectSet {
221        let effects: HashSet<_> = self.effects.difference(&other.effects).cloned().collect();
222        EffectSet { effects }
223    }
224
225    /// Check if this set is a subset of another.
226    pub fn is_subset_of(&self, other: &EffectSet) -> bool {
227        self.effects.is_subset(&other.effects)
228    }
229
230    /// Get the number of effects.
231    pub fn len(&self) -> usize {
232        self.effects.len()
233    }
234
235    /// Check if the set is empty.
236    pub fn is_empty(&self) -> bool {
237        self.effects.is_empty()
238    }
239
240    /// Iterate over the effects.
241    pub fn iter(&self) -> impl Iterator<Item = &Effect> {
242        self.effects.iter()
243    }
244
245    /// Get the effects as a vector.
246    pub fn to_vec(&self) -> Vec<Effect> {
247        self.effects.iter().cloned().collect()
248    }
249
250    /// Expand implied effects.
251    ///
252    /// For example, if FileSystem is present, IO is also added.
253    pub fn expand_implications(&mut self) {
254        let current: Vec<_> = self.effects.iter().cloned().collect();
255        for effect in current {
256            match effect {
257                Effect::FileSystem | Effect::Network => {
258                    self.effects.insert(Effect::IO);
259                }
260                Effect::GPU => {
261                    self.effects.insert(Effect::Alloc);
262                }
263                _ => {}
264            }
265        }
266    }
267}
268
269impl fmt::Display for EffectSet {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        if self.is_empty() {
272            write!(f, "Pure")
273        } else {
274            let names: Vec<_> = self.effects.iter().map(|e| e.name()).collect();
275            write!(f, "{{{}}}", names.join(", "))
276        }
277    }
278}
279
280/// An effect row for row polymorphism.
281///
282/// This allows functions to be polymorphic in their effects.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub enum EffectRow {
285    /// A closed row with specific effects
286    Closed(EffectSet),
287    /// An open row with effects and a tail variable
288    Open { effects: EffectSet, tail: String },
289}
290
291impl EffectRow {
292    /// Create a closed row from an effect set.
293    pub fn closed(effects: EffectSet) -> Self {
294        EffectRow::Closed(effects)
295    }
296
297    /// Create an open row with a tail variable.
298    pub fn open(effects: EffectSet, tail: impl Into<String>) -> Self {
299        EffectRow::Open {
300            effects,
301            tail: tail.into(),
302        }
303    }
304
305    /// Create a pure closed row.
306    pub fn pure() -> Self {
307        EffectRow::Closed(EffectSet::pure())
308    }
309
310    /// Check if this row contains an effect.
311    pub fn has(&self, effect: Effect) -> bool {
312        match self {
313            EffectRow::Closed(effects) => effects.has(effect),
314            EffectRow::Open { effects, .. } => effects.has(effect),
315        }
316    }
317
318    /// Get the free tail variables.
319    pub fn free_variables(&self) -> Vec<String> {
320        match self {
321            EffectRow::Closed(_) => vec![],
322            EffectRow::Open { tail, .. } => vec![tail.clone()],
323        }
324    }
325
326    /// Substitute a tail variable with an effect row.
327    pub fn substitute(&self, var: &str, row: &EffectRow) -> EffectRow {
328        match self {
329            EffectRow::Closed(effects) => EffectRow::Closed(effects.clone()),
330            EffectRow::Open { effects, tail } => {
331                if tail == var {
332                    match row {
333                        EffectRow::Closed(other_effects) => {
334                            EffectRow::Closed(effects.union(other_effects))
335                        }
336                        EffectRow::Open {
337                            effects: other_effects,
338                            tail: other_tail,
339                        } => EffectRow::Open {
340                            effects: effects.union(other_effects),
341                            tail: other_tail.clone(),
342                        },
343                    }
344                } else {
345                    EffectRow::Open {
346                        effects: effects.clone(),
347                        tail: tail.clone(),
348                    }
349                }
350            }
351        }
352    }
353}
354
355impl fmt::Display for EffectRow {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        match self {
358            EffectRow::Closed(effects) => write!(f, "{}", effects),
359            EffectRow::Open { effects, tail } => {
360                if effects.is_empty() {
361                    write!(f, "{}", tail)
362                } else {
363                    let names: Vec<_> = effects.iter().map(|e| e.name()).collect();
364                    write!(f, "{{{} | {}}}", names.join(", "), tail)
365                }
366            }
367        }
368    }
369}
370
371/// An effect handler that can handle specific effects.
372#[derive(Debug, Clone)]
373pub struct EffectHandler {
374    /// Name of the handler
375    pub name: String,
376    /// Effects that this handler can handle
377    pub handles: EffectSet,
378    /// Description
379    pub description: Option<String>,
380}
381
382impl EffectHandler {
383    /// Create a new effect handler.
384    pub fn new(name: impl Into<String>) -> Self {
385        EffectHandler {
386            name: name.into(),
387            handles: EffectSet::new(),
388            description: None,
389        }
390    }
391
392    /// Add an effect that this handler handles.
393    pub fn with_effect(mut self, effect: Effect) -> Self {
394        self.handles.insert(effect);
395        self
396    }
397
398    /// Set the description.
399    pub fn with_description(mut self, description: impl Into<String>) -> Self {
400        self.description = Some(description.into());
401        self
402    }
403
404    /// Check if this handler handles a specific effect.
405    pub fn handles(&self, effect: Effect) -> bool {
406        self.handles.has(effect)
407    }
408
409    /// Get the residual effects after handling.
410    pub fn residual(&self, effects: &EffectSet) -> EffectSet {
411        effects.difference(&self.handles)
412    }
413}
414
415/// Context for tracking effects.
416#[derive(Debug, Clone, Default)]
417pub struct EffectContext {
418    /// Effect annotations for functions/expressions
419    annotations: HashMap<String, EffectSet>,
420    /// Installed effect handlers
421    handlers: Vec<EffectHandler>,
422    /// Effect variables for polymorphism
423    variables: HashMap<String, EffectSet>,
424}
425
426impl EffectContext {
427    /// Create a new empty context.
428    pub fn new() -> Self {
429        EffectContext {
430            annotations: HashMap::new(),
431            handlers: Vec::new(),
432            variables: HashMap::new(),
433        }
434    }
435
436    /// Annotate an item with effects.
437    pub fn annotate(&mut self, name: impl Into<String>, effects: EffectSet) {
438        self.annotations.insert(name.into(), effects);
439    }
440
441    /// Get the effects for an item.
442    pub fn get_effects(&self, name: &str) -> Option<&EffectSet> {
443        self.annotations.get(name)
444    }
445
446    /// Install an effect handler.
447    pub fn install_handler(&mut self, handler: EffectHandler) {
448        self.handlers.push(handler);
449    }
450
451    /// Set an effect variable.
452    pub fn set_variable(&mut self, name: impl Into<String>, effects: EffectSet) {
453        self.variables.insert(name.into(), effects);
454    }
455
456    /// Get an effect variable.
457    pub fn get_variable(&self, name: &str) -> Option<&EffectSet> {
458        self.variables.get(name)
459    }
460
461    /// Compute the handled effects for a given effect set.
462    pub fn compute_residual(&self, effects: &EffectSet) -> EffectSet {
463        let mut residual = effects.clone();
464        for handler in &self.handlers {
465            residual = handler.residual(&residual);
466        }
467        residual
468    }
469
470    /// Check if all effects are handled.
471    pub fn all_handled(&self, effects: &EffectSet) -> bool {
472        self.compute_residual(effects).is_empty()
473    }
474
475    /// Get unhandled effects.
476    pub fn unhandled(&self, effects: &EffectSet) -> EffectSet {
477        self.compute_residual(effects)
478    }
479}
480
481/// Registry for effect-annotated functions.
482#[derive(Debug, Clone, Default)]
483pub struct EffectRegistry {
484    /// Function names to their effect signatures
485    functions: HashMap<String, EffectSignature>,
486}
487
488/// An effect signature for a function.
489#[derive(Debug, Clone)]
490pub struct EffectSignature {
491    /// Name of the function
492    pub name: String,
493    /// Effect row for the function
494    pub effects: EffectRow,
495    /// Description
496    pub description: Option<String>,
497}
498
499impl EffectRegistry {
500    /// Create a new empty registry.
501    pub fn new() -> Self {
502        EffectRegistry {
503            functions: HashMap::new(),
504        }
505    }
506
507    /// Create a registry with common function signatures.
508    pub fn with_builtins() -> Self {
509        let mut registry = EffectRegistry::new();
510
511        // Pure math functions
512        registry.register(EffectSignature {
513            name: "sin".to_string(),
514            effects: EffectRow::pure(),
515            description: Some("Sine function".to_string()),
516        });
517
518        registry.register(EffectSignature {
519            name: "cos".to_string(),
520            effects: EffectRow::pure(),
521            description: Some("Cosine function".to_string()),
522        });
523
524        registry.register(EffectSignature {
525            name: "exp".to_string(),
526            effects: EffectRow::pure(),
527            description: Some("Exponential function".to_string()),
528        });
529
530        // IO functions
531        registry.register(EffectSignature {
532            name: "print".to_string(),
533            effects: EffectRow::closed(EffectSet::singleton(Effect::IO)),
534            description: Some("Print to stdout".to_string()),
535        });
536
537        registry.register(EffectSignature {
538            name: "read_file".to_string(),
539            effects: EffectRow::closed(
540                EffectSet::new()
541                    .with(Effect::IO)
542                    .with(Effect::FileSystem)
543                    .with(Effect::Exception),
544            ),
545            description: Some("Read file contents".to_string()),
546        });
547
548        // Random functions
549        registry.register(EffectSignature {
550            name: "random".to_string(),
551            effects: EffectRow::closed(EffectSet::singleton(Effect::NonDet)),
552            description: Some("Generate random number".to_string()),
553        });
554
555        // GPU functions
556        registry.register(EffectSignature {
557            name: "gpu_matmul".to_string(),
558            effects: EffectRow::closed(EffectSet::new().with(Effect::GPU).with(Effect::Alloc)),
559            description: Some("GPU matrix multiplication".to_string()),
560        });
561
562        registry
563    }
564
565    /// Register a function signature.
566    pub fn register(&mut self, signature: EffectSignature) {
567        self.functions.insert(signature.name.clone(), signature);
568    }
569
570    /// Get a function signature.
571    pub fn get(&self, name: &str) -> Option<&EffectSignature> {
572        self.functions.get(name)
573    }
574
575    /// Check if a function is registered.
576    pub fn contains(&self, name: &str) -> bool {
577        self.functions.contains_key(name)
578    }
579
580    /// Get all function names.
581    pub fn function_names(&self) -> Vec<&str> {
582        self.functions.keys().map(|s| s.as_str()).collect()
583    }
584
585    /// Get the number of registered functions.
586    pub fn len(&self) -> usize {
587        self.functions.len()
588    }
589
590    /// Check if the registry is empty.
591    pub fn is_empty(&self) -> bool {
592        self.functions.is_empty()
593    }
594
595    /// Check if a function is pure.
596    pub fn is_pure(&self, name: &str) -> Option<bool> {
597        self.functions.get(name).map(|sig| match &sig.effects {
598            EffectRow::Closed(effects) => effects.is_pure(),
599            EffectRow::Open { effects, .. } => effects.is_pure(),
600        })
601    }
602}
603
604/// Infer effects for a sequence of operations.
605pub fn infer_effects(registry: &EffectRegistry, operations: &[&str]) -> EffectSet {
606    let mut effects = EffectSet::new();
607    for op in operations {
608        if let Some(sig) = registry.get(op) {
609            match &sig.effects {
610                EffectRow::Closed(op_effects) => {
611                    effects = effects.union(op_effects);
612                }
613                EffectRow::Open {
614                    effects: op_effects,
615                    ..
616                } => {
617                    effects = effects.union(op_effects);
618                }
619            }
620        }
621    }
622    effects
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    fn test_effect_set_operations() {
631        let io = EffectSet::singleton(Effect::IO);
632        let state = EffectSet::singleton(Effect::State);
633
634        let combined = io.union(&state);
635        assert!(combined.has(Effect::IO));
636        assert!(combined.has(Effect::State));
637        assert_eq!(combined.len(), 2);
638    }
639
640    #[test]
641    fn test_effect_set_pure() {
642        let pure = EffectSet::pure();
643        assert!(pure.is_pure());
644        assert!(pure.is_total());
645        assert!(pure.is_deterministic());
646    }
647
648    #[test]
649    fn test_effect_set_subset() {
650        let io = EffectSet::singleton(Effect::IO);
651        let combined = EffectSet::new().with(Effect::IO).with(Effect::State);
652
653        assert!(io.is_subset_of(&combined));
654        assert!(!combined.is_subset_of(&io));
655    }
656
657    #[test]
658    fn test_effect_set_difference() {
659        let combined = EffectSet::new().with(Effect::IO).with(Effect::State);
660        let io = EffectSet::singleton(Effect::IO);
661
662        let diff = combined.difference(&io);
663        assert!(!diff.has(Effect::IO));
664        assert!(diff.has(Effect::State));
665    }
666
667    #[test]
668    fn test_effect_row_closed() {
669        let row = EffectRow::closed(EffectSet::singleton(Effect::IO));
670        assert!(row.has(Effect::IO));
671        assert!(!row.has(Effect::State));
672    }
673
674    #[test]
675    fn test_effect_row_open() {
676        let row = EffectRow::open(EffectSet::singleton(Effect::IO), "e");
677        assert!(row.has(Effect::IO));
678        assert_eq!(row.free_variables(), vec!["e".to_string()]);
679    }
680
681    #[test]
682    fn test_effect_row_substitute() {
683        let row = EffectRow::open(EffectSet::singleton(Effect::IO), "e");
684        let tail = EffectRow::closed(EffectSet::singleton(Effect::State));
685
686        let result = row.substitute("e", &tail);
687        match result {
688            EffectRow::Closed(effects) => {
689                assert!(effects.has(Effect::IO));
690                assert!(effects.has(Effect::State));
691            }
692            _ => panic!("Expected closed row"),
693        }
694    }
695
696    #[test]
697    fn test_effect_handler() {
698        let handler = EffectHandler::new("io_handler")
699            .with_effect(Effect::IO)
700            .with_effect(Effect::FileSystem);
701
702        let effects = EffectSet::new()
703            .with(Effect::IO)
704            .with(Effect::State)
705            .with(Effect::FileSystem);
706
707        let residual = handler.residual(&effects);
708        assert!(!residual.has(Effect::IO));
709        assert!(!residual.has(Effect::FileSystem));
710        assert!(residual.has(Effect::State));
711    }
712
713    #[test]
714    fn test_effect_context() {
715        let mut ctx = EffectContext::new();
716
717        ctx.annotate("foo", EffectSet::singleton(Effect::IO));
718        ctx.annotate("bar", EffectSet::pure());
719
720        assert!(ctx.get_effects("foo").unwrap().has(Effect::IO));
721        assert!(ctx.get_effects("bar").unwrap().is_pure());
722    }
723
724    #[test]
725    fn test_effect_context_handlers() {
726        let mut ctx = EffectContext::new();
727
728        ctx.install_handler(EffectHandler::new("io").with_effect(Effect::IO));
729
730        let effects = EffectSet::new().with(Effect::IO).with(Effect::State);
731        let residual = ctx.compute_residual(&effects);
732
733        assert!(!residual.has(Effect::IO));
734        assert!(residual.has(Effect::State));
735    }
736
737    #[test]
738    fn test_effect_registry_builtins() {
739        let registry = EffectRegistry::with_builtins();
740
741        assert!(registry.is_pure("sin").unwrap());
742        assert!(!registry.is_pure("print").unwrap());
743        assert!(!registry.is_pure("random").unwrap());
744    }
745
746    #[test]
747    fn test_infer_effects() {
748        let registry = EffectRegistry::with_builtins();
749
750        let effects = infer_effects(&registry, &["sin", "cos"]);
751        assert!(effects.is_pure());
752
753        let effects = infer_effects(&registry, &["sin", "print"]);
754        assert!(effects.has(Effect::IO));
755    }
756
757    #[test]
758    fn test_effect_implies() {
759        assert!(Effect::FileSystem.implies(&Effect::IO));
760        assert!(Effect::Network.implies(&Effect::IO));
761        assert!(!Effect::IO.implies(&Effect::FileSystem));
762    }
763
764    #[test]
765    fn test_expand_implications() {
766        let mut effects = EffectSet::new().with(Effect::FileSystem);
767        effects.expand_implications();
768
769        assert!(effects.has(Effect::IO));
770        assert!(effects.has(Effect::FileSystem));
771    }
772
773    #[test]
774    fn test_effect_display() {
775        let pure = EffectSet::pure();
776        assert_eq!(format!("{}", pure), "Pure");
777
778        let row = EffectRow::open(EffectSet::singleton(Effect::IO), "e");
779        assert!(format!("{}", row).contains("IO"));
780        assert!(format!("{}", row).contains("e"));
781    }
782
783    #[test]
784    fn test_is_total() {
785        let effects = EffectSet::new().with(Effect::IO).with(Effect::State);
786        assert!(effects.is_total());
787
788        let effects = EffectSet::new().with(Effect::Exception);
789        assert!(!effects.is_total());
790    }
791
792    #[test]
793    fn test_all_effects() {
794        let all = EffectSet::all();
795        assert!(all.has(Effect::IO));
796        assert!(all.has(Effect::State));
797        assert!(all.has(Effect::GPU));
798        assert!(all.has(Effect::System));
799    }
800
801    #[test]
802    fn test_effect_signature() {
803        let sig = EffectSignature {
804            name: "my_func".to_string(),
805            effects: EffectRow::closed(EffectSet::singleton(Effect::IO)),
806            description: Some("My function".to_string()),
807        };
808
809        assert_eq!(sig.name, "my_func");
810        assert!(sig.effects.has(Effect::IO));
811    }
812}