1#![warn(missing_docs)]
20#![warn(clippy::all)]
21#![warn(clippy::pedantic)]
22extern crate proc_macro;
23
24use proc_macro2::TokenStream;
25use quote::quote;
26use syn::{parse_macro_input, Error, LitBool, LitFloat, LitInt};
27
28#[proc_macro]
100pub fn gamma_table(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
101 let input = parse_macro_input!(input as GammaTableInput);
102
103 match generate_gamma_table(&input) {
104 Ok(tokens) => tokens.into(),
105 Err(err) => err.to_compile_error().into(),
106 }
107}
108
109struct GammaTableInput {
110 name: syn::Ident,
111 entry_type: syn::Type,
112 gamma: f64,
113 size: usize,
114 max_value: Option<u64>,
115 decoding: Option<bool>,
116}
117
118impl syn::parse::Parse for GammaTableInput {
119 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
120 let mut name = None;
121 let mut entry_type = None;
122 let mut gamma = None;
123 let mut size = None;
124 let mut max_value = None;
125 let mut decoding = None;
126
127 while !input.is_empty() {
128 let ident: syn::Ident = input.parse()?;
129 input.parse::<syn::Token![:]>()?;
130
131 match ident.to_string().as_str() {
132 "name" => {
133 let value: syn::Ident = input.parse()?;
134 name = Some(value);
135 }
136 "entry_type" => {
137 let value: syn::Type = input.parse()?;
138 entry_type = Some(value);
139 }
140 "gamma" => {
141 let value: LitFloat = input.parse()?;
142 gamma = Some(value.base10_parse()?);
143 }
144 "size" => {
145 let value: LitInt = input.parse()?;
146 size = Some(value.base10_parse()?);
147 }
148 "max_value" => {
149 let value: LitInt = input.parse()?;
150 max_value = Some(value.base10_parse()?);
151 }
152 "decoding" => {
153 let value: LitBool = input.parse()?;
154 decoding = Some(value.value);
155 }
156 _ => {
157 return Err(Error::new(
158 ident.span(),
159 format!("Unknown parameter: {ident}"),
160 ))
161 }
162 }
163
164 if input.peek(syn::Token![,]) {
165 input.parse::<syn::Token![,]>()?;
166 }
167 }
168
169 Ok(GammaTableInput {
170 name: name
171 .ok_or_else(|| Error::new(input.span(), "Missing required parameter: name"))?,
172 entry_type: entry_type.ok_or_else(|| {
173 Error::new(input.span(), "Missing required parameter: entry_type")
174 })?,
175 gamma: gamma
176 .ok_or_else(|| Error::new(input.span(), "Missing required parameter: gamma"))?,
177 size: size
178 .ok_or_else(|| Error::new(input.span(), "Missing required parameter: size"))?,
179 max_value,
180 decoding,
181 })
182 }
183}
184
185fn get_integer_type_max_value(entry_type: &syn::Type) -> Option<u64> {
186 if let syn::Type::Path(type_path) = entry_type {
188 if let Some(segment) = type_path.path.segments.last() {
189 match segment.ident.to_string().as_str() {
190 "u8" => Some(u64::from(u8::MAX)),
191 "u16" => Some(u64::from(u16::MAX)),
192 "u32" => Some(u64::from(u32::MAX)),
193 "u64" => Some(u64::MAX),
194 _ => None, }
196 } else {
197 None
198 }
199 } else {
200 None
201 }
202}
203
204fn generate_gamma_table(input: &GammaTableInput) -> syn::Result<TokenStream> {
205 let name = &input.name;
206 let entry_type = &input.entry_type;
207 let gamma = input.gamma;
208 let size = input.size;
209 let max_value = input.max_value.unwrap_or((size - 1) as u64);
210 let decoding = input.decoding.unwrap_or(false);
211
212 if gamma <= 0.0 {
214 return Err(Error::new(name.span(), "Gamma value must be positive"));
215 }
216 if size < 3 {
217 return Err(Error::new(
218 name.span(),
219 "Size must be at least 3 to create a meaningful gamma table. Smaller sizes only have min and max values.",
220 ));
221 }
222
223 if let Some(type_max) = get_integer_type_max_value(entry_type) {
225 if max_value > type_max {
226 return Err(Error::new(
227 name.span(),
228 format!(
229 "max_value ({}) exceeds the maximum value ({}) that can be stored in entry_type {}",
230 max_value,
231 type_max,
232 quote!(#entry_type)
233 ),
234 ));
235 }
236 } else {
237 return Err(Error::new(
238 name.span(),
239 format!(
240 "Unsupported entry_type: {}. Supported types are: u8, u16, u32, u64",
241 quote!(#entry_type)
242 ),
243 ));
244 }
245
246 let values = generate_table_values(size, gamma, max_value, decoding);
248
249 let value_tokens: Vec<TokenStream> = values
251 .iter()
252 .map(|&v| quote! { #v as #entry_type })
253 .collect();
254
255 Ok(quote! {
256 const #name: [#entry_type; #size] = [#(#value_tokens),*];
257 })
258}
259
260fn generate_table_values(size: usize, gamma: f64, max_value: u64, decoding: bool) -> Vec<u64> {
261 let mut values = Vec::with_capacity(size);
262
263 let gamma_exponent = if decoding {
265 1.0 / gamma } else {
267 gamma };
269
270 for i in 0..size {
272 #[allow(clippy::cast_precision_loss)]
273 let normalized_input = i as f64 / (size - 1) as f64;
274 let processed = normalized_input.powf(gamma_exponent);
275 #[allow(
277 clippy::cast_precision_loss,
278 clippy::cast_possible_truncation,
279 clippy::cast_sign_loss
280 )]
281 let output_value = (processed * max_value as f64).round() as u64;
282 values.push(output_value.min(max_value));
283 }
284
285 values
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_gamma_encoding_default() {
294 let values = generate_table_values(256, 2.2, 255, false);
296 assert_eq!(values.len(), 256);
297 assert_eq!(values[0], 0);
298 assert_eq!(values[255], 255);
299
300 for i in 1..values.len() {
302 assert!(values[i] >= values[i - 1]);
303 }
304 }
305
306 #[test]
307 fn test_gamma_decoding() {
308 let values = generate_table_values(256, 2.2, 255, true);
310 assert_eq!(values.len(), 256);
311 assert_eq!(values[0], 0);
312 assert_eq!(values[255], 255);
313
314 for i in 1..values.len() {
316 assert!(values[i] >= values[i - 1]);
317 }
318 }
319
320 #[test]
321 fn test_encoding_vs_decoding_difference() {
322 let encoding_values = generate_table_values(10, 2.2, 100, false);
323 let decoding_values = generate_table_values(10, 2.2, 100, true);
324
325 assert_ne!(encoding_values[5], decoding_values[5]);
327
328 assert_eq!(encoding_values[0], decoding_values[0]); assert_eq!(encoding_values[9], decoding_values[9]); }
332
333 #[test]
334 fn test_default_max_value() {
335 let values = generate_table_values(10, 1.0, 9, false);
337 assert_eq!(values[0], 0);
338 assert_eq!(values[9], 9); }
340
341 #[test]
342 fn test_minimum_size_validation() {
343 let input = GammaTableInput {
345 name: syn::parse_str("TEST_TABLE").unwrap(),
346 entry_type: syn::parse_str("u8").unwrap(),
347 gamma: 2.2,
348 size: 2,
349 max_value: None,
350 decoding: None,
351 };
352
353 let result = generate_gamma_table(&input);
354 assert!(result.is_err());
355 assert!(result
356 .unwrap_err()
357 .to_string()
358 .contains("Size must be at least 3"));
359
360 let input = GammaTableInput {
362 name: syn::parse_str("TEST_TABLE").unwrap(),
363 entry_type: syn::parse_str("u8").unwrap(),
364 gamma: 2.2,
365 size: 3,
366 max_value: None,
367 decoding: None,
368 };
369
370 let result = generate_gamma_table(&input);
371 assert!(result.is_ok());
372 }
373
374 #[test]
375 fn test_negative_gamma_validation() {
376 let input = GammaTableInput {
378 name: syn::parse_str("TEST_TABLE").unwrap(),
379 entry_type: syn::parse_str("u8").unwrap(),
380 gamma: -1.0,
381 size: 10,
382 max_value: None,
383 decoding: None,
384 };
385
386 let result = generate_gamma_table(&input);
387 assert!(result.is_err());
388 assert!(result
389 .unwrap_err()
390 .to_string()
391 .contains("Gamma value must be positive"));
392
393 let input = GammaTableInput {
395 name: syn::parse_str("TEST_TABLE").unwrap(),
396 entry_type: syn::parse_str("u8").unwrap(),
397 gamma: 0.0,
398 size: 10,
399 max_value: None,
400 decoding: None,
401 };
402
403 let result = generate_gamma_table(&input);
404 assert!(result.is_err());
405 assert!(result
406 .unwrap_err()
407 .to_string()
408 .contains("Gamma value must be positive"));
409 }
410
411 #[test]
412 fn test_parsing_unknown_parameter() {
413 let tokens: proc_macro2::TokenStream = quote! {
415 name: TEST_TABLE,
416 entry_type: u8,
417 gamma: 2.2,
418 size: 10,
419 unknown_param: 42
420 };
421
422 let result = syn::parse2::<GammaTableInput>(tokens);
423 assert!(result.is_err());
424 }
425
426 #[test]
427 fn test_parsing_missing_required_parameters() {
428 let tokens: proc_macro2::TokenStream = quote! {
430 entry_type: u8,
431 gamma: 2.2,
432 size: 10
433 };
434 let result = syn::parse2::<GammaTableInput>(tokens);
435 assert!(result.is_err());
436
437 let tokens: proc_macro2::TokenStream = quote! {
439 name: TEST_TABLE,
440 gamma: 2.2,
441 size: 10
442 };
443 let result = syn::parse2::<GammaTableInput>(tokens);
444 assert!(result.is_err());
445
446 let tokens: proc_macro2::TokenStream = quote! {
448 name: TEST_TABLE,
449 entry_type: u8,
450 size: 10
451 };
452 let result = syn::parse2::<GammaTableInput>(tokens);
453 assert!(result.is_err());
454
455 let tokens: proc_macro2::TokenStream = quote! {
457 name: TEST_TABLE,
458 entry_type: u8,
459 gamma: 2.2
460 };
461 let result = syn::parse2::<GammaTableInput>(tokens);
462 assert!(result.is_err());
463 }
464
465 #[test]
466 fn test_parsing_invalid_parameter_types() {
467 let tokens: proc_macro2::TokenStream = quote! {
471 name: 123,
472 entry_type: u8,
473 gamma: 2.2,
474 size: 10
475 };
476 let result = syn::parse2::<GammaTableInput>(tokens);
477 assert!(result.is_err());
478
479 let tokens: proc_macro2::TokenStream = quote! {
481 name: TEST_TABLE,
482 entry_type: "u8",
483 gamma: 2.2,
484 size: 10
485 };
486 let result = syn::parse2::<GammaTableInput>(tokens);
487 assert!(result.is_err());
488
489 let tokens: proc_macro2::TokenStream = quote! {
491 name: TEST_TABLE,
492 entry_type: u8,
493 gamma: "2.2",
494 size: 10
495 };
496 let result = syn::parse2::<GammaTableInput>(tokens);
497 assert!(result.is_err());
498
499 let tokens: proc_macro2::TokenStream = quote! {
501 name: TEST_TABLE,
502 entry_type: u8,
503 gamma: 2.2,
504 size: 10.5
505 };
506 let result = syn::parse2::<GammaTableInput>(tokens);
507 assert!(result.is_err());
508
509 let tokens: proc_macro2::TokenStream = quote! {
511 name: TEST_TABLE,
512 entry_type: u8,
513 gamma: 2.2,
514 size: "10"
515 };
516 let result = syn::parse2::<GammaTableInput>(tokens);
517 assert!(result.is_err());
518
519 let tokens: proc_macro2::TokenStream = quote! {
521 name: TEST_TABLE,
522 entry_type: u8,
523 gamma: 2.2,
524 size: 10,
525 max_value: 255.5
526 };
527 let result = syn::parse2::<GammaTableInput>(tokens);
528 assert!(result.is_err());
529
530 let tokens: proc_macro2::TokenStream = quote! {
532 name: TEST_TABLE,
533 entry_type: u8,
534 gamma: 2.2,
535 size: 10,
536 max_value: "255"
537 };
538 let result = syn::parse2::<GammaTableInput>(tokens);
539 assert!(result.is_err());
540
541 let tokens: proc_macro2::TokenStream = quote! {
543 name: TEST_TABLE,
544 entry_type: u8,
545 gamma: 2.2,
546 size: 10,
547 decoding: 1.0
548 };
549 let result = syn::parse2::<GammaTableInput>(tokens);
550 assert!(result.is_err());
551
552 let tokens: proc_macro2::TokenStream = quote! {
554 name: TEST_TABLE,
555 entry_type: u8,
556 gamma: 2.2,
557 size: 10,
558 decoding: "true"
559 };
560 let result = syn::parse2::<GammaTableInput>(tokens);
561 assert!(result.is_err());
562 }
563
564 #[test]
565 fn test_max_value_overflow_validation() {
566 let input = GammaTableInput {
568 name: syn::parse_str("TEST_TABLE").unwrap(),
569 entry_type: syn::parse_str("u8").unwrap(),
570 gamma: 2.2,
571 size: 10,
572 max_value: Some(300), decoding: None,
574 };
575 let result = generate_gamma_table(&input);
576 assert!(result.is_err());
577 assert!(result
578 .unwrap_err()
579 .to_string()
580 .contains("max_value (300) exceeds the maximum value (255)"));
581
582 let input = GammaTableInput {
584 name: syn::parse_str("TEST_TABLE").unwrap(),
585 entry_type: syn::parse_str("u16").unwrap(),
586 gamma: 2.2,
587 size: 10,
588 max_value: Some(70000), decoding: None,
590 };
591 let result = generate_gamma_table(&input);
592 assert!(result.is_err());
593 assert!(result
594 .unwrap_err()
595 .to_string()
596 .contains("max_value (70000) exceeds the maximum value (65535)"));
597
598 let input = GammaTableInput {
600 name: syn::parse_str("TEST_TABLE").unwrap(),
601 entry_type: syn::parse_str("u32").unwrap(),
602 gamma: 2.2,
603 size: 10,
604 max_value: Some(5000000000), decoding: None,
606 };
607 let result = generate_gamma_table(&input);
608 assert!(result.is_err());
609 assert!(result
610 .unwrap_err()
611 .to_string()
612 .contains("max_value (5000000000) exceeds the maximum value (4294967295)"));
613
614 let input = GammaTableInput {
616 name: syn::parse_str("TEST_TABLE").unwrap(),
617 entry_type: syn::parse_str("u8").unwrap(),
618 gamma: 2.2,
619 size: 10,
620 max_value: Some(255), decoding: None,
622 };
623 let result = generate_gamma_table(&input);
624 assert!(result.is_ok());
625
626 let input = GammaTableInput {
628 name: syn::parse_str("TEST_TABLE").unwrap(),
629 entry_type: syn::parse_str("u32").unwrap(),
630 gamma: 2.2,
631 size: 10,
632 max_value: Some(1000000), decoding: None,
634 };
635 let result = generate_gamma_table(&input);
636 assert!(result.is_ok());
637
638 let input = GammaTableInput {
640 name: syn::parse_str("TEST_TABLE").unwrap(),
641 entry_type: syn::parse_str("u64").unwrap(),
642 gamma: 2.2,
643 size: 10,
644 max_value: Some(1000000), decoding: None,
646 };
647 let result = generate_gamma_table(&input);
648 assert!(result.is_ok());
649
650 let input = GammaTableInput {
652 name: syn::parse_str("TEST_TABLE").unwrap(),
653 entry_type: syn::parse_str("i32").unwrap(), gamma: 2.2,
655 size: 10,
656 max_value: Some(100),
657 decoding: None,
658 };
659 let result = generate_gamma_table(&input);
660 assert!(result.is_err());
661 assert!(result
662 .unwrap_err()
663 .to_string()
664 .contains("Unsupported entry_type"));
665
666 let input = GammaTableInput {
668 name: syn::parse_str("TEST_TABLE").unwrap(),
669 entry_type: syn::parse_str("f32").unwrap(), gamma: 2.2,
671 size: 10,
672 max_value: Some(100),
673 decoding: None,
674 };
675 let result = generate_gamma_table(&input);
676 assert!(result.is_err());
677 assert!(result
678 .unwrap_err()
679 .to_string()
680 .contains("Unsupported entry_type"));
681 }
682}