Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
Expand Down
16 changes: 16 additions & 0 deletions src/ast/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -53,6 +55,20 @@ pub struct Path<'de> {
pub segments: Vec<PathSegment<'de>>,
}

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::<Vec<_>>()
.join("::")
)
}
}

/// A single segment of a path, analogous to `syn::PathSegment`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PathSegment<'de> {
Expand Down
51 changes: 50 additions & 1 deletion src/ast/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 +545,22 @@ fn parse_item_using<'de>(p: &mut Parser<'de>) -> Result<Item<'de>, AstError> {

fn parse_item_typedef<'de>(p: &mut Parser<'de>) -> Result<ItemTypedef<'de>, 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(),
Expand Down Expand Up @@ -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 };");
Expand Down
2 changes: 1 addition & 1 deletion src/ast/ty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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> {
Expand Down
30 changes: 30 additions & 0 deletions src/transpile/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! 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,
},
/// Invalid Rust type syntax provided to the builder
#[error("Invalid Rust type syntax `{rust_type}`: {reason}")]
InvalidRustType { rust_type: String, reason: String },
}
39 changes: 39 additions & 0 deletions src/transpile/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse_str;

use crate::ast::{Ident, Path};

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> 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<Path<'de>> for $target {
type Error = syn::Error;

fn try_from(path: Path<'de>) -> Result<Self, Self::Error> {
parse_str(&path.to_string())
}
}
)*
};
}
impl_try_from_path!(syn::Type, syn::Path, syn::Expr);
51 changes: 51 additions & 0 deletions src/transpile/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Transpiler module to convert C++ ([`crate::ast`]) into Rust ([`syn`])

pub mod error;
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<TokenStream, TranspileError> {
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<TokenStream, TranspileError>
where
Self: Sized,
{
self.transpile_token_stream(transpiler)
}
}
Loading
Loading