#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
extern crate proc_macro;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Error, LitBool, LitFloat, LitInt};
#[proc_macro]
pub fn gamma_table(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as GammaTableInput);
match generate_gamma_table(&input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
struct GammaTableInput {
name: syn::Ident,
entry_type: syn::Type,
gamma: f64,
size: usize,
max_value: Option<u64>,
decoding: Option<bool>,
}
impl syn::parse::Parse for GammaTableInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut name = None;
let mut entry_type = None;
let mut gamma = None;
let mut size = None;
let mut max_value = None;
let mut decoding = None;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
input.parse::<syn::Token![:]>()?;
match ident.to_string().as_str() {
"name" => {
let value: syn::Ident = input.parse()?;
name = Some(value);
}
"entry_type" => {
let value: syn::Type = input.parse()?;
entry_type = Some(value);
}
"gamma" => {
let value: LitFloat = input.parse()?;
gamma = Some(value.base10_parse()?);
}
"size" => {
let value: LitInt = input.parse()?;
size = Some(value.base10_parse()?);
}
"max_value" => {
let value: LitInt = input.parse()?;
max_value = Some(value.base10_parse()?);
}
"decoding" => {
let value: LitBool = input.parse()?;
decoding = Some(value.value);
}
_ => {
return Err(Error::new(
ident.span(),
format!("Unknown parameter: {ident}"),
))
}
}
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
}
Ok(GammaTableInput {
name: name
.ok_or_else(|| Error::new(input.span(), "Missing required parameter: name"))?,
entry_type: entry_type.ok_or_else(|| {
Error::new(input.span(), "Missing required parameter: entry_type")
})?,
gamma: gamma
.ok_or_else(|| Error::new(input.span(), "Missing required parameter: gamma"))?,
size: size
.ok_or_else(|| Error::new(input.span(), "Missing required parameter: size"))?,
max_value,
decoding,
})
}
}
fn get_integer_type_max_value(entry_type: &syn::Type) -> Option<u64> {
if let syn::Type::Path(type_path) = entry_type {
if let Some(segment) = type_path.path.segments.last() {
match segment.ident.to_string().as_str() {
"u8" => Some(u64::from(u8::MAX)),
"u16" => Some(u64::from(u16::MAX)),
"u32" => Some(u64::from(u32::MAX)),
"u64" => Some(u64::MAX),
_ => None, }
} else {
None
}
} else {
None
}
}
fn generate_gamma_table(input: &GammaTableInput) -> syn::Result<TokenStream> {
let name = &input.name;
let entry_type = &input.entry_type;
let gamma = input.gamma;
let size = input.size;
let max_value = input.max_value.unwrap_or((size - 1) as u64);
let decoding = input.decoding.unwrap_or(false);
if gamma <= 0.0 {
return Err(Error::new(name.span(), "Gamma value must be positive"));
}
if size < 3 {
return Err(Error::new(
name.span(),
"Size must be at least 3 to create a meaningful gamma table. Smaller sizes only have min and max values.",
));
}
if let Some(type_max) = get_integer_type_max_value(entry_type) {
if max_value > type_max {
return Err(Error::new(
name.span(),
format!(
"max_value ({}) exceeds the maximum value ({}) that can be stored in entry_type {}",
max_value,
type_max,
quote!(#entry_type)
),
));
}
} else {
return Err(Error::new(
name.span(),
format!(
"Unsupported entry_type: {}. Supported types are: u8, u16, u32, u64",
quote!(#entry_type)
),
));
}
let values = generate_table_values(size, gamma, max_value, decoding);
let value_tokens: Vec<TokenStream> = values
.iter()
.map(|&v| quote! { #v as #entry_type })
.collect();
Ok(quote! {
const #name: [#entry_type; #size] = [#(#value_tokens),*];
})
}
fn generate_table_values(size: usize, gamma: f64, max_value: u64, decoding: bool) -> Vec<u64> {
let mut values = Vec::with_capacity(size);
let gamma_exponent = if decoding {
1.0 / gamma } else {
gamma };
for i in 0..size {
#[allow(clippy::cast_precision_loss)]
let normalized_input = i as f64 / (size - 1) as f64;
let processed = normalized_input.powf(gamma_exponent);
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let output_value = (processed * max_value as f64).round() as u64;
values.push(output_value.min(max_value));
}
values
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gamma_encoding_default() {
let values = generate_table_values(256, 2.2, 255, false);
assert_eq!(values.len(), 256);
assert_eq!(values[0], 0);
assert_eq!(values[255], 255);
for i in 1..values.len() {
assert!(values[i] >= values[i - 1]);
}
}
#[test]
fn test_gamma_decoding() {
let values = generate_table_values(256, 2.2, 255, true);
assert_eq!(values.len(), 256);
assert_eq!(values[0], 0);
assert_eq!(values[255], 255);
for i in 1..values.len() {
assert!(values[i] >= values[i - 1]);
}
}
#[test]
fn test_encoding_vs_decoding_difference() {
let encoding_values = generate_table_values(10, 2.2, 100, false);
let decoding_values = generate_table_values(10, 2.2, 100, true);
assert_ne!(encoding_values[5], decoding_values[5]);
assert_eq!(encoding_values[0], decoding_values[0]); assert_eq!(encoding_values[9], decoding_values[9]); }
#[test]
fn test_default_max_value() {
let values = generate_table_values(10, 1.0, 9, false);
assert_eq!(values[0], 0);
assert_eq!(values[9], 9); }
#[test]
fn test_minimum_size_validation() {
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: 2.2,
size: 2,
max_value: None,
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Size must be at least 3"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: 2.2,
size: 3,
max_value: None,
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_ok());
}
#[test]
fn test_negative_gamma_validation() {
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: -1.0,
size: 10,
max_value: None,
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Gamma value must be positive"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: 0.0,
size: 10,
max_value: None,
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Gamma value must be positive"));
}
#[test]
fn test_parsing_unknown_parameter() {
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10,
unknown_param: 42
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
}
#[test]
fn test_parsing_missing_required_parameters() {
let tokens: proc_macro2::TokenStream = quote! {
entry_type: u8,
gamma: 2.2,
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
gamma: 2.2,
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
}
#[test]
fn test_parsing_invalid_parameter_types() {
let tokens: proc_macro2::TokenStream = quote! {
name: 123,
entry_type: u8,
gamma: 2.2,
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: "u8",
gamma: 2.2,
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: "2.2",
size: 10
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10.5
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: "10"
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10,
max_value: 255.5
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10,
max_value: "255"
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10,
decoding: 1.0
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
let tokens: proc_macro2::TokenStream = quote! {
name: TEST_TABLE,
entry_type: u8,
gamma: 2.2,
size: 10,
decoding: "true"
};
let result = syn::parse2::<GammaTableInput>(tokens);
assert!(result.is_err());
}
#[test]
fn test_max_value_overflow_validation() {
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(300), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_value (300) exceeds the maximum value (255)"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u16").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(70000), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_value (70000) exceeds the maximum value (65535)"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u32").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(5000000000), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_value (5000000000) exceeds the maximum value (4294967295)"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u8").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(255), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_ok());
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u32").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(1000000), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_ok());
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("u64").unwrap(),
gamma: 2.2,
size: 10,
max_value: Some(1000000), decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_ok());
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("i32").unwrap(), gamma: 2.2,
size: 10,
max_value: Some(100),
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unsupported entry_type"));
let input = GammaTableInput {
name: syn::parse_str("TEST_TABLE").unwrap(),
entry_type: syn::parse_str("f32").unwrap(), gamma: 2.2,
size: 10,
max_value: Some(100),
decoding: None,
};
let result = generate_gamma_table(&input);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unsupported entry_type"));
}
}