Skip to main content

origin_macros/
lib.rs

1//! # Origin Macros
2//!
3//! Proc-macros for the Origin boundary system.
4//!
5//! - `#[derive(BoundaryKind)]` — auto-implement the BoundaryKind trait
6//! - `#[boundary_check]` — enforce boundary handling at compile time
7
8use proc_macro::TokenStream;
9use quote::quote;
10use syn::{parse_macro_input, DeriveInput, ItemFn};
11use syn::visit_mut::{self, VisitMut};
12use syn::spanned::Spanned;
13
14/// Derive the `BoundaryKind` trait for a struct or enum.
15///
16/// ```ignore
17/// #[derive(Debug, Clone, BoundaryKind)]
18/// pub enum InferBoundary {
19///     LowConfidence { confidence: f64, threshold: f64 },
20///     Hallucinated  { claim: String, evidence_score: f64 },
21///     OutOfDomain   { distance: f64 },
22/// }
23/// ```
24///
25/// Use `#[boundary_kind(crate_path = "crate")]` when inside the origin crate itself.
26#[proc_macro_derive(BoundaryKind, attributes(boundary_kind))]
27pub fn derive_boundary_kind(input: TokenStream) -> TokenStream {
28    let input = parse_macro_input!(input as DeriveInput);
29    let name = &input.ident;
30    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
31
32    // Check for #[boundary_kind(crate_path = "...")] to support internal use.
33    let crate_path = input.attrs.iter()
34        .find(|a| a.path().is_ident("boundary_kind"))
35        .and_then(|a| {
36            let mut path = None;
37            let _ = a.parse_nested_meta(|meta| {
38                if meta.path.is_ident("crate_path") {
39                    let value = meta.value()?;
40                    let lit: syn::LitStr = value.parse()?;
41                    path = Some(lit.value());
42                }
43                Ok(())
44            });
45            path
46        });
47
48    let trait_path: syn::Path = match crate_path.as_deref() {
49        Some("crate") => syn::parse_quote!(crate::BoundaryKind),
50        Some(p) => syn::parse_str(&format!("{p}::BoundaryKind")).unwrap(),
51        None => syn::parse_quote!(origin::BoundaryKind),
52    };
53
54    let expanded = quote! {
55        impl #impl_generics #trait_path for #name #ty_generics #where_clause {}
56    };
57
58    TokenStream::from(expanded)
59}
60
61/// Enforce boundary handling at compile time.
62///
63/// Within a `#[boundary_check]` function:
64/// - `.unwrap()` is a compile error — handle the boundary explicitly
65/// - Wildcard `Boundary { .. }` match arms are a compile error —
66///   each boundary kind deserves its own handler
67///
68/// ```ignore
69/// #[boundary_check]
70/// fn safe_pipeline(x: f64) -> Value<f64, DivisionByZero> {
71///     let a = divide(10.0, x);
72///     // a.unwrap()  — compile error!
73///     a.or(0.0)      // explicit fallback — allowed
74/// }
75/// ```
76#[proc_macro_attribute]
77pub fn boundary_check(_attr: TokenStream, item: TokenStream) -> TokenStream {
78    let mut func = parse_macro_input!(item as ItemFn);
79
80    let mut checker = BoundaryChecker;
81    checker.visit_item_fn_mut(&mut func);
82
83    TokenStream::from(quote! { #func })
84}
85
86/// AST visitor that enforces boundary discipline.
87struct BoundaryChecker;
88
89impl VisitMut for BoundaryChecker {
90    fn visit_expr_method_call_mut(&mut self, node: &mut syn::ExprMethodCall) {
91        // Recurse first so nested calls are checked.
92        visit_mut::visit_expr_method_call_mut(self, node);
93
94        if (node.method == "unwrap" && node.args.is_empty())
95            || (node.method == "expect" && node.args.len() == 1)
96        {
97            let span = node.method.span();
98            let error = syn::parse_quote_spanned! { span =>
99                compile_error!(
100                    "boundary not handled: `.unwrap()` bypasses boundary checking\n\
101                     \n\
102                     A Value<T> may be at a boundary. Handle it explicitly:\n\
103                     \n\
104                     \x20 match value {\n\
105                     \x20     Value::Contents(v) => /* safe to use */,\n\
106                     \x20     Value::Boundary { reason, last } => /* crossed edge, has context */,\n\
107                     \x20     Value::Origin(reason) => /* absolute boundary, no value */,\n\
108                     \x20 }\n\
109                     \n\
110                     or use `.or(fallback)` to provide a default value"
111                )
112            };
113            *node.receiver = error;
114        }
115    }
116
117    fn visit_expr_match_mut(&mut self, node: &mut syn::ExprMatch) {
118        // Recurse into match arms first.
119        visit_mut::visit_expr_match_mut(self, node);
120
121        // Look for wildcard Boundary arms: `Value::Boundary { .. } =>`
122        // These dodge the point of boundary checking — each kind deserves its own arm.
123        for arm in &mut node.arms {
124            if is_wildcard_boundary_arm(&arm.pat) {
125                let span = arm.pat.span();
126                let error: syn::Expr = syn::parse_quote_spanned! { span =>
127                    compile_error!(
128                        "boundary kind ignored: wildcard `Boundary { .. }` catches all boundary kinds\n\
129                         \n\
130                         Each boundary kind exists for a reason. Handle them individually:\n\
131                         \n\
132                         \x20 Value::Contents(v) => /* safe to use */,\n\
133                         \x20 Value::Boundary { reason: Kind::LowConfidence { .. }, last } => /* refer */,\n\
134                         \x20 Value::Boundary { reason: Kind::Hallucinated { .. }, .. }   => /* refuse */,\n\
135                         \x20 Value::Origin(reason) => /* absolute boundary, no value */,\n\
136                         \n\
137                         A wildcard boundary arm is the new `.unwrap()` — it acknowledges\n\
138                         the boundary exists and then ignores what it means"
139                    )
140                };
141                arm.body = Box::new(error);
142            }
143        }
144    }
145}
146
147use syn::Pat;
148
149/// Detect patterns that wildcard over the Boundary variant.
150///
151/// Catches:
152/// - `Value::Boundary { .. }` — struct wildcard
153/// - `Value::Boundary { reason: _, .. }` — reason wildcarded
154/// - `_` — bare wildcard catches everything including Boundary
155/// - `x` — bare binding catches everything including Boundary
156fn is_wildcard_boundary_arm(pat: &Pat) -> bool {
157    match pat {
158        // Value::Boundary { .. } or Value::Boundary { reason: _, .. }
159        Pat::Struct(ps) => {
160            let path_str = path_to_string(&ps.path);
161            if !path_str.ends_with("Boundary") {
162                return false;
163            }
164            // If the `reason` field is present but is a wildcard `_`, that's a dodge.
165            // If `..` covers reason entirely, that's also a dodge.
166            let reason_field = ps.fields.iter().find(|f| {
167                f.member == syn::Member::Named(syn::Ident::new("reason", proc_macro2::Span::call_site()))
168            });
169            match reason_field {
170                None => true, // `..` covers reason — wildcard
171                Some(f) => matches!(*f.pat, Pat::Wild(_)), // `reason: _`
172            }
173        }
174        // Bare `_` wildcard catches everything including Boundary
175        Pat::Wild(_) => true,
176        // Bare binding `x` without subpattern catches everything
177        Pat::Ident(pi) => pi.subpat.is_none(),
178        _ => false,
179    }
180}
181
182fn path_to_string(path: &syn::Path) -> String {
183    path.segments.iter()
184        .map(|s| s.ident.to_string())
185        .collect::<Vec<_>>()
186        .join("::")
187}