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