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 index;
13mod objective;
14mod param;
15mod set;
16mod sum;
17mod variable;
18
19use bind::{Binds, IndexBind};
20
21fn oximo_root() -> TokenStream2 {
25 fn to_path(found: &FoundCrate, fallback: &str) -> TokenStream2 {
26 let name = match found {
27 FoundCrate::Itself => fallback,
28 FoundCrate::Name(n) => n.as_str(),
29 };
30 let id = Ident::new(name, proc_macro2::Span::call_site());
31 quote!(::#id)
32 }
33
34 if let Ok(found) = crate_name("oximo") {
35 return to_path(&found, "oximo");
36 }
37 if let Ok(found) = crate_name("oximo-core") {
38 return to_path(&found, "oximo_core");
39 }
40 quote!(::oximo_core)
41}
42
43#[proc_macro]
46pub fn variable(input: TokenStream) -> TokenStream {
47 variable::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
48}
49
50#[proc_macro]
53pub fn constraint(input: TokenStream) -> TokenStream {
54 constraint::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
55}
56
57#[proc_macro]
59pub fn objective(input: TokenStream) -> TokenStream {
60 objective::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
61}
62
63#[proc_macro]
66pub fn sum(input: TokenStream) -> TokenStream {
67 sum::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
68}
69
70#[proc_macro]
73pub fn param(input: TokenStream) -> TokenStream {
74 param::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
75}
76
77#[proc_macro]
82pub fn set(input: TokenStream) -> TokenStream {
83 set::expand(input.into()).unwrap_or_else(syn::Error::into_compile_error).into()
84}
85
86#[derive(Copy, Clone, PartialEq, Eq)]
94enum RelOp {
95 Le,
96 Ge,
97 Eq,
98}
99
100impl RelOp {
101 fn method(self) -> Ident {
103 let name = match self {
104 RelOp::Le => "le",
105 RelOp::Ge => "ge",
106 RelOp::Eq => "eq",
107 };
108 Ident::new(name, proc_macro2::Span::call_site())
109 }
110}
111
112fn split_top_commas(ts: TokenStream2) -> Vec<TokenStream2> {
114 let mut out = Vec::new();
115 let mut cur = Vec::new();
116 for tt in ts {
117 if let TokenTree::Punct(p) = &tt {
118 if p.as_char() == ',' {
119 out.push(cur.drain(..).collect());
120 continue;
121 }
122 }
123 cur.push(tt);
124 }
125 out.push(cur.into_iter().collect());
126 out
127}
128
129fn split_relops(ts: TokenStream2) -> (Vec<TokenStream2>, Vec<RelOp>) {
132 let tts: Vec<TokenTree> = ts.into_iter().collect();
133 let mut segs: Vec<TokenStream2> = Vec::new();
134 let mut ops: Vec<RelOp> = Vec::new();
135 let mut cur: Vec<TokenTree> = Vec::new();
136
137 let mut i = 0;
138 while i < tts.len() {
139 if let TokenTree::Punct(p1) = &tts[i] {
140 if p1.spacing() == Spacing::Joint && i + 1 < tts.len() {
141 if let TokenTree::Punct(p2) = &tts[i + 1] {
142 let op = match (p1.as_char(), p2.as_char()) {
143 ('<', '=') => Some(RelOp::Le),
144 ('>', '=') => Some(RelOp::Ge),
145 ('=', '=') => Some(RelOp::Eq),
146 _ => None,
147 };
148 if let Some(op) = op {
149 segs.push(cur.drain(..).collect());
150 ops.push(op);
151 i += 2;
152 continue;
153 }
154 }
155 }
156 }
157 cur.push(tts[i].clone());
158 i += 1;
159 }
160 segs.push(cur.into_iter().collect());
161 (segs, ops)
162}
163
164struct Named {
167 name: Ident,
168 binds: Option<Vec<IndexBind>>,
169 cond: Option<syn::Expr>,
170}
171
172fn parse_named(seg: TokenStream2) -> syn::Result<Named> {
174 let tts: Vec<TokenTree> = seg.into_iter().collect();
175 let span = tts.first().map_or_else(proc_macro2::Span::call_site, TokenTree::span);
176 let TokenTree::Ident(name) = tts
177 .first()
178 .cloned()
179 .ok_or_else(|| syn::Error::new(span, "expected a variable/constraint name identifier"))?
180 else {
181 return Err(syn::Error::new(span, "expected a name identifier"));
182 };
183
184 let (binds, cond) = match tts.get(1) {
185 None => (None, None),
186 Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket => {
187 let parsed: Binds = syn::parse2(g.stream())?;
188 if parsed.binds.is_empty() {
189 return Err(syn::Error::new(
190 g.span(),
191 "index family needs at least one binding, e.g. `name[i in domain]`",
192 ));
193 }
194 (Some(parsed.binds), parsed.cond)
195 }
196 Some(other) => {
197 return Err(syn::Error::new(other.span(), "expected `[index in domain, ...]`"));
198 }
199 };
200 Ok(Named { name, binds, cond })
201}
202
203fn build_set(binds: &[IndexBind], root: &TokenStream2) -> TokenStream2 {
205 let mut iter = binds.iter().map(|b| {
206 let dom = &b.domain;
207 quote!(#root::__macro_support::as_set(&(#dom)))
208 });
209 let first = iter.next().expect("at least one index binding");
210 iter.fold(first, |acc, s| quote!(#root::__macro_support::product(&(#acc), &(#s))))
211}