Skip to main content

aver/types/checker/
effect_classification.rs

1//! Oracle v1 effect classification.
2//!
3//! For each built-in effect method covered by `aver proof`, this module
4//! records:
5//!
6//! - Which proof dimension(s) it belongs to (snapshot / generative / output,
7//!   and the combination `generative + output` used by e.g. `Http.get`).
8//! - For snapshot and generative, the corresponding capability/oracle
9//!   signature that lifted specs bind via `given name: E.m = [...]`.
10//!
11//! Output-only effects (for example `Console.print`, `Time.sleep`, and
12//! terminal drawing calls) are classified but do not have an oracle signature:
13//! they append to the per-branch trace segment and are asserted about via the
14//! trace API, not by binding an oracle in `given`.
15//!
16//! The table is the single source of truth consumed by:
17//!
18//! - `given`-clause type inference (`given rnd: Random.int` → oracle type
19//!   `(BranchPath, Int, Int, Int) -> Int`).
20//! - Lifting of effectful function bodies at proof-export time.
21//! - Rejection diagnostics for unclassified effects.
22//!
23//! Source of runtime signatures: `src/services/*.rs` and `docs/services.md`.
24//! Keep this table synchronized with the real built-ins.
25
26use super::super::Type;
27use crate::types::branch_path;
28
29/// Proof dimension(s) an effect participates in. `!`-combinations are
30/// modelled directly rather than as flags for readability at call sites.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum EffectDimension {
33    /// Stable within a run. Modelled as a plain reader function.
34    Snapshot,
35    /// Fresh value per call. Modelled as a branch-indexed oracle.
36    Generative,
37    /// Trace-appending side-effect only. No oracle; assertions via trace API.
38    Output,
39    /// Both generative (response value from oracle) and output (request
40    /// emitted to trace). Used by request/operation-style effects such as
41    /// `Http.*`, mutating `Disk.*`, and one-shot `Tcp.*`.
42    GenerativeOutput,
43}
44
45/// Classification of one effect method. `runtime_params` and
46/// `runtime_return` mirror the surface signature at call sites in user
47/// code; oracle signatures are derived from them (see [`oracle_signature`]).
48#[derive(Debug, Clone)]
49pub struct EffectClassification {
50    pub method: &'static str,
51    pub dimension: EffectDimension,
52    pub runtime_params: &'static [RuntimeType],
53    pub runtime_return: RuntimeType,
54}
55
56/// Compact carrier for runtime signature components — kept separate from
57/// the full [`Type`] enum so the static table can live as a const array.
58/// Converted into [`Type`] on demand via [`runtime_type`].
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum RuntimeType {
61    Unknown,
62    Unit,
63    Int,
64    Float,
65    Str,
66    Bool,
67    OptionStr,
68    ListStr,
69    ResultUnitStr,
70    ResultStrStr,
71    ResultListStrStr,
72    HttpResponseResult,
73    /// `List<Header>` — the headers argument on `Http.post/put/patch`.
74    ListHeader,
75    /// `Terminal.Size` record — the return of `Terminal.size`.
76    TerminalSize,
77    /// `Tcp.Connection` opaque token — argument/return of `Tcp.*` session methods.
78    TcpConnection,
79    /// `Result<Tcp.Connection, Str>` — return of `Tcp.connect`.
80    ResultTcpConnectionStr,
81}
82
83impl RuntimeType {
84    fn as_type(self) -> Type {
85        match self {
86            RuntimeType::Unknown => Type::Unknown,
87            RuntimeType::Unit => Type::Unit,
88            RuntimeType::Int => Type::Int,
89            RuntimeType::Float => Type::Float,
90            RuntimeType::Str => Type::Str,
91            RuntimeType::Bool => Type::Bool,
92            RuntimeType::OptionStr => Type::Option(Box::new(Type::Str)),
93            RuntimeType::ListStr => Type::List(Box::new(Type::Str)),
94            RuntimeType::ResultUnitStr => Type::Result(Box::new(Type::Unit), Box::new(Type::Str)),
95            RuntimeType::ResultStrStr => Type::Result(Box::new(Type::Str), Box::new(Type::Str)),
96            RuntimeType::ResultListStrStr => Type::Result(
97                Box::new(Type::List(Box::new(Type::Str))),
98                Box::new(Type::Str),
99            ),
100            RuntimeType::HttpResponseResult => Type::Result(
101                Box::new(Type::Named("HttpResponse".to_string())),
102                Box::new(Type::Str),
103            ),
104            RuntimeType::ListHeader => Type::List(Box::new(Type::Named("Header".to_string()))),
105            RuntimeType::TerminalSize => Type::Named("Terminal.Size".to_string()),
106            RuntimeType::TcpConnection => Type::Named("Tcp.Connection".to_string()),
107            RuntimeType::ResultTcpConnectionStr => Type::Result(
108                Box::new(Type::Named("Tcp.Connection".to_string())),
109                Box::new(Type::Str),
110            ),
111        }
112    }
113}
114
115fn runtime_type(rt: RuntimeType) -> Type {
116    rt.as_type()
117}
118
119/// Full classification table. This is the closed set for Oracle v1.
120const CLASSIFICATIONS: &[EffectClassification] = &[
121    // Snapshot
122    EffectClassification {
123        method: "Args.get",
124        dimension: EffectDimension::Snapshot,
125        runtime_params: &[],
126        runtime_return: RuntimeType::ListStr,
127    },
128    EffectClassification {
129        method: "Env.get",
130        dimension: EffectDimension::Snapshot,
131        runtime_params: &[RuntimeType::Str],
132        runtime_return: RuntimeType::OptionStr,
133    },
134    // Terminal.size: stable within a verify scope (a resize while
135    // proving is not modelled). Snapshot-shape oracle: () -> Terminal.Size.
136    EffectClassification {
137        method: "Terminal.size",
138        dimension: EffectDimension::Snapshot,
139        runtime_params: &[],
140        runtime_return: RuntimeType::TerminalSize,
141    },
142    // Generative
143    EffectClassification {
144        method: "Random.int",
145        dimension: EffectDimension::Generative,
146        runtime_params: &[RuntimeType::Int, RuntimeType::Int],
147        runtime_return: RuntimeType::Int,
148    },
149    EffectClassification {
150        method: "Random.float",
151        dimension: EffectDimension::Generative,
152        runtime_params: &[],
153        runtime_return: RuntimeType::Float,
154    },
155    EffectClassification {
156        method: "Time.now",
157        dimension: EffectDimension::Generative,
158        runtime_params: &[],
159        runtime_return: RuntimeType::Str,
160    },
161    EffectClassification {
162        method: "Time.unixMs",
163        dimension: EffectDimension::Generative,
164        runtime_params: &[],
165        runtime_return: RuntimeType::Int,
166    },
167    EffectClassification {
168        method: "Disk.readText",
169        dimension: EffectDimension::Generative,
170        runtime_params: &[RuntimeType::Str],
171        runtime_return: RuntimeType::ResultStrStr,
172    },
173    EffectClassification {
174        method: "Disk.exists",
175        dimension: EffectDimension::Generative,
176        runtime_params: &[RuntimeType::Str],
177        runtime_return: RuntimeType::Bool,
178    },
179    EffectClassification {
180        method: "Disk.listDir",
181        dimension: EffectDimension::Generative,
182        runtime_params: &[RuntimeType::Str],
183        runtime_return: RuntimeType::ResultListStrStr,
184    },
185    EffectClassification {
186        method: "Console.readLine",
187        dimension: EffectDimension::Generative,
188        runtime_params: &[],
189        runtime_return: RuntimeType::ResultStrStr,
190    },
191    // Generative + output (Http)
192    EffectClassification {
193        method: "Http.get",
194        dimension: EffectDimension::GenerativeOutput,
195        runtime_params: &[RuntimeType::Str],
196        runtime_return: RuntimeType::HttpResponseResult,
197    },
198    EffectClassification {
199        method: "Http.head",
200        dimension: EffectDimension::GenerativeOutput,
201        runtime_params: &[RuntimeType::Str],
202        runtime_return: RuntimeType::HttpResponseResult,
203    },
204    EffectClassification {
205        method: "Http.delete",
206        dimension: EffectDimension::GenerativeOutput,
207        runtime_params: &[RuntimeType::Str],
208        runtime_return: RuntimeType::HttpResponseResult,
209    },
210    // Http.post/.put/.patch — four-arg form `(url, body, contentType, headers)`.
211    EffectClassification {
212        method: "Http.post",
213        dimension: EffectDimension::GenerativeOutput,
214        runtime_params: &[
215            RuntimeType::Str,
216            RuntimeType::Str,
217            RuntimeType::Str,
218            RuntimeType::ListHeader,
219        ],
220        runtime_return: RuntimeType::HttpResponseResult,
221    },
222    EffectClassification {
223        method: "Http.put",
224        dimension: EffectDimension::GenerativeOutput,
225        runtime_params: &[
226            RuntimeType::Str,
227            RuntimeType::Str,
228            RuntimeType::Str,
229            RuntimeType::ListHeader,
230        ],
231        runtime_return: RuntimeType::HttpResponseResult,
232    },
233    EffectClassification {
234        method: "Http.patch",
235        dimension: EffectDimension::GenerativeOutput,
236        runtime_params: &[
237            RuntimeType::Str,
238            RuntimeType::Str,
239            RuntimeType::Str,
240            RuntimeType::ListHeader,
241        ],
242        runtime_return: RuntimeType::HttpResponseResult,
243    },
244    // Disk writes/deletes are modelled like HTTP writes: the operation is
245    // emitted to the trace, and success/failure comes from the oracle. Oracle
246    // does not assert persistent filesystem state after the operation.
247    EffectClassification {
248        method: "Disk.writeText",
249        dimension: EffectDimension::GenerativeOutput,
250        runtime_params: &[RuntimeType::Str, RuntimeType::Str],
251        runtime_return: RuntimeType::ResultUnitStr,
252    },
253    EffectClassification {
254        method: "Disk.appendText",
255        dimension: EffectDimension::GenerativeOutput,
256        runtime_params: &[RuntimeType::Str, RuntimeType::Str],
257        runtime_return: RuntimeType::ResultUnitStr,
258    },
259    EffectClassification {
260        method: "Disk.delete",
261        dimension: EffectDimension::GenerativeOutput,
262        runtime_params: &[RuntimeType::Str],
263        runtime_return: RuntimeType::ResultUnitStr,
264    },
265    EffectClassification {
266        method: "Disk.deleteDir",
267        dimension: EffectDimension::GenerativeOutput,
268        runtime_params: &[RuntimeType::Str],
269        runtime_return: RuntimeType::ResultUnitStr,
270    },
271    EffectClassification {
272        method: "Disk.makeDir",
273        dimension: EffectDimension::GenerativeOutput,
274        runtime_params: &[RuntimeType::Str],
275        runtime_return: RuntimeType::ResultUnitStr,
276    },
277    // One-shot TCP operations — request is trace output, response comes from oracle.
278    EffectClassification {
279        method: "Tcp.send",
280        dimension: EffectDimension::GenerativeOutput,
281        runtime_params: &[RuntimeType::Str, RuntimeType::Int, RuntimeType::Str],
282        runtime_return: RuntimeType::ResultStrStr,
283    },
284    EffectClassification {
285        method: "Tcp.ping",
286        dimension: EffectDimension::GenerativeOutput,
287        runtime_params: &[RuntimeType::Str, RuntimeType::Int],
288        runtime_return: RuntimeType::ResultUnitStr,
289    },
290    // Session TCP — connection is an opaque token. Stubs are stateless: a
291    // `writeLine` does not affect a later `readLine`. If a test wants
292    // request/response symmetry, it must encode that explicitly in the stub.
293    EffectClassification {
294        method: "Tcp.connect",
295        dimension: EffectDimension::GenerativeOutput,
296        runtime_params: &[RuntimeType::Str, RuntimeType::Int],
297        runtime_return: RuntimeType::ResultTcpConnectionStr,
298    },
299    EffectClassification {
300        method: "Tcp.readLine",
301        dimension: EffectDimension::GenerativeOutput,
302        runtime_params: &[RuntimeType::TcpConnection],
303        runtime_return: RuntimeType::ResultStrStr,
304    },
305    EffectClassification {
306        method: "Tcp.writeLine",
307        dimension: EffectDimension::GenerativeOutput,
308        runtime_params: &[RuntimeType::TcpConnection, RuntimeType::Str],
309        runtime_return: RuntimeType::ResultUnitStr,
310    },
311    EffectClassification {
312        method: "Tcp.close",
313        dimension: EffectDimension::GenerativeOutput,
314        runtime_params: &[RuntimeType::TcpConnection],
315        runtime_return: RuntimeType::ResultUnitStr,
316    },
317    // Output-only — no oracle signature, but classified for completeness.
318    // Env.set is stateless under Oracle: emitted to trace, but does NOT
319    // make a later `Env.get` return the written value. If the program
320    // depends on read-after-write consistency, the model belongs in pure
321    // user code, not in the effect oracle.
322    EffectClassification {
323        method: "Env.set",
324        dimension: EffectDimension::Output,
325        runtime_params: &[RuntimeType::Str, RuntimeType::Str],
326        runtime_return: RuntimeType::Unit,
327    },
328    EffectClassification {
329        method: "Console.print",
330        dimension: EffectDimension::Output,
331        runtime_params: &[RuntimeType::Unknown],
332        runtime_return: RuntimeType::Unit,
333    },
334    EffectClassification {
335        method: "Console.error",
336        dimension: EffectDimension::Output,
337        runtime_params: &[RuntimeType::Unknown],
338        runtime_return: RuntimeType::Unit,
339    },
340    EffectClassification {
341        method: "Console.warn",
342        dimension: EffectDimension::Output,
343        runtime_params: &[RuntimeType::Unknown],
344        runtime_return: RuntimeType::Unit,
345    },
346    EffectClassification {
347        method: "Time.sleep",
348        dimension: EffectDimension::Output,
349        runtime_params: &[RuntimeType::Int],
350        runtime_return: RuntimeType::Unit,
351    },
352    EffectClassification {
353        method: "Terminal.clear",
354        dimension: EffectDimension::Output,
355        runtime_params: &[],
356        runtime_return: RuntimeType::Unit,
357    },
358    EffectClassification {
359        method: "Terminal.moveTo",
360        dimension: EffectDimension::Output,
361        runtime_params: &[RuntimeType::Int, RuntimeType::Int],
362        runtime_return: RuntimeType::Unit,
363    },
364    EffectClassification {
365        method: "Terminal.print",
366        dimension: EffectDimension::Output,
367        runtime_params: &[RuntimeType::Unknown],
368        runtime_return: RuntimeType::Unit,
369    },
370    EffectClassification {
371        method: "Terminal.readKey",
372        dimension: EffectDimension::Generative,
373        runtime_params: &[],
374        runtime_return: RuntimeType::OptionStr,
375    },
376    EffectClassification {
377        method: "Terminal.hideCursor",
378        dimension: EffectDimension::Output,
379        runtime_params: &[],
380        runtime_return: RuntimeType::Unit,
381    },
382    EffectClassification {
383        method: "Terminal.showCursor",
384        dimension: EffectDimension::Output,
385        runtime_params: &[],
386        runtime_return: RuntimeType::Unit,
387    },
388    EffectClassification {
389        method: "Terminal.flush",
390        dimension: EffectDimension::Output,
391        runtime_params: &[],
392        runtime_return: RuntimeType::Unit,
393    },
394    // Terminal modal/visual — output only. Mode and color changes are
395    // observable via trace; the oracle does NOT model that a later `print`
396    // is "now in raw mode" or "now in red". If a test cares, it asserts the
397    // sequence of trace events.
398    EffectClassification {
399        method: "Terminal.enableRawMode",
400        dimension: EffectDimension::Output,
401        runtime_params: &[],
402        runtime_return: RuntimeType::Unit,
403    },
404    EffectClassification {
405        method: "Terminal.disableRawMode",
406        dimension: EffectDimension::Output,
407        runtime_params: &[],
408        runtime_return: RuntimeType::Unit,
409    },
410    EffectClassification {
411        method: "Terminal.setColor",
412        dimension: EffectDimension::Output,
413        runtime_params: &[RuntimeType::Str],
414        runtime_return: RuntimeType::Unit,
415    },
416    EffectClassification {
417        method: "Terminal.resetColor",
418        dimension: EffectDimension::Output,
419        runtime_params: &[],
420        runtime_return: RuntimeType::Unit,
421    },
422];
423
424/// Classify a built-in effect method, if it's in Oracle v1's closed set.
425pub fn classify(method: &str) -> Option<&'static EffectClassification> {
426    CLASSIFICATIONS.iter().find(|c| c.method == method)
427}
428
429/// Closed Oracle v1 proof-subset table, exposed for proof metadata
430/// generation. Callers must treat the returned slice as read-only metadata.
431pub fn classifications_for_proof_subset() -> &'static [EffectClassification] {
432    CLASSIFICATIONS
433}
434
435/// Return `true` if the given name refers to an effect covered by Oracle v1.
436pub fn is_classified(method: &str) -> bool {
437    classify(method).is_some()
438}
439
440/// Oracle signature for use in lifted specs.
441///
442/// - Snapshot: capability reader — unchanged from runtime signature,
443///   wrapped in a function type. `Args.get` → `() -> List<String>`.
444/// - Generative / GenerativeOutput: branch-indexed oracle —
445///   `(BranchPath, Int, <runtime_params...>) -> <runtime_return>`.
446/// - Output: `None` — output effects don't bind oracles (trace API
447///   handles assertions about emissions).
448pub fn oracle_signature(method: &str) -> Option<Type> {
449    let c = classify(method)?;
450    match c.dimension {
451        EffectDimension::Output => None,
452        EffectDimension::Snapshot => {
453            let params: Vec<Type> = c.runtime_params.iter().copied().map(runtime_type).collect();
454            Some(Type::Fn(
455                params,
456                Box::new(runtime_type(c.runtime_return)),
457                vec![],
458            ))
459        }
460        EffectDimension::Generative | EffectDimension::GenerativeOutput => {
461            let mut params = vec![Type::Named(branch_path::TYPE_NAME.to_string()), Type::Int];
462            params.extend(c.runtime_params.iter().copied().map(runtime_type));
463            Some(Type::Fn(
464                params,
465                Box::new(runtime_type(c.runtime_return)),
466                vec![],
467            ))
468        }
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    #[test]
477    fn classify_returns_none_for_unknown() {
478        assert!(classify("Nope.missing").is_none());
479        assert!(classify("Args.set").is_none());
480    }
481
482    #[test]
483    fn args_get_is_snapshot() {
484        let c = classify("Args.get").unwrap();
485        assert_eq!(c.dimension, EffectDimension::Snapshot);
486    }
487
488    #[test]
489    fn random_int_is_generative() {
490        let c = classify("Random.int").unwrap();
491        assert_eq!(c.dimension, EffectDimension::Generative);
492    }
493
494    #[test]
495    fn http_get_is_generative_output() {
496        let c = classify("Http.get").unwrap();
497        assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
498    }
499
500    #[test]
501    fn disk_write_text_is_generative_output() {
502        let c = classify("Disk.writeText").unwrap();
503        assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
504    }
505
506    #[test]
507    fn console_print_is_output() {
508        let c = classify("Console.print").unwrap();
509        assert_eq!(c.dimension, EffectDimension::Output);
510    }
511
512    #[test]
513    fn console_read_line_is_generative() {
514        let c = classify("Console.readLine").unwrap();
515        assert_eq!(c.dimension, EffectDimension::Generative);
516    }
517
518    #[test]
519    fn time_sleep_is_output() {
520        let c = classify("Time.sleep").unwrap();
521        assert_eq!(c.dimension, EffectDimension::Output);
522    }
523
524    #[test]
525    fn terminal_read_key_is_generative() {
526        let c = classify("Terminal.readKey").unwrap();
527        assert_eq!(c.dimension, EffectDimension::Generative);
528    }
529
530    #[test]
531    fn oracle_signature_for_random_int_is_branch_indexed() {
532        let sig = oracle_signature("Random.int").unwrap();
533        // (BranchPath, Int, Int, Int) -> Int
534        match sig {
535            Type::Fn(params, ret, _) => {
536                assert_eq!(params.len(), 4);
537                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
538                assert_eq!(params[1], Type::Int);
539                assert_eq!(params[2], Type::Int);
540                assert_eq!(params[3], Type::Int);
541                assert_eq!(*ret, Type::Int);
542            }
543            other => panic!("expected Fn, got {:?}", other),
544        }
545    }
546
547    #[test]
548    fn oracle_signature_for_random_float_is_branch_indexed_no_extra_args() {
549        let sig = oracle_signature("Random.float").unwrap();
550        // (BranchPath, Int) -> Float
551        match sig {
552            Type::Fn(params, ret, _) => {
553                assert_eq!(params.len(), 2);
554                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
555                assert_eq!(params[1], Type::Int);
556                assert_eq!(*ret, Type::Float);
557            }
558            other => panic!("expected Fn, got {:?}", other),
559        }
560    }
561
562    #[test]
563    fn oracle_signature_for_args_get_is_capability_reader() {
564        let sig = oracle_signature("Args.get").unwrap();
565        // () -> List<String>   (snapshot: not branch-indexed)
566        match sig {
567            Type::Fn(params, ret, _) => {
568                assert!(params.is_empty());
569                assert_eq!(*ret, Type::List(Box::new(Type::Str)));
570            }
571            other => panic!("expected Fn, got {:?}", other),
572        }
573    }
574
575    #[test]
576    fn oracle_signature_for_env_get_is_capability_reader() {
577        let sig = oracle_signature("Env.get").unwrap();
578        // String -> Option<String>
579        match sig {
580            Type::Fn(params, ret, _) => {
581                assert_eq!(params, vec![Type::Str]);
582                assert_eq!(*ret, Type::Option(Box::new(Type::Str)));
583            }
584            other => panic!("expected Fn, got {:?}", other),
585        }
586    }
587
588    #[test]
589    fn oracle_signature_for_http_get_is_branch_indexed() {
590        let sig = oracle_signature("Http.get").unwrap();
591        // (BranchPath, Int, String) -> Result<HttpResponse, String>
592        match sig {
593            Type::Fn(params, ret, _) => {
594                assert_eq!(params.len(), 3);
595                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
596                assert_eq!(params[1], Type::Int);
597                assert_eq!(params[2], Type::Str);
598                match *ret {
599                    Type::Result(ok, err) => {
600                        assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
601                        assert_eq!(*err, Type::Str);
602                    }
603                    other => panic!("expected Result, got {:?}", other),
604                }
605            }
606            other => panic!("expected Fn, got {:?}", other),
607        }
608    }
609
610    #[test]
611    fn oracle_signature_for_console_read_line_is_branch_indexed() {
612        let sig = oracle_signature("Console.readLine").unwrap();
613        // (BranchPath, Int) -> Result<String, String>
614        match sig {
615            Type::Fn(params, ret, _) => {
616                assert_eq!(params.len(), 2);
617                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
618                assert_eq!(params[1], Type::Int);
619                assert_eq!(*ret, Type::Result(Box::new(Type::Str), Box::new(Type::Str)));
620            }
621            other => panic!("expected Fn, got {:?}", other),
622        }
623    }
624
625    #[test]
626    fn oracle_signature_for_disk_list_dir_returns_result_list_string() {
627        let sig = oracle_signature("Disk.listDir").unwrap();
628        // (BranchPath, Int, String) -> Result<List<String>, String>
629        match sig {
630            Type::Fn(params, ret, _) => {
631                assert_eq!(params.len(), 3);
632                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
633                assert_eq!(params[1], Type::Int);
634                assert_eq!(params[2], Type::Str);
635                assert_eq!(
636                    *ret,
637                    Type::Result(
638                        Box::new(Type::List(Box::new(Type::Str))),
639                        Box::new(Type::Str)
640                    )
641                );
642            }
643            other => panic!("expected Fn, got {:?}", other),
644        }
645    }
646
647    #[test]
648    fn oracle_signature_for_tcp_ping_returns_result_unit_string() {
649        let sig = oracle_signature("Tcp.ping").unwrap();
650        // (BranchPath, Int, String, Int) -> Result<Unit, String>
651        match sig {
652            Type::Fn(params, ret, _) => {
653                assert_eq!(params.len(), 4);
654                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
655                assert_eq!(params[1], Type::Int);
656                assert_eq!(params[2], Type::Str);
657                assert_eq!(params[3], Type::Int);
658                assert_eq!(
659                    *ret,
660                    Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
661                );
662            }
663            other => panic!("expected Fn, got {:?}", other),
664        }
665    }
666
667    #[test]
668    fn oracle_signature_for_disk_write_text_returns_result_unit_string() {
669        let sig = oracle_signature("Disk.writeText").unwrap();
670        // (BranchPath, Int, String, String) -> Result<Unit, String>
671        match sig {
672            Type::Fn(params, ret, _) => {
673                assert_eq!(params.len(), 4);
674                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
675                assert_eq!(params[1], Type::Int);
676                assert_eq!(params[2], Type::Str);
677                assert_eq!(params[3], Type::Str);
678                assert_eq!(
679                    *ret,
680                    Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
681                );
682            }
683            other => panic!("expected Fn, got {:?}", other),
684        }
685    }
686
687    #[test]
688    fn oracle_signature_for_output_effect_is_none() {
689        assert!(oracle_signature("Console.print").is_none());
690        assert!(oracle_signature("Console.error").is_none());
691        assert!(oracle_signature("Console.warn").is_none());
692        assert!(oracle_signature("Time.sleep").is_none());
693        assert!(oracle_signature("Terminal.print").is_none());
694    }
695
696    #[test]
697    fn is_classified_covers_full_v1_set() {
698        for name in &[
699            "Args.get",
700            "Env.get",
701            "Random.int",
702            "Random.float",
703            "Time.now",
704            "Time.unixMs",
705            "Time.sleep",
706            "Disk.readText",
707            "Disk.exists",
708            "Disk.listDir",
709            "Disk.writeText",
710            "Disk.appendText",
711            "Disk.delete",
712            "Disk.deleteDir",
713            "Disk.makeDir",
714            "Console.readLine",
715            "Http.get",
716            "Http.head",
717            "Http.delete",
718            "Http.post",
719            "Http.put",
720            "Http.patch",
721            "Tcp.send",
722            "Tcp.ping",
723            "Console.print",
724            "Console.error",
725            "Console.warn",
726            "Terminal.clear",
727            "Terminal.moveTo",
728            "Terminal.print",
729            "Terminal.readKey",
730            "Terminal.hideCursor",
731            "Terminal.showCursor",
732            "Terminal.flush",
733        ] {
734            assert!(is_classified(name), "{} should be classified", name);
735        }
736    }
737
738    #[test]
739    fn oracle_signature_for_http_post_has_four_runtime_params() {
740        let sig = oracle_signature("Http.post").unwrap();
741        // (BranchPath, Int, Str, Str, Str, List<Header>) -> Result<HttpResponse, String>
742        match sig {
743            Type::Fn(params, ret, _) => {
744                assert_eq!(params.len(), 6);
745                assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
746                assert_eq!(params[1], Type::Int);
747                assert_eq!(params[2], Type::Str);
748                assert_eq!(params[3], Type::Str);
749                assert_eq!(params[4], Type::Str);
750                match &params[5] {
751                    Type::List(inner) => {
752                        assert!(matches!(&**inner, Type::Named(n) if n == "Header"));
753                    }
754                    other => panic!("expected List<Header>, got {:?}", other),
755                }
756                match *ret {
757                    Type::Result(ok, err) => {
758                        assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
759                        assert_eq!(*err, Type::Str);
760                    }
761                    other => panic!("expected Result, got {:?}", other),
762                }
763            }
764            other => panic!("expected Fn, got {:?}", other),
765        }
766    }
767
768    #[test]
769    fn server_lifecycle_remains_unclassified() {
770        // HttpServer.listen is a long-running protocol with callbacks — its
771        // handler is the unit of proof, not the lifecycle call itself. Stays
772        // outside Oracle by design.
773        for name in &["HttpServer.listen", "HttpServer.listenWith"] {
774            assert!(!is_classified(name), "{} should NOT be classified", name);
775        }
776    }
777
778    #[test]
779    fn extended_oracle_v1_methods_classified() {
780        // Output: env writes and terminal modal/visual changes — emitted to
781        // trace, no oracle signature.
782        for name in &[
783            "Env.set",
784            "Terminal.enableRawMode",
785            "Terminal.disableRawMode",
786            "Terminal.setColor",
787            "Terminal.resetColor",
788        ] {
789            let c = classify(name).unwrap_or_else(|| panic!("{} should be classified", name));
790            assert_eq!(c.dimension, EffectDimension::Output);
791        }
792        // GenerativeOutput: session TCP — request emitted, response from oracle.
793        // Stateless: writeLine does not affect a later readLine.
794        for name in &["Tcp.connect", "Tcp.readLine", "Tcp.writeLine", "Tcp.close"] {
795            let c = classify(name).unwrap_or_else(|| panic!("{} should be classified", name));
796            assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
797        }
798    }
799
800    #[test]
801    fn terminal_size_is_snapshot() {
802        let c = classify("Terminal.size").expect("Terminal.size should be classified");
803        assert_eq!(c.dimension, EffectDimension::Snapshot);
804        assert!(c.runtime_params.is_empty());
805        assert_eq!(c.runtime_return, RuntimeType::TerminalSize);
806
807        // Oracle signature: () -> Terminal.Size
808        let sig = oracle_signature("Terminal.size").unwrap();
809        match sig {
810            Type::Fn(params, ret, effects) => {
811                assert!(params.is_empty());
812                assert_eq!(*ret, Type::Named("Terminal.Size".to_string()));
813                assert!(effects.is_empty());
814            }
815            other => panic!("expected Fn, got {:?}", other),
816        }
817    }
818}