1use super::super::Type;
27use crate::types::branch_path;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum EffectDimension {
33 Snapshot,
35 Generative,
37 Output,
39 GenerativeOutput,
43}
44
45#[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#[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 ListHeader,
75 TerminalSize,
77}
78
79impl RuntimeType {
80 fn as_type(self) -> Type {
81 match self {
82 RuntimeType::Unknown => Type::Unknown,
83 RuntimeType::Unit => Type::Unit,
84 RuntimeType::Int => Type::Int,
85 RuntimeType::Float => Type::Float,
86 RuntimeType::Str => Type::Str,
87 RuntimeType::Bool => Type::Bool,
88 RuntimeType::OptionStr => Type::Option(Box::new(Type::Str)),
89 RuntimeType::ListStr => Type::List(Box::new(Type::Str)),
90 RuntimeType::ResultUnitStr => Type::Result(Box::new(Type::Unit), Box::new(Type::Str)),
91 RuntimeType::ResultStrStr => Type::Result(Box::new(Type::Str), Box::new(Type::Str)),
92 RuntimeType::ResultListStrStr => Type::Result(
93 Box::new(Type::List(Box::new(Type::Str))),
94 Box::new(Type::Str),
95 ),
96 RuntimeType::HttpResponseResult => Type::Result(
97 Box::new(Type::Named("HttpResponse".to_string())),
98 Box::new(Type::Str),
99 ),
100 RuntimeType::ListHeader => Type::List(Box::new(Type::Named("Header".to_string()))),
101 RuntimeType::TerminalSize => Type::Named("Terminal.Size".to_string()),
102 }
103 }
104}
105
106fn runtime_type(rt: RuntimeType) -> Type {
107 rt.as_type()
108}
109
110const CLASSIFICATIONS: &[EffectClassification] = &[
112 EffectClassification {
114 method: "Args.get",
115 dimension: EffectDimension::Snapshot,
116 runtime_params: &[],
117 runtime_return: RuntimeType::ListStr,
118 },
119 EffectClassification {
120 method: "Env.get",
121 dimension: EffectDimension::Snapshot,
122 runtime_params: &[RuntimeType::Str],
123 runtime_return: RuntimeType::OptionStr,
124 },
125 EffectClassification {
128 method: "Terminal.size",
129 dimension: EffectDimension::Snapshot,
130 runtime_params: &[],
131 runtime_return: RuntimeType::TerminalSize,
132 },
133 EffectClassification {
135 method: "Random.int",
136 dimension: EffectDimension::Generative,
137 runtime_params: &[RuntimeType::Int, RuntimeType::Int],
138 runtime_return: RuntimeType::Int,
139 },
140 EffectClassification {
141 method: "Random.float",
142 dimension: EffectDimension::Generative,
143 runtime_params: &[],
144 runtime_return: RuntimeType::Float,
145 },
146 EffectClassification {
147 method: "Time.now",
148 dimension: EffectDimension::Generative,
149 runtime_params: &[],
150 runtime_return: RuntimeType::Str,
151 },
152 EffectClassification {
153 method: "Time.unixMs",
154 dimension: EffectDimension::Generative,
155 runtime_params: &[],
156 runtime_return: RuntimeType::Int,
157 },
158 EffectClassification {
159 method: "Disk.readText",
160 dimension: EffectDimension::Generative,
161 runtime_params: &[RuntimeType::Str],
162 runtime_return: RuntimeType::ResultStrStr,
163 },
164 EffectClassification {
165 method: "Disk.exists",
166 dimension: EffectDimension::Generative,
167 runtime_params: &[RuntimeType::Str],
168 runtime_return: RuntimeType::Bool,
169 },
170 EffectClassification {
171 method: "Disk.listDir",
172 dimension: EffectDimension::Generative,
173 runtime_params: &[RuntimeType::Str],
174 runtime_return: RuntimeType::ResultListStrStr,
175 },
176 EffectClassification {
177 method: "Console.readLine",
178 dimension: EffectDimension::Generative,
179 runtime_params: &[],
180 runtime_return: RuntimeType::ResultStrStr,
181 },
182 EffectClassification {
184 method: "Http.get",
185 dimension: EffectDimension::GenerativeOutput,
186 runtime_params: &[RuntimeType::Str],
187 runtime_return: RuntimeType::HttpResponseResult,
188 },
189 EffectClassification {
190 method: "Http.head",
191 dimension: EffectDimension::GenerativeOutput,
192 runtime_params: &[RuntimeType::Str],
193 runtime_return: RuntimeType::HttpResponseResult,
194 },
195 EffectClassification {
196 method: "Http.delete",
197 dimension: EffectDimension::GenerativeOutput,
198 runtime_params: &[RuntimeType::Str],
199 runtime_return: RuntimeType::HttpResponseResult,
200 },
201 EffectClassification {
203 method: "Http.post",
204 dimension: EffectDimension::GenerativeOutput,
205 runtime_params: &[
206 RuntimeType::Str,
207 RuntimeType::Str,
208 RuntimeType::Str,
209 RuntimeType::ListHeader,
210 ],
211 runtime_return: RuntimeType::HttpResponseResult,
212 },
213 EffectClassification {
214 method: "Http.put",
215 dimension: EffectDimension::GenerativeOutput,
216 runtime_params: &[
217 RuntimeType::Str,
218 RuntimeType::Str,
219 RuntimeType::Str,
220 RuntimeType::ListHeader,
221 ],
222 runtime_return: RuntimeType::HttpResponseResult,
223 },
224 EffectClassification {
225 method: "Http.patch",
226 dimension: EffectDimension::GenerativeOutput,
227 runtime_params: &[
228 RuntimeType::Str,
229 RuntimeType::Str,
230 RuntimeType::Str,
231 RuntimeType::ListHeader,
232 ],
233 runtime_return: RuntimeType::HttpResponseResult,
234 },
235 EffectClassification {
239 method: "Disk.writeText",
240 dimension: EffectDimension::GenerativeOutput,
241 runtime_params: &[RuntimeType::Str, RuntimeType::Str],
242 runtime_return: RuntimeType::ResultUnitStr,
243 },
244 EffectClassification {
245 method: "Disk.appendText",
246 dimension: EffectDimension::GenerativeOutput,
247 runtime_params: &[RuntimeType::Str, RuntimeType::Str],
248 runtime_return: RuntimeType::ResultUnitStr,
249 },
250 EffectClassification {
251 method: "Disk.delete",
252 dimension: EffectDimension::GenerativeOutput,
253 runtime_params: &[RuntimeType::Str],
254 runtime_return: RuntimeType::ResultUnitStr,
255 },
256 EffectClassification {
257 method: "Disk.deleteDir",
258 dimension: EffectDimension::GenerativeOutput,
259 runtime_params: &[RuntimeType::Str],
260 runtime_return: RuntimeType::ResultUnitStr,
261 },
262 EffectClassification {
263 method: "Disk.makeDir",
264 dimension: EffectDimension::GenerativeOutput,
265 runtime_params: &[RuntimeType::Str],
266 runtime_return: RuntimeType::ResultUnitStr,
267 },
268 EffectClassification {
270 method: "Tcp.send",
271 dimension: EffectDimension::GenerativeOutput,
272 runtime_params: &[RuntimeType::Str, RuntimeType::Int, RuntimeType::Str],
273 runtime_return: RuntimeType::ResultStrStr,
274 },
275 EffectClassification {
276 method: "Tcp.ping",
277 dimension: EffectDimension::GenerativeOutput,
278 runtime_params: &[RuntimeType::Str, RuntimeType::Int],
279 runtime_return: RuntimeType::ResultUnitStr,
280 },
281 EffectClassification {
283 method: "Console.print",
284 dimension: EffectDimension::Output,
285 runtime_params: &[RuntimeType::Unknown],
286 runtime_return: RuntimeType::Unit,
287 },
288 EffectClassification {
289 method: "Console.error",
290 dimension: EffectDimension::Output,
291 runtime_params: &[RuntimeType::Unknown],
292 runtime_return: RuntimeType::Unit,
293 },
294 EffectClassification {
295 method: "Console.warn",
296 dimension: EffectDimension::Output,
297 runtime_params: &[RuntimeType::Unknown],
298 runtime_return: RuntimeType::Unit,
299 },
300 EffectClassification {
301 method: "Time.sleep",
302 dimension: EffectDimension::Output,
303 runtime_params: &[RuntimeType::Int],
304 runtime_return: RuntimeType::Unit,
305 },
306 EffectClassification {
307 method: "Terminal.clear",
308 dimension: EffectDimension::Output,
309 runtime_params: &[],
310 runtime_return: RuntimeType::Unit,
311 },
312 EffectClassification {
313 method: "Terminal.moveTo",
314 dimension: EffectDimension::Output,
315 runtime_params: &[RuntimeType::Int, RuntimeType::Int],
316 runtime_return: RuntimeType::Unit,
317 },
318 EffectClassification {
319 method: "Terminal.print",
320 dimension: EffectDimension::Output,
321 runtime_params: &[RuntimeType::Unknown],
322 runtime_return: RuntimeType::Unit,
323 },
324 EffectClassification {
325 method: "Terminal.readKey",
326 dimension: EffectDimension::Generative,
327 runtime_params: &[],
328 runtime_return: RuntimeType::OptionStr,
329 },
330 EffectClassification {
331 method: "Terminal.hideCursor",
332 dimension: EffectDimension::Output,
333 runtime_params: &[],
334 runtime_return: RuntimeType::Unit,
335 },
336 EffectClassification {
337 method: "Terminal.showCursor",
338 dimension: EffectDimension::Output,
339 runtime_params: &[],
340 runtime_return: RuntimeType::Unit,
341 },
342 EffectClassification {
343 method: "Terminal.flush",
344 dimension: EffectDimension::Output,
345 runtime_params: &[],
346 runtime_return: RuntimeType::Unit,
347 },
348];
349
350pub fn classify(method: &str) -> Option<&'static EffectClassification> {
352 CLASSIFICATIONS.iter().find(|c| c.method == method)
353}
354
355pub fn classifications_for_proof_subset() -> &'static [EffectClassification] {
358 CLASSIFICATIONS
359}
360
361pub fn is_classified(method: &str) -> bool {
363 classify(method).is_some()
364}
365
366pub fn oracle_signature(method: &str) -> Option<Type> {
375 let c = classify(method)?;
376 match c.dimension {
377 EffectDimension::Output => None,
378 EffectDimension::Snapshot => {
379 let params: Vec<Type> = c.runtime_params.iter().copied().map(runtime_type).collect();
380 Some(Type::Fn(
381 params,
382 Box::new(runtime_type(c.runtime_return)),
383 vec![],
384 ))
385 }
386 EffectDimension::Generative | EffectDimension::GenerativeOutput => {
387 let mut params = vec![Type::Named(branch_path::TYPE_NAME.to_string()), Type::Int];
388 params.extend(c.runtime_params.iter().copied().map(runtime_type));
389 Some(Type::Fn(
390 params,
391 Box::new(runtime_type(c.runtime_return)),
392 vec![],
393 ))
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn classify_returns_none_for_unknown() {
404 assert!(classify("Nope.missing").is_none());
405 assert!(classify("Args.set").is_none());
406 }
407
408 #[test]
409 fn args_get_is_snapshot() {
410 let c = classify("Args.get").unwrap();
411 assert_eq!(c.dimension, EffectDimension::Snapshot);
412 }
413
414 #[test]
415 fn random_int_is_generative() {
416 let c = classify("Random.int").unwrap();
417 assert_eq!(c.dimension, EffectDimension::Generative);
418 }
419
420 #[test]
421 fn http_get_is_generative_output() {
422 let c = classify("Http.get").unwrap();
423 assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
424 }
425
426 #[test]
427 fn disk_write_text_is_generative_output() {
428 let c = classify("Disk.writeText").unwrap();
429 assert_eq!(c.dimension, EffectDimension::GenerativeOutput);
430 }
431
432 #[test]
433 fn console_print_is_output() {
434 let c = classify("Console.print").unwrap();
435 assert_eq!(c.dimension, EffectDimension::Output);
436 }
437
438 #[test]
439 fn console_read_line_is_generative() {
440 let c = classify("Console.readLine").unwrap();
441 assert_eq!(c.dimension, EffectDimension::Generative);
442 }
443
444 #[test]
445 fn time_sleep_is_output() {
446 let c = classify("Time.sleep").unwrap();
447 assert_eq!(c.dimension, EffectDimension::Output);
448 }
449
450 #[test]
451 fn terminal_read_key_is_generative() {
452 let c = classify("Terminal.readKey").unwrap();
453 assert_eq!(c.dimension, EffectDimension::Generative);
454 }
455
456 #[test]
457 fn oracle_signature_for_random_int_is_branch_indexed() {
458 let sig = oracle_signature("Random.int").unwrap();
459 match sig {
461 Type::Fn(params, ret, _) => {
462 assert_eq!(params.len(), 4);
463 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
464 assert_eq!(params[1], Type::Int);
465 assert_eq!(params[2], Type::Int);
466 assert_eq!(params[3], Type::Int);
467 assert_eq!(*ret, Type::Int);
468 }
469 other => panic!("expected Fn, got {:?}", other),
470 }
471 }
472
473 #[test]
474 fn oracle_signature_for_random_float_is_branch_indexed_no_extra_args() {
475 let sig = oracle_signature("Random.float").unwrap();
476 match sig {
478 Type::Fn(params, ret, _) => {
479 assert_eq!(params.len(), 2);
480 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
481 assert_eq!(params[1], Type::Int);
482 assert_eq!(*ret, Type::Float);
483 }
484 other => panic!("expected Fn, got {:?}", other),
485 }
486 }
487
488 #[test]
489 fn oracle_signature_for_args_get_is_capability_reader() {
490 let sig = oracle_signature("Args.get").unwrap();
491 match sig {
493 Type::Fn(params, ret, _) => {
494 assert!(params.is_empty());
495 assert_eq!(*ret, Type::List(Box::new(Type::Str)));
496 }
497 other => panic!("expected Fn, got {:?}", other),
498 }
499 }
500
501 #[test]
502 fn oracle_signature_for_env_get_is_capability_reader() {
503 let sig = oracle_signature("Env.get").unwrap();
504 match sig {
506 Type::Fn(params, ret, _) => {
507 assert_eq!(params, vec![Type::Str]);
508 assert_eq!(*ret, Type::Option(Box::new(Type::Str)));
509 }
510 other => panic!("expected Fn, got {:?}", other),
511 }
512 }
513
514 #[test]
515 fn oracle_signature_for_http_get_is_branch_indexed() {
516 let sig = oracle_signature("Http.get").unwrap();
517 match sig {
519 Type::Fn(params, ret, _) => {
520 assert_eq!(params.len(), 3);
521 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
522 assert_eq!(params[1], Type::Int);
523 assert_eq!(params[2], Type::Str);
524 match *ret {
525 Type::Result(ok, err) => {
526 assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
527 assert_eq!(*err, Type::Str);
528 }
529 other => panic!("expected Result, got {:?}", other),
530 }
531 }
532 other => panic!("expected Fn, got {:?}", other),
533 }
534 }
535
536 #[test]
537 fn oracle_signature_for_console_read_line_is_branch_indexed() {
538 let sig = oracle_signature("Console.readLine").unwrap();
539 match sig {
541 Type::Fn(params, ret, _) => {
542 assert_eq!(params.len(), 2);
543 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
544 assert_eq!(params[1], Type::Int);
545 assert_eq!(*ret, Type::Result(Box::new(Type::Str), Box::new(Type::Str)));
546 }
547 other => panic!("expected Fn, got {:?}", other),
548 }
549 }
550
551 #[test]
552 fn oracle_signature_for_disk_list_dir_returns_result_list_string() {
553 let sig = oracle_signature("Disk.listDir").unwrap();
554 match sig {
556 Type::Fn(params, ret, _) => {
557 assert_eq!(params.len(), 3);
558 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
559 assert_eq!(params[1], Type::Int);
560 assert_eq!(params[2], Type::Str);
561 assert_eq!(
562 *ret,
563 Type::Result(
564 Box::new(Type::List(Box::new(Type::Str))),
565 Box::new(Type::Str)
566 )
567 );
568 }
569 other => panic!("expected Fn, got {:?}", other),
570 }
571 }
572
573 #[test]
574 fn oracle_signature_for_tcp_ping_returns_result_unit_string() {
575 let sig = oracle_signature("Tcp.ping").unwrap();
576 match sig {
578 Type::Fn(params, ret, _) => {
579 assert_eq!(params.len(), 4);
580 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
581 assert_eq!(params[1], Type::Int);
582 assert_eq!(params[2], Type::Str);
583 assert_eq!(params[3], Type::Int);
584 assert_eq!(
585 *ret,
586 Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
587 );
588 }
589 other => panic!("expected Fn, got {:?}", other),
590 }
591 }
592
593 #[test]
594 fn oracle_signature_for_disk_write_text_returns_result_unit_string() {
595 let sig = oracle_signature("Disk.writeText").unwrap();
596 match sig {
598 Type::Fn(params, ret, _) => {
599 assert_eq!(params.len(), 4);
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 assert_eq!(params[3], Type::Str);
604 assert_eq!(
605 *ret,
606 Type::Result(Box::new(Type::Unit), Box::new(Type::Str))
607 );
608 }
609 other => panic!("expected Fn, got {:?}", other),
610 }
611 }
612
613 #[test]
614 fn oracle_signature_for_output_effect_is_none() {
615 assert!(oracle_signature("Console.print").is_none());
616 assert!(oracle_signature("Console.error").is_none());
617 assert!(oracle_signature("Console.warn").is_none());
618 assert!(oracle_signature("Time.sleep").is_none());
619 assert!(oracle_signature("Terminal.print").is_none());
620 }
621
622 #[test]
623 fn is_classified_covers_full_v1_set() {
624 for name in &[
625 "Args.get",
626 "Env.get",
627 "Random.int",
628 "Random.float",
629 "Time.now",
630 "Time.unixMs",
631 "Time.sleep",
632 "Disk.readText",
633 "Disk.exists",
634 "Disk.listDir",
635 "Disk.writeText",
636 "Disk.appendText",
637 "Disk.delete",
638 "Disk.deleteDir",
639 "Disk.makeDir",
640 "Console.readLine",
641 "Http.get",
642 "Http.head",
643 "Http.delete",
644 "Http.post",
645 "Http.put",
646 "Http.patch",
647 "Tcp.send",
648 "Tcp.ping",
649 "Console.print",
650 "Console.error",
651 "Console.warn",
652 "Terminal.clear",
653 "Terminal.moveTo",
654 "Terminal.print",
655 "Terminal.readKey",
656 "Terminal.hideCursor",
657 "Terminal.showCursor",
658 "Terminal.flush",
659 ] {
660 assert!(is_classified(name), "{} should be classified", name);
661 }
662 }
663
664 #[test]
665 fn oracle_signature_for_http_post_has_four_runtime_params() {
666 let sig = oracle_signature("Http.post").unwrap();
667 match sig {
669 Type::Fn(params, ret, _) => {
670 assert_eq!(params.len(), 6);
671 assert!(matches!(params[0], Type::Named(ref n) if n == "BranchPath"));
672 assert_eq!(params[1], Type::Int);
673 assert_eq!(params[2], Type::Str);
674 assert_eq!(params[3], Type::Str);
675 assert_eq!(params[4], Type::Str);
676 match ¶ms[5] {
677 Type::List(inner) => {
678 assert!(matches!(&**inner, Type::Named(n) if n == "Header"));
679 }
680 other => panic!("expected List<Header>, got {:?}", other),
681 }
682 match *ret {
683 Type::Result(ok, err) => {
684 assert!(matches!(*ok, Type::Named(ref n) if n == "HttpResponse"));
685 assert_eq!(*err, Type::Str);
686 }
687 other => panic!("expected Result, got {:?}", other),
688 }
689 }
690 other => panic!("expected Fn, got {:?}", other),
691 }
692 }
693
694 #[test]
695 fn ambient_protocol_and_modal_effects_not_classified() {
696 for name in &[
698 "Env.set",
699 "Tcp.connect",
700 "Tcp.writeLine",
701 "Tcp.readLine",
702 "Tcp.close",
703 "HttpServer.listen",
704 "Terminal.enableRawMode",
705 "Terminal.disableRawMode",
706 "Terminal.setColor",
707 "Terminal.resetColor",
708 ] {
709 assert!(!is_classified(name), "{} should NOT be classified", name);
710 }
711 }
712
713 #[test]
714 fn terminal_size_is_snapshot() {
715 let c = classify("Terminal.size").expect("Terminal.size should be classified");
716 assert_eq!(c.dimension, EffectDimension::Snapshot);
717 assert!(c.runtime_params.is_empty());
718 assert_eq!(c.runtime_return, RuntimeType::TerminalSize);
719
720 let sig = oracle_signature("Terminal.size").unwrap();
722 match sig {
723 Type::Fn(params, ret, effects) => {
724 assert!(params.is_empty());
725 assert_eq!(*ret, Type::Named("Terminal.Size".to_string()));
726 assert!(effects.is_empty());
727 }
728 other => panic!("expected Fn, got {:?}", other),
729 }
730 }
731}