Skip to main content

packtab_macro/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::parse::{Parse, ParseStream};
6use syn::{parse_macro_input, Expr, Ident, LitInt, Token, Visibility, Type};
7
8/// Input syntax:
9/// ```text
10/// packtab_macro::pack_table! {
11///     pub fn lookup(u: usize) -> u8 {
12///         data: [1, 2, 3, 4, 5],
13///         default: 0,
14///         compression: 1.0,
15///     }
16/// }
17/// ```
18struct PackTableInput {
19    vis: Visibility,
20    fn_name: Ident,
21    _arg_name: Ident,
22    _ret_type: Type,
23    data: Vec<i64>,
24    default: Option<i64>,
25    compression: f64,
26    unsafe_access: bool,
27}
28
29impl Parse for PackTableInput {
30    fn parse(input: ParseStream) -> syn::Result<Self> {
31        let vis: Visibility = input.parse()?;
32        input.parse::<Token![fn]>()?;
33        let fn_name: Ident = input.parse()?;
34
35        let paren_content;
36        syn::parenthesized!(paren_content in input);
37        let arg_name: Ident = paren_content.parse()?;
38        paren_content.parse::<Token![:]>()?;
39        let _arg_type: Type = paren_content.parse()?;
40
41        input.parse::<Token![->]>()?;
42        let ret_type: Type = input.parse()?;
43
44        let brace_content;
45        syn::braced!(brace_content in input);
46
47        // data: [...]
48        let data_ident: Ident = brace_content.parse()?;
49        if data_ident != "data" {
50            return Err(syn::Error::new_spanned(data_ident, "expected 'data'"));
51        }
52        brace_content.parse::<Token![:]>()?;
53
54        let bracket_content;
55        syn::bracketed!(bracket_content in brace_content);
56        let mut data = Vec::new();
57        while !bracket_content.is_empty() {
58            if bracket_content.peek(Token![-]) {
59                bracket_content.parse::<Token![-]>()?;
60                let lit: LitInt = bracket_content.parse()?;
61                data.push(-(lit.base10_parse::<i64>()?));
62            } else {
63                let lit: LitInt = bracket_content.parse()?;
64                data.push(lit.base10_parse::<i64>()?);
65            }
66            if bracket_content.peek(Token![,]) {
67                bracket_content.parse::<Token![,]>()?;
68            }
69        }
70        brace_content.parse::<Token![,]>()?;
71
72        // Optional default: N
73        let mut default = None;
74        if !brace_content.is_empty() && !brace_content.peek(Token![,]) {
75            let ident: Ident = brace_content.parse()?;
76            if ident != "default" {
77                return Err(syn::Error::new_spanned(ident, "expected 'default'"));
78            }
79            brace_content.parse::<Token![:]>()?;
80            default = Some(if brace_content.peek(Token![-]) {
81                brace_content.parse::<Token![-]>()?;
82                let lit: LitInt = brace_content.parse()?;
83                -(lit.base10_parse::<i64>()?)
84            } else {
85                let lit: LitInt = brace_content.parse()?;
86                lit.base10_parse::<i64>()?
87            });
88        }
89
90        // Optional trailing fields: compression, unsafe
91        let mut compression = 1.0f64;
92        let mut unsafe_access = false;
93        while brace_content.peek(Token![,]) {
94            brace_content.parse::<Token![,]>()?;
95            if brace_content.is_empty() {
96                break;
97            }
98            if brace_content.peek(Token![unsafe]) {
99                let kw: Token![unsafe] = brace_content.parse()?;
100                brace_content.parse::<Token![:]>()?;
101                let lit: syn::LitBool = brace_content.parse()
102                    .map_err(|_| syn::Error::new_spanned(kw, "expected bool after 'unsafe:'"))?;
103                unsafe_access = lit.value;
104            } else {
105                let ident: Ident = brace_content.parse()?;
106                match ident.to_string().as_str() {
107                    "compression" => {
108                        brace_content.parse::<Token![:]>()?;
109                        let expr: Expr = brace_content.parse()?;
110                        compression = match &expr {
111                            Expr::Lit(lit) => match &lit.lit {
112                                syn::Lit::Float(f) => f.base10_parse::<f64>()?,
113                                syn::Lit::Int(i) => i.base10_parse::<f64>()?,
114                                _ => return Err(syn::Error::new_spanned(lit, "expected number")),
115                            },
116                            _ => return Err(syn::Error::new_spanned(expr, "expected number literal")),
117                        };
118                    }
119                    _ => return Err(syn::Error::new_spanned(ident, "expected 'compression' or 'unsafe'")),
120                }
121            }
122        }
123
124        Ok(PackTableInput {
125            vis,
126            fn_name,
127            _arg_name: arg_name,
128            _ret_type: ret_type,
129            data,
130            default,
131            compression,
132            unsafe_access,
133        })
134    }
135}
136
137/// Pack a table of integers into compact multi-level lookup tables at compile time.
138///
139/// # Example
140///
141/// ```text
142/// packtab_macro::pack_table! {
143///     pub fn lookup(u: usize) -> u8 {
144///         data: [1, 2, 3, 4, 5, 6, 7, 8],
145///         default: 0,
146///     }
147/// }
148/// ```
149#[proc_macro]
150pub fn pack_table(input: TokenStream) -> TokenStream {
151    let input = parse_macro_input!(input as PackTableInput);
152
153    let (info, best_idx) = packtab::pack_table(&input.data, input.default, input.compression);
154    let code_str = packtab::generate(
155        &info,
156        best_idx,
157        &input.fn_name.to_string(),
158        packtab::codegen::Language::Rust { unsafe_access: input.unsafe_access },
159    );
160
161    // Adjust visibility: replace "pub(crate) fn name_get" with user's visibility + name.
162    let vis_str = match &input.vis {
163        Visibility::Public(_) => "pub",
164        Visibility::Inherited => "",
165        _ => "pub(crate)",
166    };
167
168    let fn_name_str = input.fn_name.to_string();
169    let adjusted = code_str.replace(
170        &format!("pub(crate) fn {}_get", fn_name_str),
171        &format!("{} fn {}", vis_str, fn_name_str),
172    );
173    // Replace internal references to name_get with just name
174    let adjusted = adjusted.replace(
175        &format!("{}_get", fn_name_str),
176        &fn_name_str,
177    );
178
179    let generated: proc_macro2::TokenStream = adjusted
180        .parse()
181        .unwrap_or_else(|e| panic!("Failed to parse generated code: {}\n\nCode:\n{}", e, adjusted));
182
183    let output = quote! {
184        #generated
185    };
186
187    output.into()
188}