Skip to main content

krabka_protocol_codegen/emit/
common.rs

1use std::str::FromStr;
2
3use proc_macro2::{Delimiter, Literal, TokenStream, TokenTree};
4use quote::quote;
5use syn::parse::Parse;
6
7use crate::{emit::EmitError, ir::FieldSpec};
8
9/// Lex an emitter-produced Rust fragment into tokens.
10///
11/// # Errors
12/// Returns [`EmitError::Lex`] when the fragment is not valid Rust tokens,
13/// which is an emitter bug.
14pub fn parse_tokens(s: &str) -> Result<TokenStream, EmitError> {
15    TokenStream::from_str(s).map_err(|e| EmitError::Lex {
16        fragment: s.to_owned(),
17        message: e.to_string(),
18    })
19}
20
21/// Parse generated tokens as a complete syntax node of type `T`, to check
22/// that an emitter produced valid Rust.
23///
24/// # Errors
25/// Returns [`EmitError::Syntax`], naming `what`, when the tokens do not parse.
26pub fn validate<T: Parse>(tokens: &TokenStream, what: &'static str) -> Result<T, EmitError> {
27    syn::parse2(tokens.clone()).map_err(|source| EmitError::Syntax { what, source })
28}
29
30/// `expr` as a statement: followed by `;`, unless it is block-like (`if`,
31/// `match`, `for` or a `{ .. }` block), which stands as a statement alone.
32#[must_use]
33pub fn statement(expr: &TokenStream) -> TokenStream {
34    let block_like = expr
35        .clone()
36        .into_iter()
37        .next()
38        .is_some_and(|first| match first {
39            TokenTree::Ident(ident) => ident == "if" || ident == "match" || ident == "for",
40            TokenTree::Group(group) => group.delimiter() == Delimiter::Brace,
41            TokenTree::Punct(_) | TokenTree::Literal(_) => false,
42        });
43    if block_like {
44        expr.clone()
45    } else {
46        quote!(#expr;)
47    }
48}
49
50/// True when `tokens` holds the identifier `name` at any depth.
51#[must_use]
52pub fn uses_ident(tokens: &TokenStream, name: &str) -> bool {
53    tokens.clone().into_iter().any(|tree| match tree {
54        TokenTree::Ident(ident) => ident == name,
55        TokenTree::Group(group) => uses_ident(&group.stream(), name),
56        TokenTree::Punct(_) | TokenTree::Literal(_) => false,
57    })
58}
59
60/// The tag of a tagged field, as an unsuffixed literal.
61///
62/// # Errors
63/// Returns [`EmitError::MissingTag`] when the field has no tag.
64pub fn tag_literal(f: &FieldSpec) -> Result<Literal, EmitError> {
65    f.tag
66        .map(Literal::u32_unsuffixed)
67        .ok_or_else(|| EmitError::MissingTag(f.name.clone()))
68}
69
70/// Return the standard AUTO-GENERATED header comment for a generated source file.
71#[must_use]
72pub fn banner(schemas_version: &str) -> String {
73    format!("{BANNER_PREFIX} against {schemas_version}. Do not edit.\n")
74}
75
76/// The opening bytes of every file the generator writes.
77///
78/// The regeneration binary matches this prefix before it deletes a stale
79/// output. It deletes only a file that it generated.
80pub const BANNER_PREFIX: &str = "// AUTO-GENERATED by krabka-protocol-codegen";
81
82/// Format an integer string as a Rust literal with underscores every 3 digits
83/// from the right. This satisfies `clippy::unreadable_literal` for large
84/// numbers, for example `-2_147_483_648i32`.
85///
86/// This function handles hex literals (`0x...`). It parses them to decimal
87/// first, so the output is a clean decimal literal, for example
88/// `"0x7fffffff"` → `2_147_483_647i32`.
89#[must_use]
90pub fn format_int_literal(s: &str, suffix: &str) -> String {
91    // Normalise hex literals to decimal so we don't produce malformed output
92    // like `0_x7f_fff_fffi32`.
93    let (negative_in, s) = s.strip_prefix('-').map_or((false, s), |rest| (true, rest));
94    let s = s.trim();
95    // Try to parse as hex; on failure fall through and treat as decimal string.
96    let decimal_string: String;
97    let (negative, digits): (bool, &str) =
98        if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
99            // Parse the hex digits as u64 then reinterpret the bits at the target
100            // type's width. A 32-bit constant like `0xffffffff` for an `i32` field
101            // means -1, not 4_294_967_295 (which would overflow the literal).
102            if let Ok(v) = u64::from_str_radix(hex, 16) {
103                let signed = hex_to_signed(v, suffix);
104                if negative_in || signed < 0 {
105                    decimal_string = format!("{}", signed.unsigned_abs());
106                    (true, &decimal_string)
107                } else {
108                    decimal_string = format!("{signed}");
109                    (false, &decimal_string)
110                }
111            } else {
112                // Fallback: keep the hex string as-is (should not happen with valid schemas).
113                decimal_string = s.to_owned();
114                (negative_in, &decimal_string)
115            }
116        } else {
117            decimal_string = s.to_owned();
118            (negative_in, &decimal_string)
119        };
120    let underscored: String = {
121        let chars: Vec<char> = digits.chars().collect();
122        let mut result = String::new();
123        let len = chars.len();
124        for (i, &c) in chars.iter().enumerate() {
125            if i > 0 && (len - i).is_multiple_of(3) {
126                result.push('_');
127            }
128            result.push(c);
129        }
130        result
131    };
132    if negative {
133        format!("-{underscored}{suffix}")
134    } else {
135        format!("{underscored}{suffix}")
136    }
137}
138
139/// Reinterpret the bits of a hex literal at the width of its Rust type suffix,
140/// for example `"i32"` or `"i64"`. For a signed suffix the top bit of that
141/// width sign-extends, so `0xffffffff` with `"i32"` yields `-1`. This function
142/// returns unsigned suffixes and 64-bit widths unchanged.
143fn hex_to_signed(v: u64, suffix: &str) -> i64 {
144    let bits: u32 = suffix.trim_start_matches(['i', 'u']).parse().unwrap_or(64);
145    if bits >= 64 {
146        return v.cast_signed();
147    }
148    let mask = (1u64 << bits) - 1;
149    let x = v & mask;
150    if suffix.starts_with('i') && (x & (1u64 << (bits - 1))) != 0 {
151        let shift = 64 - bits;
152        (x << shift).cast_signed() >> shift
153    } else {
154        // `x` holds at most 63 bits here, so the reinterpretation keeps its
155        // value.
156        x.cast_signed()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use assert2::assert;
163
164    use super::*;
165
166    #[test]
167    fn decimal_literals() {
168        for (input, want) in [
169            ("0", "0i32"),
170            ("-1", "-1i32"),
171            ("2147483647", "2_147_483_647i32"),
172            ("-2147483648", "-2_147_483_648i32"),
173        ] {
174            assert!(format_int_literal(input, "i32") == want);
175        }
176    }
177
178    #[test]
179    fn hex_positive_normalizes_to_decimal() {
180        assert!(format_int_literal("0x7fffffff", "i32") == "2_147_483_647i32");
181        assert!(format_int_literal("0X10", "i32") == "16i32");
182    }
183
184    #[test]
185    fn hex_high_bit_is_sign_extended_to_target_width() {
186        // 0xffffffff as i32 is -1, not 4_294_967_295 (which would overflow i32).
187        // The same 32-bit pattern widened to i64 is a large positive value.
188        for (input, suffix, want) in [
189            ("0xffffffff", "i32", "-1i32"),
190            ("0x80000000", "i32", "-2_147_483_648i32"),
191            ("0xffffffff", "i64", "4_294_967_295i64"),
192        ] {
193            assert!(format_int_literal(input, suffix) == want);
194        }
195        assert!(hex_to_signed(0x4000_0000_0000_0000, "i63") == -(1i64 << 62));
196    }
197}