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