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