diff --git a/Cargo.toml b/Cargo.toml index e52310d..233b31a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,24 @@ [package] name = "cppshift" -version = "0.1.0" +version = "0.1.1" authors = ["Jérémy HERGAULT", "Enzo PASQUALINI"] description = "CPP parser and transpiler" repository = "https://github.com/worldline/cppshift" edition = "2024" license = "Apache-2.0" +[features] +default = ["transpiler"] +ast = [] +transpiler = ["ast", "dep:serde", "dep:syn", "dep:quote", "dep:proc-macro2"] + [dependencies] miette = { version = "7", features = ["fancy"] } thiserror = "2" +serde = { version = "1", features = ["derive"], optional = true } +syn = { version = "2", features = ["full", "extra-traits", "printing"], optional = true } +quote = { version = "1", optional = true } +proc-macro2 = { version = "1", optional = true } [dev-dependencies] tokio = { version = "1", features = ["macros"] } diff --git a/src/ast/item.rs b/src/ast/item.rs index 10ac611..93c6182 100644 --- a/src/ast/item.rs +++ b/src/ast/item.rs @@ -3,6 +3,8 @@ //! Each variant of [`Item`] corresponds to a top-level declaration in a C++ translation unit, //! following the naming conventions of `syn::Item`. +use std::fmt; + use crate::SourceSpan; use crate::lex::Token; @@ -53,6 +55,20 @@ pub struct Path<'de> { pub segments: Vec>, } +impl<'de> fmt::Display for Path<'de> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + self.segments + .iter() + .map(|s| s.ident.sym) + .collect::>() + .join("::") + ) + } +} + /// A single segment of a path, analogous to `syn::PathSegment`. #[derive(Debug, Clone, Copy, PartialEq)] pub struct PathSegment<'de> { diff --git a/src/ast/parse.rs b/src/ast/parse.rs index 221c6b8..424ef91 100644 --- a/src/ast/parse.rs +++ b/src/ast/parse.rs @@ -545,8 +545,22 @@ fn parse_item_using<'de>(p: &mut Parser<'de>) -> Result, AstError> { fn parse_item_typedef<'de>(p: &mut Parser<'de>) -> Result, AstError> { p.expect(TokenKind::KeywordTypedef)?; - let ty = parse_type(p)?; + let mut ty = parse_type(p)?; let ident = parse_ident(p)?; + // Handle C-style array typedefs: `typedef char type24[3];` + while p.peek_kind() == Some(TokenKind::LeftBracket) { + p.bump()?; + let size = if p.peek_kind() != Some(TokenKind::RightBracket) { + Some(parse_expr(p)?) + } else { + None + }; + p.expect(TokenKind::RightBracket)?; + ty = Type::Array(TypeArray { + element: Box::new(ty), + size, + }); + } p.expect(TokenKind::Semicolon)?; Ok(ItemTypedef { attrs: Vec::new(), @@ -4011,6 +4025,41 @@ mod tests { } } + #[test] + fn parse_typedef_array() { + let file = parse("typedef char type24[3];"); + match &file.items[0] { + Item::Typedef(td) => { + assert_eq!(td.ident.sym, "type24"); + match &td.ty { + Type::Array(arr) => { + assert!(matches!(arr.element.as_ref(), Type::Fundamental(_))); + assert!(arr.size.is_some()); + } + other => panic!("expected Array type, got {other:?}"), + } + } + other => panic!("expected Typedef, got {other:?}"), + } + } + + #[test] + fn parse_typedef_array_2d() { + let file = parse("typedef int matrix[3][4];"); + match &file.items[0] { + Item::Typedef(td) => { + assert_eq!(td.ident.sym, "matrix"); + match &td.ty { + Type::Array(outer) => { + assert!(matches!(outer.element.as_ref(), Type::Array(_))); + } + other => panic!("expected Array type, got {other:?}"), + } + } + other => panic!("expected Typedef, got {other:?}"), + } + } + #[test] fn parse_enum_test() { let file = parse("enum Color { Red, Green, Blue };"); diff --git a/src/ast/ty.rs b/src/ast/ty.rs index 12b03c2..f91d1c5 100644 --- a/src/ast/ty.rs +++ b/src/ast/ty.rs @@ -9,7 +9,7 @@ use super::item::Path; use super::punct::Punctuated; /// The kind of a fundamental (built-in) type. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FundamentalKind { Void, Bool, @@ -67,6 +67,17 @@ pub enum Type<'de> { Qualified(TypeQualified<'de>), } +impl<'de> Type<'de> { + /// Check if the type is `auto`. + pub fn is_auto(&self) -> bool { + match self { + Type::Auto(_) => true, + Type::Qualified(q) => q.ty.is_auto(), + _ => false, + } + } +} + /// A fundamental (built-in) type. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TypeFundamental<'de> { diff --git a/src/lib.rs b/src/lib.rs index 85b30ef..db147d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ +#[cfg(feature = "ast")] pub mod ast; pub mod lex; +#[cfg(feature = "transpiler")] +pub mod transpile; use std::fmt; pub use lex::Lexer; @@ -32,6 +35,11 @@ impl<'de> SourceSpan<'de> { pub fn src(&self) -> &'de str { &self.src[core::ops::Range::from(*self)] } + + /// Returns the full backing source string (not just this span's slice). + pub fn full_source(&self) -> &'de str { + self.src + } } impl<'de> fmt::Debug for SourceSpan<'de> { diff --git a/src/transpile/error.rs b/src/transpile/error.rs new file mode 100644 index 0000000..6e0690c --- /dev/null +++ b/src/transpile/error.rs @@ -0,0 +1,39 @@ +//! Transpilation error types with rich diagnostics via miette + +use miette::Diagnostic; +use thiserror::Error; + +/// Errors that can occur during C++ → Rust type mapping +#[derive(Diagnostic, Debug, Error)] +pub enum TranspileError { + /// C++ path has no registered mapping + #[error("No mapping registered for C++ path `{path}`")] + UnmappedPath { + path: String, + #[source_code] + src: String, + #[label = "no mapping for this path"] + err_span: miette::SourceSpan, + }, + /// C++ type variant cannot be mapped to Rust + #[error("{message}")] + UnsupportedType { + message: String, + #[source_code] + src: String, + #[label = "{message}"] + err_span: miette::SourceSpan, + }, + /// C++ expression cannot be transpiled to Rust + #[error("{message}")] + UnsupportedExpr { + message: String, + #[source_code] + src: String, + #[label = "{message}"] + err_span: miette::SourceSpan, + }, + /// Invalid Rust type syntax provided to the builder + #[error("Invalid Rust type syntax `{rust_type}`: {reason}")] + InvalidRustType { rust_type: String, reason: String }, +} diff --git a/src/transpile/expr.rs b/src/transpile/expr.rs new file mode 100644 index 0000000..b0cc3f5 --- /dev/null +++ b/src/transpile/expr.rs @@ -0,0 +1,90 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; + +use crate::ast::expr::{Expr, ExprBool, ExprNullptr, ExprParen, ExprUnary, UnaryOp}; +use crate::transpile::{Transpile, Transpiler}; + +use super::error::TranspileError; + +/// Extract a source span from an expression (best-effort). +pub(crate) fn expr_span<'de>(expr: &Expr<'de>) -> Option> { + match expr { + Expr::Lit(l) => Some(l.span), + Expr::Ident(i) => Some(i.ident.span), + Expr::Path(p) => p.path.segments.first().map(|s| s.ident.span), + _ => None, + } +} + +/// Build a [`TranspileError::UnsupportedExpr`] from an expression. +pub(crate) fn unsupported_from_expr(message: &str, expr: &Expr<'_>) -> TranspileError { + match expr_span(expr) { + Some(span) => TranspileError::UnsupportedExpr { + message: message.to_owned(), + src: span.full_source().to_owned(), + err_span: span.into(), + }, + None => TranspileError::UnsupportedExpr { + message: message.to_owned(), + src: String::new(), + err_span: miette::SourceSpan::new(0.into(), 0), + }, + } +} + +impl<'de> Transpile for Expr<'de> { + #[allow(clippy::only_used_in_recursion)] + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError> { + match self { + Expr::Lit(lit) => { + let rust_expr: syn::Expr = syn::parse_str(lit.span.src()).map_err(|_| { + TranspileError::UnsupportedExpr { + message: "cannot parse literal".to_owned(), + src: lit.span.full_source().to_owned(), + err_span: lit.span.into(), + } + })?; + tokens.extend(quote::quote!(#rust_expr)); + } + Expr::Bool(ExprBool { value, .. }) => { + tokens.extend(quote::quote!(#value)); + } + Expr::Nullptr(ExprNullptr { .. }) => { + tokens.extend(quote::quote!(std::ptr::null())); + } + Expr::Ident(i) => { + i.ident.to_tokens(tokens); + } + Expr::Path(p) => { + let rust_expr = syn::Expr::try_from(p.path.clone()) + .map_err(|_| unsupported_from_expr("cannot transpile path expression", self))?; + tokens.extend(quote::quote!(#rust_expr)); + } + Expr::Unary(ExprUnary { + op: UnaryOp::Negate, + operand, + }) => { + let mut inner_tokens = TokenStream::new(); + operand.transpile(transpiler, &mut inner_tokens)?; + tokens.extend(quote::quote!(- #inner_tokens)); + } + Expr::Paren(ExprParen { expr }) => { + let mut inner_tokens = TokenStream::new(); + expr.transpile(transpiler, &mut inner_tokens)?; + tokens.extend(quote::quote!((#inner_tokens))); + } + other => { + return Err(unsupported_from_expr( + "expression cannot be transpiled to Rust", + other, + )); + } + } + + Ok(()) + } +} diff --git a/src/transpile/item.rs b/src/transpile/item.rs new file mode 100644 index 0000000..95b1faf --- /dev/null +++ b/src/transpile/item.rs @@ -0,0 +1,152 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse_str; + +use crate::{ + ast::{Ident, ItemEnum, Path}, + transpile::{Transpile, TranspileError, Transpiler}, +}; + +impl<'de> From> for syn::Ident { + fn from(ident: Ident<'de>) -> Self { + syn::Ident::new(ident.sym, proc_macro2::Span::call_site()) + } +} + +impl<'de> From<&Ident<'de>> for syn::Ident { + fn from(ident: &Ident<'de>) -> Self { + syn::Ident::new(ident.sym, proc_macro2::Span::call_site()) + } +} + +impl<'de> ToTokens for Ident<'de> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident: syn::Ident = self.into(); + ident.to_tokens(tokens); + } +} + +macro_rules! impl_try_from_path { + ($($target:ty),* $(,)?) => { + $( + impl<'de> TryFrom> for $target { + type Error = syn::Error; + + fn try_from(path: Path<'de>) -> Result { + parse_str(&path.to_string()) + } + } + )* + }; +} +impl_try_from_path!(syn::Type, syn::Path, syn::Expr); + +impl<'de> Transpile for ItemEnum<'de> { + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError> { + let name: syn::Ident = self + .ident + .as_ref() + .ok_or_else(|| TranspileError::UnsupportedType { + message: "anonymous enums cannot be transpiled".to_owned(), + src: String::new(), + err_span: miette::SourceSpan::new(0.into(), 0), + })? + .into(); + + // Build #[repr(...)] if an underlying type is specified + let repr_attr = match &self.underlying_type { + Some(ty) => { + let rust_ty = transpiler.ty_mapper.map_type(ty)?; + Some(quote::quote! { #[repr(#rust_ty)] }) + } + None => None, + }; + + // Build variant tokens + let mut variant_tokens = TokenStream::new(); + for variant in self.variants.iter() { + let v_name: syn::Ident = (&variant.ident).into(); + if let Some(ref disc) = variant.discriminant { + let mut expr_tokens = TokenStream::new(); + disc.transpile(transpiler, &mut expr_tokens)?; + variant_tokens.extend(quote::quote! { #v_name = #expr_tokens, }); + } else { + variant_tokens.extend(quote::quote! { #v_name, }); + } + } + + tokens.extend(quote::quote! { + #[doc = concat!(" Auto-transpiled enum for ", stringify!(#name))] + #repr_attr + pub enum #name { #variant_tokens } + }); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::parse_file; + + // ---- ItemEnum transpilation ---- + + #[test] + fn enum_class_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "enum class Color { Red, Green, Blue };"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Enum(e) => { + assert_eq!( + e.transpile_token_stream(&transpiler)?.to_string(), + "pub enum Color { Red , Green , Blue , }" + ); + } + item => panic!("expected ItemEnum, got {item:?}"), + } + + Ok(()) + } + + #[test] + fn enum_with_underlying_type_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "enum Color : int { Red, Green, Blue };"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Enum(e) => { + assert_eq!( + e.transpile_token_stream(&transpiler)?.to_string(), + "# [repr (i32)] pub enum Color { Red , Green , Blue , }" + ); + } + item => panic!("expected ItemEnum, got {item:?}"), + } + + Ok(()) + } + + #[test] + fn enum_with_discriminants_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "enum class Color : unsigned char { A = 1, B = 2 };"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Enum(e) => { + assert_eq!( + e.transpile_token_stream(&transpiler)?.to_string(), + "# [repr (u8)] pub enum Color { A = 1 , B = 2 , }" + ); + } + item => panic!("expected ItemEnum, got {item:?}"), + } + + Ok(()) + } +} diff --git a/src/transpile/mod.rs b/src/transpile/mod.rs new file mode 100644 index 0000000..17dc807 --- /dev/null +++ b/src/transpile/mod.rs @@ -0,0 +1,52 @@ +//! Transpiler module to convert C++ ([`crate::ast`]) into Rust ([`syn`]) + +pub mod error; +pub mod expr; +pub mod item; +pub mod ty; + +pub use error::TranspileError; +use proc_macro2::TokenStream; +use serde::Deserialize; +pub use ty::*; + +/// Transpiler struct, which is the configuration entrypoint for all transpilation operations. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct Transpiler { + pub ty_mapper: TypeMapper, +} + +pub trait Transpile { + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError>; + + /// Convert `self` with a `Transpiler` configuration into a `TokenStream` object. + /// + /// This method is implicitly implemented using `transpile`, and acts as a + /// convenience method for consumers of the `Transpile` trait. + fn transpile_token_stream( + &self, + transpiler: &Transpiler, + ) -> Result { + let mut tokens = TokenStream::new(); + self.transpile(transpiler, &mut tokens)?; + Ok(tokens) + } + + /// Convert `self` with a `Transpiler` configuration into a `TokenStream` object. + /// + /// This method is implicitly implemented using `transpile`, and acts as a + /// convenience method for consumers of the `Transpile` trait. + fn transpile_into_token_stream( + self, + transpiler: &Transpiler, + ) -> Result + where + Self: Sized, + { + self.transpile_token_stream(transpiler) + } +} diff --git a/src/transpile/ty.rs b/src/transpile/ty.rs new file mode 100644 index 0000000..2876117 --- /dev/null +++ b/src/transpile/ty.rs @@ -0,0 +1,1065 @@ +use std::collections::HashMap; + +use proc_macro2::TokenStream; +use serde::Deserialize; +use serde::de::{self, MapAccess, Visitor}; + +use crate::ast::ItemTypedef; +use crate::ast::expr::{Expr, ExprLit, LitKind}; +use crate::ast::item::{ItemConst, ItemStatic, Path}; +use crate::ast::ty::{FundamentalKind, TemplateArg, Type}; +use crate::transpile::expr::expr_span; +use crate::transpile::{Transpile, Transpiler}; + +use super::error::TranspileError; + +impl From for syn::Type { + fn from(kind: FundamentalKind) -> Self { + use FundamentalKind::*; + let s = match kind { + Void => "()", + Bool => "bool", + Char | Char8 | UnsignedChar => "u8", + Char16 | UnsignedShort => "u16", + Char32 | Wchar | UnsignedInt => "u32", + Short => "i16", + Int => "i32", + Long | LongLong => "i64", + Float => "f32", + Double | LongDouble => "f64", + SignedChar => "i8", + UnsignedLong | UnsignedLongLong => "u64", + }; + syn::parse_str(s).unwrap() + } +} + +/// Configurable mapper from C++ AST types to `syn::Type`. +/// +/// Built via [`TypeMapper::builder()`] or [`TypeMapper::new()`] (defaults only). +/// +/// ``` +/// use cppshift::transpile::TypeMapper; +/// use cppshift::ast::ty::{Type, TypeFundamental, FundamentalKind}; +/// use cppshift::SourceSpan; +/// +/// let mapper = TypeMapper::new(); +/// let src = "int"; +/// let ty = Type::Fundamental(TypeFundamental { +/// span: SourceSpan::new(src, 0, 3), +/// kind: FundamentalKind::Int, +/// }); +/// let rust_ty = mapper.map_type(&ty).expect("fundamental types always map"); +/// assert_eq!(quote::quote!(#rust_ty).to_string(), "i32"); +/// ``` +#[derive(Debug, Clone)] +pub struct TypeMapper { + paths: HashMap, +} + +/// Builder for [`TypeMapper`]. +pub struct TypeMapperBuilder { + paths: HashMap, +} + +impl TypeMapper { + /// Create a builder for configuring type mappings. + pub fn builder() -> TypeMapperBuilder { + TypeMapperBuilder { + paths: HashMap::new(), + } + } + + /// Create a mapper with default fundamental type mappings only. + pub fn new() -> Self { + Self::builder().build() + } + + /// Map a C++ AST type to a `syn::Type`. + /// + /// # Errors + /// + /// Returns [`TranspileError`] if the type cannot be mapped (e.g. unknown path, + /// `auto`, `decltype`, unsized array). + pub fn map_type(&self, ty: &Type<'_>) -> Result { + match ty { + Type::Fundamental(f) => Ok(syn::Type::from(f.kind)), + Type::Path(p) => self.resolve_path(&p.path), + Type::Ptr(p) => { + let inner = self.map_type(&p.pointee)?; + if p.cv.const_token { + Ok(syn::parse_quote!(*const #inner)) + } else { + Ok(syn::parse_quote!(*mut #inner)) + } + } + Type::Reference(r) => { + let inner = self.map_type(&r.referent)?; + if r.cv.const_token { + Ok(syn::parse_quote!(&#inner)) + } else { + Ok(syn::parse_quote!(&mut #inner)) + } + } + Type::RvalueReference(r) => self.map_type(&r.referent), + Type::Array(a) => { + let inner = self.map_type(&a.element)?; + match &a.size { + Some(Expr::Lit(lit)) if lit.kind == LitKind::Integer => { + let n: usize = + lit.span.src().parse().map_err(|_| { + unsupported_from_type("invalid array size literal", ty) + })?; + let lit_n = + syn::LitInt::new(&n.to_string(), proc_macro2::Span::call_site()); + Ok(syn::parse_quote!([#inner; #lit_n])) + } + _ => Err(unsupported_from_type("unsized or dynamic array", ty)), + } + } + Type::FnPtr(f) => { + let ret = self.map_type(&f.return_type)?; + let params: Result, _> = + f.params.iter().map(|p| self.map_type(p)).collect(); + let params = params?; + Ok(syn::parse_quote!(fn(#(#params),*) -> #ret)) + } + Type::Qualified(q) => self.map_type(&q.ty), + Type::TemplateInst(t) => { + let base_ty = self.resolve_path(&t.path)?; + let mapped_args: Result, _> = t + .args + .iter() + .map(|arg| match arg { + TemplateArg::Type(ty) => self.map_type(ty), + TemplateArg::Expr(_) => { + Err(unsupported_from_type("expression template argument", ty)) + } + }) + .collect(); + let mapped_args = mapped_args?; + + let mut result = base_ty; + if let syn::Type::Path(ref mut type_path) = result + && let Some(last_seg) = type_path.path.segments.last_mut() + { + last_seg.arguments = + syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: syn::token::Lt::default(), + args: mapped_args + .into_iter() + .map(syn::GenericArgument::Type) + .collect(), + gt_token: syn::token::Gt::default(), + }); + } + Ok(result) + } + Type::Auto(_) => Err(unsupported_from_type( + "auto type cannot be mapped to Rust", + ty, + )), + Type::Decltype(_) => Err(unsupported_from_type( + "decltype cannot be mapped to Rust", + ty, + )), + } + } + + fn resolve_path(&self, path: &Path<'_>) -> Result { + let key = path.to_string(); + if let Some(ty) = self.paths.get(&key) { + Ok(ty.clone()) + } else { + syn::Type::try_from(path.clone()).map_err(|_| unmapped_path_error(&key, path)) + } + } +} + +impl Default for TypeMapper { + fn default() -> Self { + Self::new() + } +} + +impl<'de> Deserialize<'de> for TypeMapper { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TypeMapperVisitor; + + impl<'de> Visitor<'de> for TypeMapperVisitor { + type Value = TypeMapper; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a map of C++ type paths to Rust type strings") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut builder = TypeMapper::builder(); + while let Some((cpp_path, rust_type)) = access.next_entry::()? { + builder = builder + .map_path(&cpp_path, &rust_type) + .map_err(de::Error::custom)?; + } + Ok(builder.build()) + } + } + + deserializer.deserialize_map(TypeMapperVisitor) + } +} + +impl TypeMapperBuilder { + /// Register a C++ path → Rust type mapping. + /// + /// The `rust_type` string is parsed via `syn::parse_str`. + /// + /// # Errors + /// + /// Returns [`TranspileError::InvalidRustType`] if `rust_type` is not valid Rust syntax. + pub fn map_path(mut self, cpp_path: &str, rust_type: &str) -> Result { + let ty: syn::Type = + syn::parse_str(rust_type).map_err(|e| TranspileError::InvalidRustType { + rust_type: rust_type.to_owned(), + reason: e.to_string(), + })?; + self.paths.insert(cpp_path.to_owned(), ty); + Ok(self) + } + + /// Register a C++ path → Rust type mapping with a pre-built `syn::Type`. + pub fn map_path_to_type(mut self, cpp_path: &str, ty: syn::Type) -> Self { + self.paths.insert(cpp_path.to_owned(), ty); + self + } + + /// Build the [`TypeMapper`]. + pub fn build(self) -> TypeMapper { + TypeMapper { paths: self.paths } + } +} + +/// Build an [`TranspileError::UnmappedPath`] from a path string and AST path. +fn unmapped_path_error(path_str: &str, path: &Path<'_>) -> TranspileError { + let span = path.segments.first().map(|s| s.ident.span); + match span { + Some(span) => TranspileError::UnmappedPath { + path: path_str.to_owned(), + src: span.full_source().to_owned(), + err_span: span.into(), + }, + None => TranspileError::UnmappedPath { + path: path_str.to_owned(), + src: String::new(), + err_span: miette::SourceSpan::new(0.into(), 0), + }, + } +} + +/// Build an [`TranspileError::UnsupportedType`] by extracting the best span from a [`Type`]. +fn unsupported_from_type(message: &str, ty: &Type<'_>) -> TranspileError { + match type_span(ty) { + Some(span) => TranspileError::UnsupportedType { + message: message.to_owned(), + src: span.full_source().to_owned(), + err_span: span.into(), + }, + None => TranspileError::UnsupportedType { + message: message.to_owned(), + src: String::new(), + err_span: miette::SourceSpan::new(0.into(), 0), + }, + } +} + +/// Extract the most relevant source span from a type, if available. +fn type_span<'de>(ty: &Type<'de>) -> Option> { + match ty { + Type::Fundamental(f) => Some(f.span), + Type::Path(p) => p.path.segments.first().map(|s| s.ident.span), + Type::Auto(a) => Some(a.span), + Type::Decltype(d) => expr_span(&d.expr), + Type::Ptr(p) => type_span(&p.pointee), + Type::Reference(r) => type_span(&r.referent), + Type::RvalueReference(r) => type_span(&r.referent), + Type::Array(a) => type_span(&a.element), + Type::FnPtr(f) => type_span(&f.return_type), + Type::Qualified(q) => type_span(&q.ty), + Type::TemplateInst(t) => t.path.segments.first().map(|s| s.ident.span), + } +} + +impl<'de> Transpile for ItemTypedef<'de> { + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError> { + let name = self.ident; + if let Type::Path(p) = &self.ty { + let rust_ty = transpiler.ty_mapper.resolve_path(&p.path)?; + tokens.extend(quote::quote! { + #[doc = concat!(" Auto-transpiled type for ", stringify!(#name))] + pub type #name = #rust_ty; + }); + } else { + let rust_ty = transpiler.ty_mapper.map_type(&self.ty)?; + tokens.extend(quote::quote! { + #[doc = concat!(" Auto-transpiled type for ", stringify!(#name))] + pub type #name = #rust_ty; + }); + } + + Ok(()) + } +} + +/// Returns `true` if `ty` is a byte-sized char type (char, char8_t, unsigned char, signed char), +/// stripping CV-qualifiers. +fn is_char_element_type(ty: &Type<'_>) -> bool { + use FundamentalKind::*; + match ty { + Type::Fundamental(f) => matches!(f.kind, Char | Char8 | UnsignedChar | SignedChar), + Type::Qualified(q) => is_char_element_type(&q.ty), + _ => false, + } +} + +/// Count the number of code units in the raw content of a C string literal +/// (text between the outer quotes), handling escape sequences. +fn count_c_string_chars(inner: &str) -> usize { + let bytes = inner.as_bytes(); + let mut count = 0; + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'\\' { + i += 1; + match bytes.get(i) { + Some(b'x') => { + // \xNN — skip x + up to 2 hex digits + i += 1; + let mut n = 0; + while n < 2 && bytes.get(i).is_some_and(|b| b.is_ascii_hexdigit()) { + i += 1; + n += 1; + } + } + Some(b'u') => i += 5, // \uNNNN + Some(b'U') => i += 9, // \UNNNNNNNN + Some(b'0'..=b'7') => { + // \NNN — skip first octal digit + up to 2 more + i += 1; + let mut n = 0; + while n < 2 && bytes.get(i).is_some_and(|b| matches!(b, b'0'..=b'7')) { + i += 1; + n += 1; + } + } + _ => i += 1, // \n, \t, \\, \", etc. + } + } else { + i += 1; + } + count += 1; + } + count +} + +/// Try to transpile an unsized char array initialised with a string literal. +/// +/// C++: `const char foo[] = "ALPN";` / `static char foo[] = "ALPN";` +/// Rust: `pub static foo: [u8; 5] = *b"ALPN\0";` +/// +/// `keyword` is the Rust storage keyword to emit (`const` or `static`). +/// Returns `None` if the pattern doesn't match and normal mapping should proceed. +fn try_transpile_char_array_from_str_lit<'de>( + name: crate::ast::item::Ident<'de>, + element: &Type<'de>, + expr: &Expr<'de>, + transpiler: &Transpiler, + keyword: &str, + tokens: &mut TokenStream, +) -> Option> { + if !is_char_element_type(element) { + return None; + } + let Expr::Lit(ExprLit { + span, + kind: LitKind::String, + }) = expr + else { + return None; + }; + + let raw = span.src(); // e.g. `"ALPN"` (including surrounding quotes) + if raw.len() < 2 { + return None; + } + let inner = &raw[1..raw.len() - 1]; // strip surrounding quotes + let len = count_c_string_chars(inner) + 1; // +1 for null terminator + let elem_ty = match transpiler.ty_mapper.map_type(element) { + Ok(t) => t, + Err(e) => return Some(Err(e)), + }; + let lit_n = syn::LitInt::new(&len.to_string(), proc_macro2::Span::call_site()); + + // Build `*b"...\0"` by inserting `\0` before the closing quote. + let byte_expr_src = format!("*b{}\\0\"", &raw[..raw.len() - 1]); + let byte_expr: syn::Expr = match syn::parse_str(&byte_expr_src) { + Ok(e) => e, + Err(_) => { + return Some(Err(TranspileError::UnsupportedExpr { + message: format!("cannot convert C++ string literal `{raw}` to Rust byte string"), + src: span.full_source().to_owned(), + err_span: (*span).into(), + })); + } + }; + + let keyword_tok: proc_macro2::TokenStream = keyword.parse().unwrap(); + tokens.extend(quote::quote! { + pub #keyword_tok #name: [#elem_ty; #lit_n] = #byte_expr; + }); + Some(Ok(())) +} + +impl<'de> Transpile for ItemConst<'de> { + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError> { + let name = self.ident; + + // Special case: `const char foo[] = "ALPN";` → `pub const foo: [u8; 5] = *b"ALPN\0";` + if let Type::Array(arr) = &self.ty + && arr.size.is_none() + && let Some(result) = try_transpile_char_array_from_str_lit( + name, + &arr.element, + &self.expr, + transpiler, + "const", + tokens, + ) + { + return result; + } + + let rust_ty = transpiler.ty_mapper.map_type(&self.ty)?; + let mut expr_tokens = TokenStream::new(); + self.expr.transpile(transpiler, &mut expr_tokens)?; + tokens.extend(quote::quote! { + pub const #name: #rust_ty = #expr_tokens; + }); + + Ok(()) + } +} + +impl<'de> Transpile for ItemStatic<'de> { + fn transpile( + &self, + transpiler: &Transpiler, + tokens: &mut TokenStream, + ) -> Result<(), TranspileError> { + let name = self.ident; + let expr = self + .expr + .as_ref() + .ok_or_else(|| TranspileError::UnsupportedExpr { + message: "Rust statics require an initializer".to_owned(), + src: name.span.full_source().to_owned(), + err_span: name.span.into(), + })?; + + // Special case: `static char foo[] = "ALPN";` → `pub static foo: [u8; 5] = *b"ALPN\0";` + if let Type::Array(arr) = &self.ty + && arr.size.is_none() + && let Some(result) = try_transpile_char_array_from_str_lit( + name, + &arr.element, + expr, + transpiler, + "static", + tokens, + ) + { + return result; + } + + let rust_ty = transpiler.ty_mapper.map_type(&self.ty)?; + let mut expr_tokens = TokenStream::new(); + expr.transpile(transpiler, &mut expr_tokens)?; + tokens.extend(quote::quote! { + pub static #name: #rust_ty = #expr_tokens; + }); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::quote; + + use crate::SourceSpan; + use crate::ast::expr::ExprLit; + use crate::ast::item::{Ident, PathSegment}; + use crate::ast::punct::Punctuated; + use crate::ast::{parse_file, ty::*}; + + fn ty_str(ty: &syn::Type) -> String { + quote!(#ty).to_string() + } + + fn make_fundamental(src: &str, kind: FundamentalKind) -> Type<'_> { + Type::Fundamental(TypeFundamental { + span: SourceSpan::new(src, 0, src.len()), + kind, + }) + } + + fn make_path<'a>(src: &'a str, segments: &[&'a str]) -> Type<'a> { + Type::Path(TypePath { + path: make_raw_path(src, segments), + }) + } + + fn make_raw_path<'a>(src: &'a str, segments: &[&'a str]) -> Path<'a> { + Path { + leading_colon: false, + segments: segments + .iter() + .map(|s| { + let offset = s.as_ptr() as usize - src.as_ptr() as usize; + PathSegment { + ident: Ident { + sym: s, + span: SourceSpan::new(src, offset, s.len()), + }, + } + }) + .collect(), + } + } + + #[test] + fn typedef_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler { + ty_mapper: TypeMapper::builder() + .map_path("std::string", "BytesMut")? + .build(), + }; + + let typedef_header = r#" + typedef Custom::int16 MyInt16; + typedef std::string MyString; + typedef char type24[3]; + "#; + + let typedef_file = parse_file(typedef_header).unwrap(); + let mut typedef_iter = typedef_file.items.iter(); + + match typedef_iter.next() { + Some(crate::ast::Item::Typedef(t)) => { + assert_eq!( + "# [doc = concat ! (\" Auto-transpiled type for \" , stringify ! (MyInt16))] pub type MyInt16 = Custom :: int16 ;", + t.transpile_token_stream(&transpiler)?.to_string() + ); + } + t => panic!("unexpected typedef {t:?}"), + }; + + match typedef_iter.next() { + Some(crate::ast::Item::Typedef(t)) => { + assert_eq!( + "# [doc = concat ! (\" Auto-transpiled type for \" , stringify ! (MyString))] pub type MyString = BytesMut ;", + t.transpile_token_stream(&transpiler)?.to_string() + ); + } + t => panic!("unexpected typedef {t:?}"), + }; + + match typedef_iter.next() { + Some(crate::ast::Item::Typedef(t)) => { + assert_eq!( + "# [doc = concat ! (\" Auto-transpiled type for \" , stringify ! (type24))] pub type type24 = [u8 ; 3] ;", + t.transpile_token_stream(&transpiler)?.to_string() + ); + } + t => panic!("unexpected typedef {t:?}"), + }; + + Ok(()) + } + + // ---- Fundamental types (table-driven) ---- + + #[test] + fn fundamental_defaults() -> Result<(), TranspileError> { + use FundamentalKind::*; + let mapper = TypeMapper::new(); + let cases: &[(FundamentalKind, &str, &str)] = &[ + (Void, "void", "()"), + (Bool, "bool", "bool"), + (Char, "char", "u8"), + (Char8, "char8_t", "u8"), + (Char16, "char16_t", "u16"), + (Char32, "char32_t", "u32"), + (Wchar, "wchar_t", "u32"), + (Short, "short", "i16"), + (Int, "int", "i32"), + (Long, "long", "i64"), + (LongLong, "long long", "i64"), + (Float, "float", "f32"), + (Double, "double", "f64"), + (LongDouble, "long double", "f64"), + (SignedChar, "signed char", "i8"), + (UnsignedChar, "unsigned char", "u8"), + (UnsignedShort, "unsigned short", "u16"), + (UnsignedInt, "unsigned int", "u32"), + (UnsignedLong, "unsigned long", "u64"), + (UnsignedLongLong, "unsigned long long", "u64"), + ]; + for &(kind, src, expected) in cases { + let ty = make_fundamental(src, kind); + let result = mapper.map_type(&ty)?; + assert_eq!(ty_str(&result), expected, "failed for {kind:?}"); + } + Ok(()) + } + + // ---- Path mappings ---- + + #[test] + fn path_mapping_custom() -> Result<(), TranspileError> { + let mapper = TypeMapper::builder() + .map_path("Custom::int32", "i32")? + .map_path("std::string", "String")? + .build(); + + let src = "Custom::int32"; + let ty = make_path(src, &[&src[..6], &src[8..]]); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "i32"); + + let src2 = "std::string"; + let ty2 = make_path(src2, &[&src2[..3], &src2[5..]]); + assert_eq!(ty_str(&mapper.map_type(&ty2)?), "String"); + Ok(()) + } + + #[test] + fn path_unknown_passes_through() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "Unknown"; + let ty = make_path(src, &[src]); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "Unknown"); + Ok(()) + } + + // ---- Composite types ---- + + #[test] + fn const_ptr() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Ptr(TypePtr { + cv: CvQualifiers { + const_token: true, + volatile_token: false, + }, + pointee: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "* const i32"); + Ok(()) + } + + #[test] + fn mut_ptr() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Ptr(TypePtr { + cv: CvQualifiers::default(), + pointee: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "* mut i32"); + Ok(()) + } + + #[test] + fn reference_mut() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Reference(TypeReference { + cv: CvQualifiers::default(), + referent: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "& mut i32"); + Ok(()) + } + + #[test] + fn reference_const() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Reference(TypeReference { + cv: CvQualifiers { + const_token: true, + volatile_token: false, + }, + referent: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "& i32"); + Ok(()) + } + + #[test] + fn rvalue_reference() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::RvalueReference(TypeRvalueReference { + referent: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "i32"); + Ok(()) + } + + #[test] + fn array_with_size() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src_elem = "int"; + let src_size = "10"; + let inner = make_fundamental(src_elem, FundamentalKind::Int); + let ty = Type::Array(TypeArray { + element: Box::new(inner), + size: Some(Expr::Lit(ExprLit { + span: SourceSpan::new(src_size, 0, 2), + kind: LitKind::Integer, + })), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "[i32 ; 10]"); + Ok(()) + } + + #[test] + fn array_without_size() { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Array(TypeArray { + element: Box::new(inner), + size: None, + }); + assert!(mapper.map_type(&ty).is_err()); + } + + // ---- CV-qualified ---- + + #[test] + fn cv_qualified_strips() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "int"; + let inner = make_fundamental(src, FundamentalKind::Int); + let ty = Type::Qualified(TypeQualified { + cv: CvQualifiers { + const_token: true, + volatile_token: false, + }, + ty: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "i32"); + Ok(()) + } + + // ---- Template instantiation ---- + + #[test] + fn template_inst_with_mapped_args() -> Result<(), TranspileError> { + let mapper = TypeMapper::builder() + .map_path("std::vector", "Vec")? + .build(); + + let path_src = "std::vector"; + let inner_src = "int"; + let inner = make_fundamental(inner_src, FundamentalKind::Int); + let ty = Type::TemplateInst(TypeTemplateInst { + path: make_raw_path(path_src, &[&path_src[..3], &path_src[5..]]), + args: vec![TemplateArg::Type(inner)], + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "Vec < i32 >"); + Ok(()) + } + + #[test] + fn template_inst_unknown_path_passes_through() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let path_src = "std::deque"; + let inner_src = "int"; + let inner = make_fundamental(inner_src, FundamentalKind::Int); + let ty = Type::TemplateInst(TypeTemplateInst { + path: make_raw_path(path_src, &[&path_src[..3], &path_src[5..]]), + args: vec![TemplateArg::Type(inner)], + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "std :: deque < i32 >"); + Ok(()) + } + + #[test] + fn template_inst_unmapped_arg_passes_through() -> Result<(), TranspileError> { + let mapper = TypeMapper::builder() + .map_path("std::vector", "Vec")? + .build(); + + let path_src = "std::vector"; + let inner_src = "Unknown"; + let inner = make_path(inner_src, &[inner_src]); + let ty = Type::TemplateInst(TypeTemplateInst { + path: make_raw_path(path_src, &[&path_src[..3], &path_src[5..]]), + args: vec![TemplateArg::Type(inner)], + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "Vec < Unknown >"); + Ok(()) + } + + // ---- auto / decltype ---- + + #[test] + fn auto_returns_err() { + let mapper = TypeMapper::new(); + let src = "auto"; + let ty = Type::Auto(TypeAuto { + span: SourceSpan::new(src, 0, 4), + }); + assert!(mapper.map_type(&ty).is_err()); + } + + #[test] + fn decltype_returns_err() { + let mapper = TypeMapper::new(); + let src = "x"; + let ty = Type::Decltype(TypeDecltype { + expr: Expr::Ident(crate::ast::expr::ExprIdent { + ident: Ident { + sym: src, + span: SourceSpan::new(src, 0, 1), + }, + }), + }); + assert!(mapper.map_type(&ty).is_err()); + } + + // ---- Inner type unknown in composite ---- + + #[test] + fn ptr_unknown_inner_passes_through() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let src = "Unknown"; + let inner = make_path(src, &[src]); + let ty = Type::Ptr(TypePtr { + cv: CvQualifiers::default(), + pointee: Box::new(inner), + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "* mut Unknown"); + Ok(()) + } + + // ---- Function pointer ---- + + #[test] + fn fn_ptr() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let ret_src = "int"; + let p1_src = "double"; + let p2_src = "float"; + + let ret = make_fundamental(ret_src, FundamentalKind::Int); + let p1 = make_fundamental(p1_src, FundamentalKind::Double); + let p2 = make_fundamental(p2_src, FundamentalKind::Float); + + let mut params = Punctuated::new(); + params.push_value(p1); + params.push_value(p2); + + let ty = Type::FnPtr(TypeFnPtr { + return_type: Box::new(ret), + params, + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "fn (f64 , f32) -> i32"); + Ok(()) + } + + #[test] + fn fn_ptr_unmapped_param_passes_through() -> Result<(), TranspileError> { + let mapper = TypeMapper::new(); + let ret_src = "int"; + let p_src = "Unknown"; + + let ret = make_fundamental(ret_src, FundamentalKind::Int); + let p = make_path(p_src, &[p_src]); + + let mut params = Punctuated::new(); + params.push_value(p); + + let ty = Type::FnPtr(TypeFnPtr { + return_type: Box::new(ret), + params, + }); + assert_eq!(ty_str(&mapper.map_type(&ty)?), "fn (Unknown) -> i32"); + Ok(()) + } + + // ---- Builder error ---- + + #[test] + fn builder_invalid_rust_type() { + let result = TypeMapper::builder().map_path("foo", "not a {{ valid type"); + assert!(result.is_err()); + } + + // ---- Error diagnostics ---- + + #[test] + fn error_is_diagnostic() { + let mapper = TypeMapper::new(); + let src = "auto"; + let ty = Type::Auto(TypeAuto { + span: SourceSpan::new(src, 0, 4), + }); + let err = mapper.map_type(&ty).unwrap_err(); + // TranspileError implements miette::Diagnostic + let diagnostic: &dyn miette::Diagnostic = &err; + assert!(diagnostic.source_code().is_some()); + assert!(diagnostic.labels().is_some()); + } + + // ---- ItemConst / ItemStatic transpilation ---- + + #[test] + fn const_int_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "const int MAX = 100;"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Const(c) => { + assert_eq!( + c.transpile_token_stream(&transpiler)?.to_string(), + "pub const MAX : i32 = 100 ;" + ); + } + item => panic!("expected ItemConst, got {item:?}"), + } + Ok(()) + } + + #[test] + fn constexpr_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "constexpr double PI = 3.14;"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Const(c) => { + assert_eq!( + c.transpile_token_stream(&transpiler)?.to_string(), + "pub const PI : f64 = 3.14 ;" + ); + } + item => panic!("expected ItemConst, got {item:?}"), + } + Ok(()) + } + + #[test] + fn const_bool_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "const bool FLAG = true;"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Const(c) => { + assert_eq!( + c.transpile_token_stream(&transpiler)?.to_string(), + "pub const FLAG : bool = true ;" + ); + } + item => panic!("expected ItemConst, got {item:?}"), + } + Ok(()) + } + + #[test] + fn static_int_transpiles() -> Result<(), TranspileError> { + let transpiler = Transpiler::default(); + let src = "static int count = 0;"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Static(s) => { + assert_eq!( + s.transpile_token_stream(&transpiler)?.to_string(), + "pub static count : i32 = 0 ;" + ); + } + item => panic!("expected ItemStatic, got {item:?}"), + } + Ok(()) + } + + #[test] + fn char_array_from_string_literal() -> Result<(), TranspileError> { + // `const char` is parsed as a static with a const-qualified element type. + // The transpiler should infer the size (4 chars + null terminator = 5). + let transpiler = Transpiler::default(); + let src = r#"const char listOfChars[] = "ALPN";"#; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Static(s) => { + let out = s.transpile_token_stream(&transpiler)?.to_string(); + assert_eq!(out, r#"pub static listOfChars : [u8 ; 5] = * b"ALPN\0" ;"#); + } + item => panic!("expected ItemStatic, got {item:?}"), + } + + Ok(()) + } + + #[test] + fn char_array_with_escape_sequence() -> Result<(), TranspileError> { + // `\n` counts as one character: size = 3 + 1 = 4. + let transpiler = Transpiler::default(); + let src = r#"const char nl[] = "a\nb";"#; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Static(s) => { + let out = s.transpile_token_stream(&transpiler)?.to_string(); + assert_eq!(out, r#"pub static nl : [u8 ; 4] = * b"a\nb\0" ;"#); + } + item => panic!("expected ItemStatic, got {item:?}"), + } + + Ok(()) + } + + #[test] + fn static_no_init_errors() { + let transpiler = Transpiler::default(); + let src = "static int count;"; + let file = parse_file(src).unwrap(); + match &file.items[0] { + crate::ast::Item::Static(s) => { + assert!(s.transpile_token_stream(&transpiler).is_err()); + } + item => panic!("expected ItemStatic, got {item:?}"), + } + } +}