Skip to main content

oximo_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use proc_macro::TokenStream;
5use proc_macro_crate::{FoundCrate, crate_name};
6use proc_macro2::{Delimiter, Spacing, TokenStream as TokenStream2, TokenTree};
7use quote::quote;
8use syn::Ident;
9
10mod bind;
11mod constraint;
12mod extrema;
13mod index;
14mod indicator;
15mod objective;
16mod param;
17mod set;
18mod soc;
19mod sos;
20mod sum;
21mod variable;
22
23use bind::{Binds, IndexBind};
24
25/// Resolve the path prefix used to reach `__macro_support`. Prefers the umbrella
26/// `oximo` crate (which re-exports the support module) and falls back to
27/// `oximo-core`.
28fn oximo_root() -> TokenStream2 {
29    fn to_path(found: &FoundCrate, fallback: &str) -> TokenStream2 {
30        let name = match found {
31            FoundCrate::Itself => fallback,
32            FoundCrate::Name(n) => n.as_str(),
33        };
34        let id = Ident::new(name, proc_macro2::Span::call_site());
35        quote!(::#id)
36    }
37
38    if let Ok(found) = crate_name("oximo") {
39        return to_path(&found, "oximo");
40    }
41    if let Ok(found) = crate_name("oximo-core") {
42        return to_path(&found, "oximo_core");
43    }
44    quote!(::oximo_core)
45}
46
47/// `variable!(model, spec)`, declare a decision variable (or an indexed family)
48/// and bind it to a local of the same name. See the crate docs for the grammar.
49#[proc_macro]
50pub fn variable(input: TokenStream) -> TokenStream {
51    variable::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
52}
53
54/// `constraint!(model, [name|name[idx]], lhs <op> rhs)`, register a constraint,
55/// an auto-named anonymous constraint, or an indexed family of constraints.
56///
57/// Single relations return `ConstraintHandle` for scalars and `IndexedConstraint<K>`
58/// for families. Two-sided ranges return `RangeConstraintHandles` for scalars and
59/// `IndexedRangeConstraint<K>` for families. Bind a result explicitly with
60/// `let handle = constraint!(...)` to query the registered rows.
61#[proc_macro]
62pub fn constraint(input: TokenStream) -> TokenStream {
63    constraint::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
64}
65
66/// Register a native binary-triggered affine constraint.
67/// `indicator_constraint!(model, [name|name[idx]], binary == 0|1 => relation)`.
68#[proc_macro]
69pub fn indicator_constraint(input: TokenStream) -> TokenStream {
70    indicator::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
71}
72
73/// `soc_constraint!(model, [name|name = expr|name[idx]], [terms] <= bound)`,
74/// register the second-order cone constraint `||terms||_2 <= bound` (every
75/// term and the bound must be affine), an auto-named anonymous cone, or an
76/// indexed family of cones.
77#[proc_macro]
78pub fn soc_constraint(input: TokenStream) -> TokenStream {
79    soc::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
80}
81
82/// `sos_constraint!(model, [name|name[idx]], SOS1|SOS2, [var, ...])`.
83/// Explicit weights can be supplied as `[(var, weight), ...]`.
84/// The short form assigns consecutive weights starting at one.
85#[proc_macro]
86pub fn sos_constraint(input: TokenStream) -> TokenStream {
87    sos::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
88}
89
90/// `objective!(model, Min|Max, expr)`, set the model objective and sense.
91/// `objective!(model, Feasibility)` (also `feasibility`/`feas`) declares a
92/// feasibility problem with no objective to optimize.
93#[proc_macro]
94pub fn objective(input: TokenStream) -> TokenStream {
95    objective::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
96}
97
98/// `sum!(body for pat in domain[, pat in domain ...])`, algebraic summation,
99/// lowered to nested `sum_over` folds. To allow an empty domain (or an empty
100/// filter), anchor the sum to its model: `sum!(model, body for pat in domain)`.
101/// Sums written inside `constraint!`, `soc_constraint!`, `objective!`, or an
102/// anchored sum inherit its expression context automatically. Selected terms
103/// must belong to that model. Standalone unanchored sums require a first term.
104#[proc_macro]
105pub fn sum(input: TokenStream) -> TokenStream {
106    sum::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
107}
108
109/// `min!(body for pat in domain[, pat in domain ...][ if cond])`, an indexed minimum.
110#[proc_macro]
111pub fn min(input: TokenStream) -> TokenStream {
112    extrema::expand(input.into(), extrema::Extremum::Min)
113        .unwrap_or_else(syn::Error::into_compile_error)
114        .into()
115}
116
117/// `max!(body for pat in domain[, pat in domain ...][ if cond])`, an indexed maximum.
118#[proc_macro]
119pub fn max(input: TokenStream) -> TokenStream {
120    extrema::expand(input.into(), extrema::Extremum::Max)
121        .unwrap_or_else(syn::Error::into_compile_error)
122        .into()
123}
124
125/// `param!(model, name = value)`, declare a re-bindable scalar parameter and
126/// bind it to a local of the same name.
127#[proc_macro]
128pub fn param(input: TokenStream) -> TokenStream {
129    param::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
130}
131
132/// `set!(name = domain)`, bind a local to an index `Set`. A plain right side
133/// (`0..5`, `a * b`) is normalized to an owned set (a top-level `*` is a borrowing
134/// Cartesian product). A `pat in domain[ if cond]` comprehension builds (and
135/// optionally filters) the set. See the crate docs.
136#[proc_macro]
137pub fn set(input: TokenStream) -> TokenStream {
138    set::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
139}
140
141// ---------------------------------------------------------------------------
142// Shared token-walking helpers. The macros must accept forms that are not valid
143// `syn::Expr` (indexed `name[i in set]`, chained `lb <= x <= ub`), so a few
144// splits are done at the raw token-tree level.
145// ---------------------------------------------------------------------------
146
147/// Relational operator recognized inside `constraint!`/`variable!`.
148#[derive(Copy, Clone, PartialEq, Eq)]
149enum RelOp {
150    Le,
151    Ge,
152    Eq,
153}
154
155impl RelOp {
156    /// The `Relate` method this operator maps to.
157    fn method(self) -> Ident {
158        let name = match self {
159            RelOp::Le => "le",
160            RelOp::Ge => "ge",
161            RelOp::Eq => "eq",
162        };
163        Ident::new(name, proc_macro2::Span::call_site())
164    }
165}
166
167/// Split a token stream on top-level commas.
168fn split_top_commas(ts: TokenStream2) -> Vec<TokenStream2> {
169    let mut out = Vec::new();
170    let mut cur = Vec::new();
171    for tt in ts {
172        if let TokenTree::Punct(p) = &tt
173            && p.as_char() == ','
174        {
175            out.push(cur.drain(..).collect());
176            continue;
177        }
178        cur.push(tt);
179    }
180    out.push(cur.into_iter().collect());
181    out
182}
183
184/// Split a token stream on top-level relational operators (`==`, `<=`, `>=`),
185/// returning the intervening segments and the operators between them.
186fn split_relops(ts: &TokenStream2) -> (Vec<TokenStream2>, Vec<RelOp>) {
187    let tts: Vec<TokenTree> = ts.clone().into_iter().collect();
188    let mut segs: Vec<TokenStream2> = Vec::new();
189    let mut ops: Vec<RelOp> = Vec::new();
190    let mut cur: Vec<TokenTree> = Vec::new();
191
192    let mut i = 0;
193    while i < tts.len() {
194        if let TokenTree::Punct(p1) = &tts[i]
195            && p1.spacing() == Spacing::Joint
196            && i + 1 < tts.len()
197            && let TokenTree::Punct(p2) = &tts[i + 1]
198        {
199            let op = match (p1.as_char(), p2.as_char()) {
200                ('<', '=') => Some(RelOp::Le),
201                ('>', '=') => Some(RelOp::Ge),
202                ('=', '=') => Some(RelOp::Eq),
203                _ => None,
204            };
205            if let Some(op) = op {
206                segs.push(cur.drain(..).collect());
207                ops.push(op);
208                i += 2;
209                continue;
210            }
211        }
212        cur.push(tts[i].clone());
213        i += 1;
214    }
215    segs.push(cur.into_iter().collect());
216    (segs, ops)
217}
218
219/// A parsed `name` or `name[binds]` "core" of a `variable!`/`constraint!`
220/// declaration. `cond` holds an optional `if` filter on the index family.
221struct Named {
222    name: Ident,
223    binds: Option<Vec<IndexBind>>,
224    cond: Option<syn::Expr>,
225}
226
227/// Parse a `name`/`name[i in dom, ...]` core out of a token segment.
228fn parse_named(seg: TokenStream2) -> syn::Result<Named> {
229    let tts: Vec<TokenTree> = seg.into_iter().collect();
230    let span = tts.first().map_or_else(proc_macro2::Span::call_site, TokenTree::span);
231    let TokenTree::Ident(name) = tts
232        .first()
233        .cloned()
234        .ok_or_else(|| syn::Error::new(span, "expected a variable/constraint name identifier"))?
235    else {
236        return Err(syn::Error::new(span, "expected a name identifier"));
237    };
238
239    let (binds, cond) = match tts.get(1) {
240        None => (None, None),
241        Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {
242            let parsed: Binds = syn::parse2(g.stream())?;
243            if parsed.binds.is_empty() {
244                return Err(syn::Error::new(
245                    g.span(),
246                    "index family needs at least one binding, e.g. `name[i in domain]`",
247                ));
248            }
249            (Some(parsed.binds), parsed.cond)
250        }
251        Some(other) => {
252            return Err(syn::Error::new(other.span(), "expected `[index in domain, ...]`"));
253        }
254    };
255    if let Some(extra) = tts.get(2) {
256        return Err(syn::Error::new(extra.span(), "unexpected tokens after the index bindings"));
257    }
258    Ok(Named { name, binds, cond })
259}
260
261/// Build an owned `Set` token expression from one or more index bindings.
262fn build_set(binds: &[IndexBind], root: &TokenStream2) -> syn::Result<TokenStream2> {
263    let Some((first, rest)) = binds.split_first() else {
264        return Err(syn::Error::new(
265            proc_macro2::Span::call_site(),
266            "an index family needs at least one binding",
267        ));
268    };
269    let dom = &first.domain;
270    let acc = quote!(#root::__macro_support::as_set(&(#dom)));
271    Ok(rest.iter().fold(acc, |set, b| {
272        let dom = &b.domain;
273        quote!(#root::__macro_support::product(
274            &(#set),
275            &(#root::__macro_support::as_set(&(#dom))),
276        ))
277    }))
278}
279
280/// Take the next operand segment of a relation.
281fn next_seg(segs: &mut std::vec::IntoIter<TokenStream2>) -> syn::Result<TokenStream2> {
282    segs.next().ok_or_else(|| {
283        syn::Error::new(proc_macro2::Span::call_site(), "malformed relation: missing an operand")
284    })
285}