1use std::fmt;
14
15pub use origin_macros::BoundaryKind;
17pub use origin_macros::boundary_check;
18
19pub trait BoundaryKind: fmt::Debug + Send + Sync + 'static {}
27
28#[derive(Debug)]
37#[must_use = "this Value may be at a boundary that must be handled"]
38pub enum Value<T, B: BoundaryKind = GenericBoundary> {
39 Origin(B),
43 Boundary {
47 reason: B,
48 last: T,
49 },
50 Contents(T),
52}
53
54impl<T, B: BoundaryKind> Value<T, B> {
55 pub fn origin(reason: B) -> Self {
57 Value::Origin(reason)
58 }
59
60 pub fn boundary(reason: B, last: T) -> Self {
62 Value::Boundary { reason, last }
63 }
64
65 pub fn contents(value: T) -> Self {
67 Value::Contents(value)
68 }
69
70 pub fn is_contents(&self) -> bool {
71 matches!(self, Value::Contents(_))
72 }
73
74 pub fn is_origin(&self) -> bool {
75 matches!(self, Value::Origin(_))
76 }
77
78 pub fn is_boundary(&self) -> bool {
79 matches!(self, Value::Boundary { .. })
80 }
81
82 pub fn or(self, fallback: T) -> T {
87 match self {
88 Value::Contents(v) => v,
89 Value::Boundary { .. } => fallback,
90 Value::Origin(_) => fallback,
91 }
92 }
93
94 pub fn or_else(
101 self,
102 on_boundary: impl FnOnce(B, T) -> T,
103 on_origin: impl FnOnce(B) -> T,
104 ) -> T {
105 match self {
106 Value::Contents(v) => v,
107 Value::Boundary { reason, last } => on_boundary(reason, last),
108 Value::Origin(reason) => on_origin(reason),
109 }
110 }
111
112 pub fn unwrap(self) -> T {
114 match self {
115 Value::Contents(v) => v,
116 Value::Boundary { reason, .. } => {
117 panic!("called unwrap on a Boundary: {reason:?}")
118 }
119 Value::Origin(reason) => {
120 panic!("called unwrap on an Origin: {reason:?}")
121 }
122 }
123 }
124
125 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Value<U, B> {
132 match self {
133 Value::Contents(v) => Value::Contents(f(v)),
134 Value::Boundary { reason, last } => Value::Boundary {
135 reason,
136 last: f(last),
137 },
138 Value::Origin(reason) => Value::Origin(reason),
139 }
140 }
141
142 pub fn propagate(self) -> Result<T, B> {
149 match self {
150 Value::Contents(v) => Ok(v),
151 Value::Boundary { reason, .. } => Err(reason),
152 Value::Origin(reason) => Err(reason),
153 }
154 }
155
156 pub fn propagate_with_last(self) -> Result<T, (B, Option<T>)> {
162 match self {
163 Value::Contents(v) => Ok(v),
164 Value::Boundary { reason, last } => Err((reason, Some(last))),
165 Value::Origin(reason) => Err((reason, None)),
166 }
167 }
168
169 pub fn trace(self, chain: &mut Chain, label: &'static str) -> Result<T, B>
171 where
172 T: std::fmt::Debug,
173 {
174 match self {
175 Value::Contents(v) => {
176 chain.record(label, &v);
177 Ok(v)
178 }
179 Value::Boundary { reason, last } => {
180 chain.record_boundary(label, &last);
181 Err(reason)
182 }
183 Value::Origin(reason) => {
184 chain.record_origin(label);
185 Err(reason)
186 }
187 }
188 }
189
190 pub fn and_then<U>(self, f: impl FnOnce(T) -> Value<U, B>) -> Value<U, B> {
201 match self {
202 Value::Contents(v) => f(v),
203 Value::Boundary { reason, .. } => Value::Origin(reason),
204 Value::Origin(reason) => Value::Origin(reason),
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
213pub struct ChainEntry {
214 pub label: &'static str,
216 pub value: String,
218 pub is_boundary: bool,
220}
221
222#[derive(Debug, Clone, Default)]
228pub struct Chain {
229 entries: Vec<ChainEntry>,
230}
231
232impl Chain {
233 pub fn new() -> Self {
234 Chain { entries: Vec::new() }
235 }
236
237 pub fn record(&mut self, label: &'static str, value: &dyn fmt::Debug) {
239 self.entries.push(ChainEntry {
240 label,
241 value: format!("{value:?}"),
242 is_boundary: false,
243 });
244 }
245
246 pub fn record_boundary(&mut self, label: &'static str, value: &dyn fmt::Debug) {
248 self.entries.push(ChainEntry {
249 label,
250 value: format!("{value:?}"),
251 is_boundary: true,
252 });
253 }
254
255 pub fn record_origin(&mut self, label: &'static str) {
257 self.entries.push(ChainEntry {
258 label,
259 value: "⊘ origin".to_string(),
260 is_boundary: true,
261 });
262 }
263
264 pub fn entries(&self) -> &[ChainEntry] {
266 &self.entries
267 }
268
269 pub fn depth(&self) -> usize {
271 self.entries.iter().filter(|e| !e.is_boundary).count()
272 }
273}
274
275impl fmt::Display for Chain {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 for (i, entry) in self.entries.iter().enumerate() {
278 let marker = if entry.is_boundary { "✗" } else { "✓" };
279 write!(f, " {marker} [{}] {}: {}", i + 1, entry.label, entry.value)?;
280 if i < self.entries.len() - 1 {
281 writeln!(f)?;
282 }
283 }
284 Ok(())
285 }
286}
287
288#[derive(Debug, Clone)]
291pub struct GenericBoundary;
292impl BoundaryKind for GenericBoundary {}
293
294#[derive(Debug, Clone)]
296pub struct DivisionByZero;
297impl BoundaryKind for DivisionByZero {}
298
299#[derive(Debug, Clone)]
300pub struct Overflow;
301impl BoundaryKind for Overflow {}
302
303#[derive(Debug, Clone)]
304pub struct Underflow;
305impl BoundaryKind for Underflow {}
306
307#[derive(Debug, Clone)]
308pub struct Imaginary { pub real_part: f64 }
309impl BoundaryKind for Imaginary {}
310
311#[derive(Debug, Clone)]
313pub struct ModelUncertain { pub confidence: f64 }
314impl BoundaryKind for ModelUncertain {}
315
316#[derive(Debug, Clone)]
317pub struct Hallucinated { pub confidence: f64, pub grounding: Option<String> }
318impl BoundaryKind for Hallucinated {}
319
320#[derive(Debug, Clone)]
321pub struct Contradicted { pub sources: Vec<String>, pub claim: String }
322impl BoundaryKind for Contradicted {}
323
324pub fn divide(a: f64, b: f64) -> f64 {
340 a / b
341}
342
343pub fn sqrt(a: f64) -> f64 {
347 a.sqrt()
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn division_is_always_contents() {
356 assert_eq!(divide(10.0, 2.0), 5.0);
358 }
359
360 #[test]
361 fn division_by_zero_is_contents() {
362 assert!(divide(10.0, 0.0).is_infinite());
365 }
366
367 #[test]
368 fn zero_div_zero_is_contents() {
369 assert!(divide(0.0, 0.0).is_nan());
372 }
373
374 #[test]
375 fn sqrt_is_always_contents() {
376 assert_eq!(sqrt(9.0), 3.0);
377 }
378
379 #[test]
380 fn sqrt_negative_is_contents() {
381 assert!(sqrt(-1.0).is_nan());
383 }
384
385 #[test]
386 fn map_propagates_boundary() {
387 let v: Value<f64, DivisionByZero> = Value::boundary(DivisionByZero, 10.0);
388 let result = v.map(|v| v * 2.0);
389 assert!(result.is_boundary());
390 }
391
392 #[test]
393 fn map_transforms_contents() {
394 let v: Value<f64, DivisionByZero> = Value::contents(10.0);
395 let result = v.map(|v| v * 2.0);
396 assert_eq!(result.unwrap(), 20.0);
397 }
398
399 #[test]
400 fn map_propagates_origin() {
401 let v: Value<f64, DivisionByZero> = Value::origin(DivisionByZero);
402 let result = v.map(|x| x * 2.0);
403 assert!(result.is_origin());
404 }
405
406 #[test]
407 fn model_uncertain() {
408 let result: Value<String, ModelUncertain> = Value::Boundary {
409 reason: ModelUncertain { confidence: 0.3 },
410 last: "Paris is the capital of France".to_string(),
411 };
412 assert!(result.is_boundary());
413 }
414
415 #[test]
416 fn hallucinated() {
417 let result: Value<String, Hallucinated> = Value::Boundary {
418 reason: Hallucinated { confidence: 0.1, grounding: None },
419 last: "The moon is made of cheese".to_string(),
420 };
421 match result {
422 Value::Boundary { reason, .. } => assert!(reason.grounding.is_none()),
423 _ => panic!("expected boundary"),
424 }
425 }
426
427 #[test]
428 fn and_then_contents() {
429 let v: Value<f64, DivisionByZero> = Value::contents(10.0);
430 let result = v.and_then(|x| Value::contents(x / 2.0));
431 assert!(result.is_contents());
432 assert_eq!(result.unwrap(), 5.0);
433 }
434
435 #[test]
436 fn and_then_propagates_boundary_as_origin() {
437 let v: Value<f64, DivisionByZero> = Value::boundary(DivisionByZero, 10.0);
439 let result = v.and_then(|x| Value::contents(x / 2.0));
440 assert!(result.is_origin());
441 }
442
443 #[test]
444 fn origin_propagates_through_and_then() {
445 let v: Value<f64, DivisionByZero> = Value::origin(DivisionByZero);
446 let result = v.and_then(|x| Value::contents(x / 2.0));
447 assert!(result.is_origin());
448 }
449
450 #[test]
451 fn origin_or_fallback() {
452 let v: Value<f64, DivisionByZero> = Value::origin(DivisionByZero);
453 assert_eq!(v.or(42.0), 42.0);
454 }
455
456 fn make_value(a: f64, b: f64) -> Value<f64, DivisionByZero> {
459 if b.abs() < 1e-10 {
463 Value::boundary(DivisionByZero, a)
464 } else {
465 Value::contents(a / b)
466 }
467 }
468
469 #[boundary_check]
470 fn safe_divide(a: f64, b: f64) -> f64 {
471 make_value(a, b).or(0.0)
472 }
473
474 #[test]
475 fn boundary_check_allows_or() {
476 assert_eq!(safe_divide(10.0, 2.0), 5.0);
477 assert_eq!(safe_divide(10.0, 0.0), 0.0);
478 }
479
480 #[boundary_check]
481 fn safe_match(a: f64, b: f64) -> f64 {
482 match make_value(a, b) {
483 Value::Contents(v) => v,
484 Value::Boundary { reason: DivisionByZero, last } => last,
485 Value::Origin(DivisionByZero) => 0.0,
486 }
487 }
488
489 #[test]
490 fn boundary_check_allows_match() {
491 assert_eq!(safe_match(10.0, 2.0), 5.0);
492 assert_eq!(safe_match(10.0, 0.0), 10.0);
493 }
494
495 #[derive(Debug, Clone, BoundaryKind)]
498 #[boundary_kind(crate_path = "crate")]
499 struct InsufficientFunds {
500 balance: f64,
501 required: f64,
502 }
503
504 #[test]
505 fn user_defined_boundary() {
506 let result: Value<f64, InsufficientFunds> = Value::Boundary {
507 reason: InsufficientFunds { balance: 50.0, required: 100.0 },
508 last: 50.0,
509 };
510 assert!(result.is_boundary());
511 match result {
512 Value::Boundary { reason, .. } => {
513 assert_eq!(reason.balance, 50.0);
514 assert_eq!(reason.required, 100.0);
515 }
516 _ => panic!("expected boundary"),
517 }
518 }
519}