1use proc_macro::TokenStream;
16use quote::{format_ident, quote};
17use syn::parse::{Parse, ParseStream};
18use syn::{parse_macro_input, DeriveInput, Expr, ExprLit, Ident, Lit, Path, Token};
19
20#[derive(Debug, Default)]
21struct ExtensionArgs {
22 namespace: Option<String>,
23 name: Option<String>,
24 version: Option<u64>,
25}
26
27struct RuntimeArgs {
28 runtime: Ident,
29 family_id: Path,
30 op_type: Path,
31 execute: Path,
32 execute_reads: Path,
33 execute_in_session: Option<Path>,
34 session_supported: Option<Path>,
35 backend_bound: Path,
36}
37
38impl Parse for ExtensionArgs {
39 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
40 let mut args = Self::default();
41 while !input.is_empty() {
42 let key: syn::Ident = input.parse()?;
43 input.parse::<Token![=]>()?;
44 let value: Expr = input.parse()?;
45 match key.to_string().as_str() {
46 "namespace" => args.namespace = Some(expect_string(value, "namespace")?),
47 "name" => args.name = Some(expect_string(value, "name")?),
48 "version" => args.version = Some(expect_u64(value, "version")?),
49 other => {
50 return Err(syn::Error::new(
51 key.span(),
52 format!("unsupported tenferro_extension argument {other:?}"),
53 ));
54 }
55 }
56 if input.is_empty() {
57 break;
58 }
59 input.parse::<Token![,]>()?;
60 }
61 Ok(args)
62 }
63}
64
65impl Parse for RuntimeArgs {
66 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
67 let mut runtime = None;
68 let mut family_id = None;
69 let mut op_type = None;
70 let mut execute = None;
71 let mut execute_reads = None;
72 let mut execute_in_session = None;
73 let mut session_supported = None;
74 let mut backend_bound = None;
75
76 while !input.is_empty() {
77 let key: Ident = input.parse()?;
78 input.parse::<Token![=]>()?;
79 match key.to_string().as_str() {
80 "runtime" => runtime = Some(input.parse()?),
81 "family_id" => family_id = Some(input.parse()?),
82 "op_type" => op_type = Some(input.parse()?),
83 "execute" => execute = Some(input.parse()?),
84 "execute_reads" => execute_reads = Some(input.parse()?),
85 "execute_in_session" => execute_in_session = Some(input.parse()?),
86 "session_supported" => session_supported = Some(input.parse()?),
87 "backend_bound" => backend_bound = Some(input.parse()?),
88 other => {
89 return Err(syn::Error::new(
90 key.span(),
91 format!("unsupported define_extension_runtime argument {other:?}"),
92 ));
93 }
94 }
95 if input.is_empty() {
96 break;
97 }
98 input.parse::<Token![,]>()?;
99 }
100
101 Ok(Self {
102 runtime: required(runtime, "runtime")?,
103 family_id: required(family_id, "family_id")?,
104 op_type: required(op_type, "op_type")?,
105 execute: required(execute, "execute")?,
106 execute_reads: required(execute_reads, "execute_reads")?,
107 execute_in_session,
108 session_supported,
109 backend_bound: backend_bound
110 .unwrap_or_else(|| syn::parse_quote!(tenferro_tensor::TensorBackend)),
111 })
112 }
113}
114
115#[proc_macro_derive(ExtensionFamilyId, attributes(tenferro_extension))]
122pub fn derive_extension_family_id(input: TokenStream) -> TokenStream {
123 let input = parse_macro_input!(input as DeriveInput);
124 match expand_extension_family_id(input) {
125 Ok(tokens) => tokens.into(),
126 Err(err) => err.to_compile_error().into(),
127 }
128}
129
130#[proc_macro]
144pub fn define_extension_runtime(input: TokenStream) -> TokenStream {
145 let args = parse_macro_input!(input as RuntimeArgs);
146 match expand_extension_runtime(args) {
147 Ok(tokens) => tokens.into(),
148 Err(err) => err.to_compile_error().into(),
149 }
150}
151
152fn expand_extension_family_id(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
153 let mut parsed = None;
154 for attr in &input.attrs {
155 if attr.path().is_ident("tenferro_extension") {
156 let args = attr.parse_args::<ExtensionArgs>()?;
157 parsed = Some(args);
158 }
159 }
160 let args = parsed.ok_or_else(|| {
161 syn::Error::new_spanned(
162 &input.ident,
163 "missing #[tenferro_extension(namespace = \"...\", version = N)]",
164 )
165 })?;
166 let namespace = args.namespace.ok_or_else(|| {
167 syn::Error::new_spanned(&input.ident, "missing tenferro_extension namespace")
168 })?;
169 let version = args.version.ok_or_else(|| {
170 syn::Error::new_spanned(&input.ident, "missing tenferro_extension version")
171 })?;
172 let name = args
173 .name
174 .unwrap_or_else(|| to_snake_case(&input.ident.to_string()));
175 let family_id = format!("{namespace}.{name}.v{version}");
176 let ident = input.ident;
177
178 Ok(quote! {
179 impl #ident {
180 pub const FAMILY_ID: &'static str = #family_id;
182 }
183 })
184}
185
186fn expand_extension_runtime(args: RuntimeArgs) -> syn::Result<proc_macro2::TokenStream> {
187 let RuntimeArgs {
188 runtime,
189 family_id,
190 op_type,
191 execute: _execute,
192 execute_reads,
193 execute_in_session,
194 session_supported,
195 backend_bound,
196 } = args;
197 let module = format_ident!("{}Module", runtime);
198 let planning_config = format_ident!("{}PlanningConfig", runtime);
199 let prepared_operation = format_ident!("{}PreparedOperation", runtime);
200 let session_methods = match (execute_in_session, session_supported) {
201 (Some(execute_in_session), Some(session_supported)) => quote! {
202 fn supports_session(&self) -> bool {
203 #session_supported::<B>(&self.op)
204 }
205
206 fn execute_in_session(
207 &self,
208 session: &mut dyn tenferro_tensor::BackendSession,
209 extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
210 inputs: &[tenferro_tensor::TensorRead<'_>],
211 ) -> tenferro_runtime::Result<Vec<tenferro_tensor::Tensor>> {
212 Ok(#execute_in_session(&self.op, session, extension_caches, inputs)?)
213 }
214 },
215 (None, None) => quote! {},
216 (Some(path), None) | (None, Some(path)) => {
217 return Err(syn::Error::new_spanned(
218 path,
219 "execute_in_session and session_supported must be supplied together",
220 ))
221 }
222 };
223 Ok(quote! {
224 pub(crate) struct #runtime<B: #backend_bound + 'static> {
225 engine_id: tenferro_runtime::EngineId,
226 _backend: std::marker::PhantomData<fn() -> B>,
227 }
228
229 pub(crate) struct #module<B: #backend_bound + 'static> {
230 module_id: tenferro_runtime::ExtensionModuleId,
231 engine_id: tenferro_runtime::EngineId,
232 _backend: std::marker::PhantomData<fn() -> B>,
233 }
234
235 #[derive(Debug, Default)]
236 pub(crate) struct #planning_config;
237
238 pub(crate) struct #prepared_operation<B: #backend_bound + 'static> {
239 binding: tenferro_runtime::PreparedOperationBinding,
240 specialization: tenferro_runtime::SpecializationProjection,
241 op: #op_type,
242 _backend: std::marker::PhantomData<fn() -> B>,
243 }
244
245 impl<B: #backend_bound + 'static> std::fmt::Debug for #runtime<B> {
246 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 formatter
248 .debug_struct(stringify!(#runtime))
249 .field("family_id", &#family_id)
250 .field("engine_id", &self.engine_id)
251 .field("backend_type", &std::any::type_name::<B>())
252 .finish()
253 }
254 }
255
256 impl<B: #backend_bound + 'static> std::fmt::Debug for #module<B> {
257 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 formatter
259 .debug_struct(stringify!(#module))
260 .field("module_id", &self.module_id)
261 .field("engine_id", &self.engine_id)
262 .field("backend_type", &std::any::type_name::<B>())
263 .finish()
264 }
265 }
266
267 impl<B: #backend_bound + 'static> std::fmt::Debug for #prepared_operation<B> {
268 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 formatter
270 .debug_struct(stringify!(#prepared_operation))
271 .field("family_id", &#family_id)
272 .field("binding", &self.binding)
273 .field("specialization", &self.specialization)
274 .field("backend_type", &std::any::type_name::<B>())
275 .finish_non_exhaustive()
276 }
277 }
278
279 impl<B: #backend_bound + 'static> tenferro_runtime::ExtensionEngine for #runtime<B>
280 where
281 #op_type: Clone + Send + Sync + 'static,
282 {
283 fn family_id(&self) -> &'static str {
284 #family_id
285 }
286
287 fn engine_id(&self) -> &tenferro_runtime::EngineId {
288 &self.engine_id
289 }
290
291 fn context_identity(&self) -> tenferro_runtime::ExecutionContextIdentity {
292 tenferro_runtime::ExecutionContextIdentity::of::<B>()
293 }
294
295 fn prepare(
296 &self,
297 request: tenferro_runtime::ExtensionPrepareRequest<'_>,
298 ) -> std::result::Result<tenferro_runtime::PrepareCapability, tenferro_runtime::PrepareError> {
299 let op = request
300 .operation()
301 .as_any()
302 .downcast_ref::<#op_type>()
303 .cloned()
304 .ok_or_else(|| tenferro_runtime::PrepareError::ProviderContract {
305 source: tenferro_runtime::ProviderContractError::WrongOperationFamily {
306 expected: tenferro_runtime::CoreCapabilityKind::Elementwise,
307 operation: #family_id,
308 },
309 })?;
310 let prepared = std::sync::Arc::new(#prepared_operation::<B> {
311 binding: request.binding().clone(),
312 specialization: request.specialization().clone(),
313 op,
314 _backend: std::marker::PhantomData,
315 });
316 Ok(tenferro_runtime::PrepareCapability::Prepared(
317 tenferro_runtime::PreparedOperationPlan::executable(prepared.clone(), prepared)
318 ))
319 }
320 }
321
322 impl tenferro_runtime::ExtensionPlanningConfig for #planning_config {
323 fn family_id(&self) -> &'static str {
324 #family_id
325 }
326
327 fn as_any(&self) -> &dyn std::any::Any {
328 self
329 }
330
331 fn payload_hash(&self, state: &mut dyn std::hash::Hasher) {
332 state.write_u8(0);
333 }
334
335 fn payload_eq(&self, other: &dyn tenferro_runtime::ExtensionPlanningConfig) -> bool {
336 other.as_any().downcast_ref::<Self>().is_some()
337 }
338
339 fn retained_bytes(&self) -> usize {
340 0
341 }
342 }
343
344 impl<B: #backend_bound + 'static> tenferro_runtime::PreparedOperation for #prepared_operation<B>
345 where
346 #op_type: Clone + Send + Sync + 'static,
347 {
348 fn binding(&self) -> &tenferro_runtime::PreparedOperationBinding {
349 &self.binding
350 }
351
352 fn specialization(&self) -> &tenferro_runtime::SpecializationProjection {
353 &self.specialization
354 }
355
356 fn retained_bytes(&self) -> usize {
357 0
358 }
359
360 }
361
362 impl<B: #backend_bound + 'static> tenferro_runtime::PreparedOperationExecutor for #prepared_operation<B>
363 where
364 #op_type: Clone + Send + Sync + 'static,
365 {
366 fn execute(
367 &self,
368 context: &mut tenferro_runtime::ErasedExecutionContext<'_>,
369 extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
370 inputs: &[tenferro_tensor::TensorRead<'_>],
371 ) -> tenferro_runtime::Result<Vec<tenferro_tensor::Tensor>> {
372 let backend = context
373 .downcast_mut::<B>(self.binding.context_identity())
374 .map_err(|source| tenferro_runtime::Error::runtime_state_source(
375 "extension",
376 tenferro_runtime::ErrorPhase::Execution,
377 source,
378 ))?;
379 let mut ctx = tenferro_runtime::ExtensionExecutionContext::new(
380 backend,
381 extension_caches,
382 );
383 Ok(#execute_reads(&self.op, inputs, &mut ctx)?)
384 }
385
386 #session_methods
387 }
388
389 impl<B: #backend_bound + 'static> tenferro_runtime::ExtensionModule for #module<B>
390 where
391 #op_type: Clone + Send + Sync + 'static,
392 {
393 fn module_id(&self) -> &tenferro_runtime::ExtensionModuleId {
394 &self.module_id
395 }
396
397 fn configure(
398 &self,
399 registrar: &mut tenferro_runtime::ExtensionModuleRegistrar<'_>,
400 ) -> std::result::Result<(), tenferro_runtime::ExtensionModuleError> {
401 registrar.register_engine(std::sync::Arc::new(#runtime::<B> {
402 engine_id: self.engine_id.clone(),
403 _backend: std::marker::PhantomData,
404 }))?;
405 registrar.register_planning_config(
406 self.engine_id.clone(),
407 std::sync::Arc::new(#planning_config),
408 )?;
409 Ok(())
410 }
411 }
412
413 #[doc = "Build this extension module for one runtime engine."]
414 #[doc = "\n# Errors\n\nReturns `RuntimeConfigError::MalformedIdentity` when the generated module identifier is invalid."]
415 pub fn extension_module<B: #backend_bound + 'static>(
416 engine_id: tenferro_runtime::EngineId,
417 ) -> std::result::Result<
418 std::sync::Arc<dyn tenferro_runtime::ExtensionModule>,
419 tenferro_runtime::RuntimeConfigError,
420 >
421 where
422 #op_type: Clone + Send + Sync + 'static,
423 {
424 Ok(std::sync::Arc::new(#module::<B> {
425 module_id: tenferro_runtime::ExtensionModuleId::new(format!("{}.module", #family_id))?,
426 engine_id,
427 _backend: std::marker::PhantomData,
428 }))
429 }
430
431 })
432}
433
434fn expect_string(value: Expr, field: &str) -> syn::Result<String> {
435 match value {
436 Expr::Lit(ExprLit {
437 lit: Lit::Str(value),
438 ..
439 }) => Ok(value.value()),
440 other => Err(syn::Error::new_spanned(
441 other,
442 format!("{field} must be a string literal"),
443 )),
444 }
445}
446
447fn expect_u64(value: Expr, field: &str) -> syn::Result<u64> {
448 match value {
449 Expr::Lit(ExprLit {
450 lit: Lit::Int(value),
451 ..
452 }) => value.base10_parse(),
453 other => Err(syn::Error::new_spanned(
454 other,
455 format!("{field} must be an integer literal"),
456 )),
457 }
458}
459
460fn required<T>(value: Option<T>, field: &str) -> syn::Result<T> {
461 value.ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), format!("missing {field}")))
462}
463
464fn to_snake_case(input: &str) -> String {
465 let mut out = String::new();
466 let mut prev_lower_or_digit = false;
467 for ch in input.chars() {
468 if ch.is_ascii_uppercase() {
469 if prev_lower_or_digit {
470 out.push('_');
471 }
472 out.push(ch.to_ascii_lowercase());
473 prev_lower_or_digit = false;
474 } else {
475 prev_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
476 out.push(ch);
477 }
478 }
479 out
480}
481
482#[cfg(test)]
483mod tests;