diff --git a/.gitignore b/.gitignore index 39ffcc7..1787bff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ /target /result -# direnv +# dev env .direnv +.ccls-cache + # trixy is a library Cargo.lock diff --git a/.licensure.yml b/.licensure.yml index a0789e6..b61088b 100644 --- a/.licensure.yml +++ b/.licensure.yml @@ -102,7 +102,7 @@ comments: commenter: type: line comment_char: "//" - trailing_lines: 2 + trailing_lines: 1 - extensions: - rs @@ -124,7 +124,7 @@ comments: start_block_char: "/*\n" end_block_char: "*/\n" per_line_char: "*" - trailing_lines: 2 + trailing_lines: 1 # In this case extension is singular and a single string extension is provided. - extension: html @@ -139,7 +139,7 @@ comments: commenter: type: line comment_char: ";;;" - trailing_lines: 2 + trailing_lines: 1 - extensions: - ebnf @@ -148,7 +148,7 @@ comments: start_block_char: "#(*\n" end_block_char: "#*)\n" per_line_char: "#" - trailing_lines: 2 + trailing_lines: 1 # The extension string "any" is special and so will match any file # extensions. Commenter configurations are always checked in the @@ -162,4 +162,4 @@ comments: commenter: type: line comment_char: '#' - trailing_lines: 2 + trailing_lines: 1 diff --git a/Cargo.toml b/Cargo.toml index 2770481..18d6742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,13 +22,12 @@ name = "trixy" version = "0.1.0" edition = "2021" -[lib] -proc-macro = true - [dependencies] convert_case = "0.6.0" +log = "0.4.20" proc-macro2 = "1.0.70" quote = "1.0.33" syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] } -trixy-lang_parser = { path = "./trixy-lang_parser" } -trixy-macros = { path = "./trixy-macros" } +thiserror = "1.0.51" +# trixy-lang_parser = { path = "./trixy-lang_parser" } +# trixy-macros = { path = "./trixy-macros" } diff --git a/flake.nix b/flake.nix index 1ca9c2d..25dcfbe 100644 --- a/flake.nix +++ b/flake.nix @@ -17,6 +17,7 @@ # If not, see . + { description = "A rust crate used to generate multi-language apis for your application"; @@ -122,6 +123,10 @@ cargo-edit cargo-expand + # Used to format the c header files + libclang + gdb + ebnf2pdf.outputs.packages."${system}".default ]; inherit nativeBuildInputs buildInputs; diff --git a/trixy-macros/examples/main/api_correct.tri b/src/error/mod.rs similarity index 69% rename from trixy-macros/examples/main/api_correct.tri rename to src/error/mod.rs index 44f64f3..612b235 100644 --- a/trixy-macros/examples/main/api_correct.tri +++ b/src/error/mod.rs @@ -18,23 +18,15 @@ * If not, see . */ +use std::ffi::c_char; -nasp trinitrix { - struct Callback { - func: String, - timeout: String, - }; +use thiserror::Error; - enum CallbackPriority { - High, - Medium, - Low, - }; +#[derive(Error, Debug)] +pub enum TypeConversionError { + #[error("Can't convert this ('{got:#?}') string into a valid rust string; Remember that these must be valid utf-8")] + String { got: *const c_char }, - fn execute_callback(callback: Callback, priority: CallbackPriority); + #[error("You passed a null pointer to the conversion function!")] + NullPointer, } - - - -// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing: -// vim: syntax=rust diff --git a/src/lib.rs b/src/lib.rs index 914c2f8..e0e7dcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,3 +17,28 @@ * and the Lesser GNU General Public License along with this program. * If not, see . */ + +//! Trixy contains the types used by the [trixy-macros] crate to provide ffi safe types +use std::{ffi::c_char, ops::Deref}; + +use proc_macro2::TokenStream; +use quote::quote; + +pub mod error; +pub mod traits; + +// NOTE(@soispha): All types specified here *MUST* be include in the BASE_TYPES constant, otherwise +// they are not usable from Trixy code <2023-12-25> + +#[repr(C)] +pub struct String(*const c_char); + +/// These are the "primitive" types used in Trixy, you can use any of them to create new structures +pub const BASE_TYPES: [&'static str; 2] = ["String", "u8"]; + +pub fn to_c_name>(rust_type: T) -> TokenStream { + match &*rust_type { + "String" => quote!(const char*), + other => panic! {"'{}' is not a vaild type name!", other}, + } +} diff --git a/src/traits/errno.rs b/src/traits/errno.rs new file mode 100644 index 0000000..a8ef7b4 --- /dev/null +++ b/src/traits/errno.rs @@ -0,0 +1,152 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + +use core::slice; +use std::{ + cell::RefCell, + error::Error, + ffi::{c_char, c_int}, + ptr, +}; + +use log::{error, warn}; + +use crate::error::TypeConversionError; + +#[macro_export] +macro_rules! convert { + ($input:expr) => { + match $input.try_into() { + Ok(ok) => ok, + Err(err) => { + trixy::traits::errno::set(err); + return 0; + } + } + }; +} + +// This code is heavily inspired by: https://michael-f-bryan.github.io/rust-ffi-guide/errors/return_types.html +thread_local! { + static LAST_ERROR: RefCell>> = RefCell::new(None); +} + +/// Update the most recent error, clearing whatever may have been there before. +pub fn set(error: TypeConversionError) { + error!("Setting LAST_ERROR: {}", error); + + { + // Print a pseudo-backtrace for this error, following back each error's + // cause until we reach the root error. + let mut cause = error.source(); + while let Some(parent_err) = cause { + warn!("Caused by: {}", parent_err); + cause = parent_err.source(); + } + } + + LAST_ERROR.with(|prev| { + *prev.borrow_mut() = Some(Box::new(error)); + }); +} + +/// Retrieve the most recent error, clearing it in the process. +pub fn take_last_error() -> Option> { + LAST_ERROR.with(|prev| prev.borrow_mut().take()) +} + +pub const ERROR_FUNCTIONS: &'static str = r#" +/// Calculate the number of bytes in the last error's error message **not** +/// including any trailing `null` characters. +extern int last_error_length(); + +/// Write the most recent error message into a caller-provided buffer as a UTF-8 +/// string, returning the number of bytes written. +/// +/// # Note +/// +/// This writes a **UTF-8** string into the buffer. Windows users may need to +/// convert it to a UTF-16 "unicode" afterwards. +/// +/// If there are no recent errors then this returns `0` (because we wrote 0 +/// bytes). `-1` is returned if there are any errors, for example when passed a +/// null pointer or a buffer of insufficient size. +extern int last_error_message(char* buffer, int length); +"#; + +/// Calculate the number of bytes in the last error's error message **not** +/// including any trailing `null` characters. +#[no_mangle] +pub extern "C" fn last_error_length() -> c_int { + LAST_ERROR.with(|prev| match *prev.borrow() { + Some(ref err) => err.to_string().len() as c_int + 1, + None => 0, + }) +} + +/// Write the most recent error message into a caller-provided buffer as a UTF-8 +/// string, returning the number of bytes written. +/// +/// # Note +/// +/// This writes a **UTF-8** string into the buffer. Windows users may need to +/// convert it to a UTF-16 "unicode" afterwards. +/// +/// If there are no recent errors then this returns `0` (because we wrote 0 +/// bytes). `-1` is returned if there are any errors, for example when passed a +/// null pointer or a buffer of insufficient size. +#[no_mangle] +pub unsafe extern "C" fn last_error_message(buffer: *mut c_char, length: c_int) -> c_int { + if buffer.is_null() { + warn!("Null pointer passed into last_error_message() as the buffer"); + return -1; + } + + let last_error = match take_last_error() { + Some(err) => err, + None => return 0, + }; + + let error_message = last_error.to_string(); + + let buffer = slice::from_raw_parts_mut(buffer as *mut u8, length as usize); + + if error_message.len() >= buffer.len() { + warn!("Buffer provided for writing the last error message is too small."); + warn!( + "Expected at least {} bytes but got {}", + error_message.len() + 1, + buffer.len() + ); + return -1; + } + + ptr::copy_nonoverlapping( + error_message.as_ptr(), + buffer.as_mut_ptr(), + error_message.len(), + ); + + // Add a trailing null so people using the string as a `char *` don't + // accidentally read into garbage. + buffer[error_message.len()] = 0; + + error_message.len() as c_int +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs new file mode 100644 index 0000000..8e70861 --- /dev/null +++ b/src/traits/mod.rs @@ -0,0 +1,55 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + +use std::ffi::CStr; + +use crate::error::TypeConversionError; + +pub mod errno; + +impl TryFrom for &str { + type Error = crate::error::TypeConversionError; + + fn try_from(value: crate::String) -> Result { + let ptr = value.0; + if ptr.is_null() { + Err(TypeConversionError::NullPointer) + } else { + // SAFETY: We checked for null, which is a huge upside other things that we simply hope + // for: + // - null terminated + // - actually be a valid pointer (`(void*) 2` is *valid* c but not a *valid* pointer) + // - be contained in a single allocated object + // - the memory must obviously not be mutated, while this &str exists + // - the null terminator must be within `isize::MAX` from the start position + let str = unsafe { CStr::from_ptr(ptr) }; + str.to_str() + .map_err(|_err| crate::error::TypeConversionError::String { got: value.0 }) + } + } +} +impl TryFrom for String { + type Error = crate::error::TypeConversionError; + + fn try_from(value: crate::String) -> Result { + let str: &str = value.try_into()?; + Ok(str.to_owned()) + } +} diff --git a/trixy-lang_parser/Cargo.toml b/trixy-lang_parser/Cargo.toml index 9aa2415..69ec984 100644 --- a/trixy-lang_parser/Cargo.toml +++ b/trixy-lang_parser/Cargo.toml @@ -25,7 +25,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4.4.11", features = ["derive"] } +clap = { version = "4.4.11", features = ["derive"], optional = true } convert_case = "0.6.0" -pretty_assertions = "1.4.0" thiserror = "1.0.50" +trixy = { path = "../../trixy" } + +[dev-dependencies] +pretty_assertions = "1.4.0" + +[features] +build-binary = ["clap"] + +[[bin]] +name = "trixy-parser" +required-features = ["build-binary"] diff --git a/trixy-lang_parser/README.md b/trixy-lang_parser/README.md index 0a6f6bb..e48f30f 100644 --- a/trixy-lang_parser/README.md +++ b/trixy-lang_parser/README.md @@ -2,5 +2,9 @@ This crate contains a parser (and lexer) for the Trixy language. The corresponding grammar is in the grammar file [here](./docs/grammar.ebnf) encoded in [Extended Backus-Naur Form](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). +## Testing +A binary (`trixy-parser`) exists, which provides easy access to the different library +parsing steps + ## Docs Run `./generate_docs` to turn the grammar file into railroad diagrams. diff --git a/trixy-lang_parser/example/empty.tri b/trixy-lang_parser/example/empty.tri index 0da6daa..332e35f 100644 --- a/trixy-lang_parser/example/empty.tri +++ b/trixy-lang_parser/example/empty.tri @@ -19,3 +19,9 @@ */ + + +// an empty comment: +// + +// diff --git a/trixy-lang_parser/src/main.rs b/trixy-lang_parser/src/bin/trixy-parser.rs similarity index 99% rename from trixy-lang_parser/src/main.rs rename to trixy-lang_parser/src/bin/trixy-parser.rs index 2004949..f14ef39 100644 --- a/trixy-lang_parser/src/main.rs +++ b/trixy-lang_parser/src/bin/trixy-parser.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use std::{fs, process::exit}; use trixy_lang_parser::{lexing::TokenStream, parse_trixy_lang}; diff --git a/trixy-lang_parser/src/command_spec/checked.rs b/trixy-lang_parser/src/command_spec/checked.rs index d38ba5d..8c75c93 100644 --- a/trixy-lang_parser/src/command_spec/checked.rs +++ b/trixy-lang_parser/src/command_spec/checked.rs @@ -18,26 +18,17 @@ * If not, see . */ + + + //! This module contains the already type checked types. -use std::fmt::Display; +use std::fmt::{Display, Write}; use crate::lexing::TokenKind; use super::unchecked; -/// These are the "primitive" types used in trixy, you can use any of them to create new structures -pub const BASE_TYPES: [ConstIdentifier; 8] = [ - Identifier::from("Integer"), - Identifier::from("Float"), - Identifier::from("Decimal"), - Identifier::from("String"), - Identifier::from("Function"), - Identifier::from("Option"), - Identifier::from("Result"), - Identifier::from("Vec"), -]; - #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Namespace { pub name: Identifier, @@ -95,6 +86,28 @@ pub struct Type { pub identifier: Identifier, pub generic_args: Vec, } +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ident = &self.identifier.name; + + f.write_str(ident)?; + if !self.generic_args.is_empty() { + f.write_char('<')?; + let mut first_run = true; + for arg in &self.generic_args { + if !first_run { + f.write_str(", ")?; + } else { + first_run = false; + } + write!(f, "{}", arg)?; + } + f.write_char('>') + } else { + f.write_str("") + } + } +} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct NamedType { diff --git a/trixy-lang_parser/src/command_spec/mod.rs b/trixy-lang_parser/src/command_spec/mod.rs index a5ec343..e113a55 100644 --- a/trixy-lang_parser/src/command_spec/mod.rs +++ b/trixy-lang_parser/src/command_spec/mod.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + pub mod checked; pub mod unchecked; diff --git a/trixy-lang_parser/src/command_spec/unchecked.rs b/trixy-lang_parser/src/command_spec/unchecked.rs index 56f8468..2703db9 100644 --- a/trixy-lang_parser/src/command_spec/unchecked.rs +++ b/trixy-lang_parser/src/command_spec/unchecked.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + //! This module contains the not type checked types. //! These are generated on the first pass of the parser, to be later converted into the checked //! ones. diff --git a/trixy-lang_parser/src/error.rs b/trixy-lang_parser/src/error.rs index 4b4657a..c0ff457 100644 --- a/trixy-lang_parser/src/error.rs +++ b/trixy-lang_parser/src/error.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use core::fmt; use thiserror::Error; diff --git a/trixy-lang_parser/src/lexing/error.rs b/trixy-lang_parser/src/lexing/error.rs index 32a129c..722dfa1 100644 --- a/trixy-lang_parser/src/lexing/error.rs +++ b/trixy-lang_parser/src/lexing/error.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use std::{error::Error, fmt::Display}; use thiserror::Error; diff --git a/trixy-lang_parser/src/lexing/mod.rs b/trixy-lang_parser/src/lexing/mod.rs index e1a30e8..22d0888 100644 --- a/trixy-lang_parser/src/lexing/mod.rs +++ b/trixy-lang_parser/src/lexing/mod.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use std::fmt::Display; use self::{error::SpannedLexingError, tokenizer::Tokenizer}; diff --git a/trixy-lang_parser/src/lexing/test.rs b/trixy-lang_parser/src/lexing/test.rs index 7eb32fe..15060b7 100644 --- a/trixy-lang_parser/src/lexing/test.rs +++ b/trixy-lang_parser/src/lexing/test.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use crate::lexing::{Keyword, Token, TokenKind, TokenSpan}; use super::TokenStream; diff --git a/trixy-lang_parser/src/lexing/tokenizer.rs b/trixy-lang_parser/src/lexing/tokenizer.rs index 104b117..144f725 100644 --- a/trixy-lang_parser/src/lexing/tokenizer.rs +++ b/trixy-lang_parser/src/lexing/tokenizer.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + // This code is heavily inspired by: https://michael-f-bryan.github.io/static-analyser-in-rust/book/lex.html use crate::{ @@ -147,6 +150,18 @@ impl<'a> Tokenizer<'a> { } } +/// checks if the next char in the input str is a newline +fn end_of_line(text: &str) -> bool { + let next = text.chars().next(); + if let Some('\n') = next { + true + } else if let Some('\r') = next { + true + } else { + false + } +} + fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> { // every token starts with two slashes let slashes: &str = &text[..2]; @@ -155,25 +170,34 @@ fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> { } else { let text: &str = &text[2..]; if let Some('/') = text.chars().next() { - let text = &text[1..]; - let (doc_comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; + if end_of_line(&text) { + Ok((TokenKind::DocComment("".to_owned()), 1 + 3)) + } else { + let text = &text[1..]; + let (doc_comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; - // trim whitespace - let doc_comment = doc_comment.trim_start(); - let doc_comment = doc_comment.trim_end(); + // trim whitespace + let doc_comment = doc_comment.trim_start(); + let doc_comment = doc_comment.trim_end(); - return Ok(( - TokenKind::DocComment(doc_comment.to_owned()), - chars_read + 3, - )); + Ok(( + TokenKind::DocComment(doc_comment.to_owned()), + chars_read + 3, + )) + } + } else { + if end_of_line(&text) { + Ok((TokenKind::Comment("".to_owned()), 1 + 2)) + } else { + let (comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; + + // trim whitespace + let comment = comment.trim_start(); + let comment = comment.trim_end(); + + Ok((TokenKind::Comment(comment.to_owned()), chars_read + 2)) + } } - let (comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; - - // trim whitespace - let comment = comment.trim_start(); - let comment = comment.trim_end(); - - Ok((TokenKind::Comment(comment.to_owned()), chars_read + 2)) } } diff --git a/trixy-lang_parser/src/lib.rs b/trixy-lang_parser/src/lib.rs index e5f7ba8..0a715cf 100644 --- a/trixy-lang_parser/src/lib.rs +++ b/trixy-lang_parser/src/lib.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use error::TrixyError; use crate::lexing::TokenStream; diff --git a/trixy-lang_parser/src/parsing/checked/error.rs b/trixy-lang_parser/src/parsing/checked/error.rs index ae08ff5..19bd13b 100644 --- a/trixy-lang_parser/src/parsing/checked/error.rs +++ b/trixy-lang_parser/src/parsing/checked/error.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use thiserror::Error; use std::{error::Error, fmt::Display}; diff --git a/trixy-lang_parser/src/parsing/checked/mod.rs b/trixy-lang_parser/src/parsing/checked/mod.rs index 74aff7e..8c2ac09 100644 --- a/trixy-lang_parser/src/parsing/checked/mod.rs +++ b/trixy-lang_parser/src/parsing/checked/mod.rs @@ -18,15 +18,19 @@ * If not, see . */ + + + use std::mem; use convert_case::{Case, Casing}; +use trixy::BASE_TYPES; use crate::{ command_spec::{ checked::{ CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, NamedType, - Namespace, Structure, Type, BASE_TYPES, + Namespace, Structure, Type, }, unchecked::{ CommandSpec as UncheckedCommandSpec, DocNamedType as UncheckedDocNamedType, @@ -258,7 +262,7 @@ impl Parser { .iter() .map(|r#enum| Into::::into(r#enum.identifier.kind.clone())) .any(|ident| ident == identifier) - && !BASE_TYPES.iter().any(|ident| ident.name == identifier.name) + && !BASE_TYPES.iter().any(|ident| ident == &identifier.name) { return Err(ParsingError::TypeNotDeclared { r#type: identifier, diff --git a/trixy-lang_parser/src/parsing/checked/test.rs b/trixy-lang_parser/src/parsing/checked/test.rs index 6037feb..178dbc0 100644 --- a/trixy-lang_parser/src/parsing/checked/test.rs +++ b/trixy-lang_parser/src/parsing/checked/test.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use crate::command_spec::checked::{ Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, NamedType, Namespace, Structure, Type, diff --git a/trixy-lang_parser/src/parsing/mod.rs b/trixy-lang_parser/src/parsing/mod.rs index 01c4cea..5db7b1b 100644 --- a/trixy-lang_parser/src/parsing/mod.rs +++ b/trixy-lang_parser/src/parsing/mod.rs @@ -18,5 +18,8 @@ * If not, see . */ + + + pub mod checked; pub mod unchecked; diff --git a/trixy-lang_parser/src/parsing/unchecked/error.rs b/trixy-lang_parser/src/parsing/unchecked/error.rs index 476d932..7bcac27 100644 --- a/trixy-lang_parser/src/parsing/unchecked/error.rs +++ b/trixy-lang_parser/src/parsing/unchecked/error.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use std::{error::Error, fmt::Display}; use thiserror::Error; diff --git a/trixy-lang_parser/src/parsing/unchecked/mod.rs b/trixy-lang_parser/src/parsing/unchecked/mod.rs index b40d800..e97145d 100644 --- a/trixy-lang_parser/src/parsing/unchecked/mod.rs +++ b/trixy-lang_parser/src/parsing/unchecked/mod.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use std::mem; use crate::{ diff --git a/trixy-lang_parser/src/parsing/unchecked/test.rs b/trixy-lang_parser/src/parsing/unchecked/test.rs index f25083f..c712e12 100644 --- a/trixy-lang_parser/src/parsing/unchecked/test.rs +++ b/trixy-lang_parser/src/parsing/unchecked/test.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + use pretty_assertions::assert_eq; use crate::{ diff --git a/trixy-macros/Cargo.toml b/trixy-macros/Cargo.toml index 75fa66e..24e9964 100644 --- a/trixy-macros/Cargo.toml +++ b/trixy-macros/Cargo.toml @@ -22,12 +22,11 @@ name = "trixy-macros" version = "0.1.0" edition = "2021" -[lib] -proc-macro = true - [dependencies] convert_case = "0.6.0" +prettyplease = "0.2.15" proc-macro2 = "1.0.70" quote = "1.0.33" syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] } trixy-lang_parser = { path = "../trixy-lang_parser" } +trixy = { path = "../../trixy" } diff --git a/trixy-macros/example/main/.gitignore b/trixy-macros/example/main/.gitignore new file mode 100644 index 0000000..99fbf70 --- /dev/null +++ b/trixy-macros/example/main/.gitignore @@ -0,0 +1,7 @@ +# build +/target +/result +/dist + +# This crate is a library +Cargo.lock diff --git a/trixy-macros/example/main/Cargo.toml b/trixy-macros/example/main/Cargo.toml new file mode 100644 index 0000000..94ff40b --- /dev/null +++ b/trixy-macros/example/main/Cargo.toml @@ -0,0 +1,35 @@ +# Copyright (C) 2023 The Trinitrix Project +# +# This file is part of the Trixy crate for Trinitrix. +# +# Trixy is free software: you can redistribute it and/or modify +# it under the terms of the Lesser GNU General Public License as +# published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# and the Lesser GNU General Public License along with this program. +# If not, see . + + +[package] +name = "main-example" +version = "0.0.0" +publish = false +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +trixy = { path = "../../../../trixy" } +env_logger = { version = "0.10.1" } +log = "0.4.20" + +[build-dependencies] +trixy-macros = {path = "../../../trixy-macros"} diff --git a/trixy-macros/example/main/build.rs b/trixy-macros/example/main/build.rs new file mode 100644 index 0000000..cca5cd7 --- /dev/null +++ b/trixy-macros/example/main/build.rs @@ -0,0 +1,31 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +use trixy_macros::config::TrixyConfig; + +fn main() { + println!("cargo:rerun-if-changed=./src/api.tri"); + TrixyConfig::new("callback") + .trixy_path("./src/api.tri") + .dist_dir_path("./dist") + .generate_debug(true) + .generate(); +} diff --git a/trixy-macros/example/main/c_src/main.c b/trixy-macros/example/main/c_src/main.c new file mode 100644 index 0000000..12d9332 --- /dev/null +++ b/trixy-macros/example/main/c_src/main.c @@ -0,0 +1,50 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +#include "../dist/interface.h" +#include +#include + +typedef struct { + int two; +} a_t; + +int main(void) { + fn_alone("hi"); + + if (!fn_alone(0x0)) { + int error_length = last_error_length(); + char *error = malloc(error_length); + last_error_message(error, error_length); + printf("Encountered error: %s\n", error); + free(error); + } + + one.fn_one(); + // one.two.fn_two(); + // + // two_t two = one.two; + // two.three.fn_three(); + // one.two.three.fn_three(); + // one_two_fn_two(); + + return 0; +} diff --git a/trixy-macros/example/main/src/api.tri b/trixy-macros/example/main/src/api.tri new file mode 100644 index 0000000..e43da58 --- /dev/null +++ b/trixy-macros/example/main/src/api.tri @@ -0,0 +1,54 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +// nasp trinitrix { +// struct Callback { +// func: String, +// timeout: String, +// }; +// +// enum CallbackPriority { +// High, +// Medium, +// Low, +// }; +// +// fn execute_callback(callback: Callback, priority: CallbackPriority); +// } + +/// Some doc comment +fn fn_alone(input: String); +/// Some doc comment +nasp one { + /// Some doc comment + fn fn_one(); + nasp two { + fn fn_two(); + nasp three { + fn fn_three(); + } + } +} + + + +// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing: +// vim: syntax=rust diff --git a/trixy-macros/examples/main/api_failing.tri b/trixy-macros/example/main/src/lib.rs similarity index 79% rename from trixy-macros/examples/main/api_failing.tri rename to trixy-macros/example/main/src/lib.rs index 5edbc63..753bb37 100644 --- a/trixy-macros/examples/main/api_failing.tri +++ b/trixy-macros/example/main/src/lib.rs @@ -19,12 +19,12 @@ */ -fn print(message: CommandTransferValue); -nasp trinitrix { {} - fn hi honner(name: String) -> String; ; + +macro_rules! callback { + ($cmd:expr) => {{ + println!("{:#?}", $cmd); + }}; } - -// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing: -// vim: syntax=rust +include!(concat!(env!("OUT_DIR"), "/api.rs")); diff --git a/trixy-macros/examples/main/main.rs b/trixy-macros/example/main/src/main.rs similarity index 83% rename from trixy-macros/examples/main/main.rs rename to trixy-macros/example/main/src/main.rs index f83b38a..bcfee48 100644 --- a/trixy-macros/examples/main/main.rs +++ b/trixy-macros/example/main/src/main.rs @@ -18,12 +18,8 @@ * If not, see . */ -use trixy_macros::trixy_generate; fn main() { - trixy_generate! { - path: "./examples/main/api.tri" - languages: rust, lua, c - generate_debug: false - } + let input = include_str!(concat!(env!("OUT_DIR"), "/interface.h")); + println!("{}", input); } diff --git a/trixy-macros/examples/main/api.tri b/trixy-macros/examples/main/api.tri deleted file mode 120000 index 98790fc..0000000 --- a/trixy-macros/examples/main/api.tri +++ /dev/null @@ -1 +0,0 @@ -api_correct.tri \ No newline at end of file diff --git a/trixy-macros/src/config/mod.rs b/trixy-macros/src/config/mod.rs index 84907db..8333f89 100644 --- a/trixy-macros/src/config/mod.rs +++ b/trixy-macros/src/config/mod.rs @@ -18,138 +18,119 @@ * If not, see . */ -//! This module is responsible for parsing the config passed to the macro call: -//! For example: + + + +//! This module is responsible for the config passed to trixy. +//! It works using the popular builder syntax: //! ```no_run -//! trixy_generate! { -//! path: "./path/to/trixy/file.tri" -//! languages: rust, lua, c -//! generate_debug: false -//! } +//! use trixy_macros::config::{Language, TrixyConfig}; +//!# fn main() { +//! let config = TrixyConfig::new() +//! .set_input_path("path/to/trixy/api.tri") +//! .set_output_path("api.rs") +//! .set_languages(vec![Language::Rust, Language::C]) +//! .set_generate_debug(false); +//!# } //! ``` -use std::path::PathBuf; - -use proc_macro2::Ident; -use syn::{parse::Parse, punctuated::Punctuated, LitBool, LitStr, Result, Token}; - -mod kw { - syn::custom_keyword!(path); - syn::custom_keyword!(languages); - syn::custom_keyword!(generate_debug); -} +use std::path::{Path, PathBuf}; #[derive(Debug)] pub enum Language { Rust, - Lua, C, + Lua, } -#[derive(Debug)] -struct Languages { - #[allow(dead_code)] - languages: kw::languages, - #[allow(dead_code)] - colon: Token![:], - raw: Punctuated, -} - -#[derive(Debug)] -struct Path { - #[allow(dead_code)] - path: kw::path, - #[allow(dead_code)] - colon: Token![:], - raw: PathBuf, -} - -#[derive(Debug)] -struct GenerateDebug { - #[allow(dead_code)] - generate_debug: kw::generate_debug, - #[allow(dead_code)] - colon: Token![:], - raw: bool, -} - -#[derive(Debug)] +#[derive(Default, Debug)] pub struct TrixyConfig { /// The Path to the base command interface config file - path: Path, + pub trixy_path: PathBuf, - /// The languages the commands should be exposed in - languages: Languages, + /// The name of the outputted host code (rust and c bindings) + /// This file is written in $OUT_DIR + pub host_code_name: PathBuf, + /// The name of the c header + /// This file is written in $OUT_DIR + pub c_header_name: PathBuf, + + /// The path from the root to the distribution directory. + /// Things like the c headers are copied in this dir + /// When this is [None] no dist dir will be generated + pub dist_dir_path: Option, + + /// Whether to check if the dist dir is empty before writing something to it + pub check_dist_dir: bool, /// Should the macro generate Debug trait implementation for each enum - /// These are very useful but completely obscure the `cargo expand` ouput - generate_debug: GenerateDebug, + /// These are very useful but completely obscure the `cargo expand` output + pub generate_debug: bool, + + /// This function is executed whenever an API function is called. + /// The only argument is the command (encoded as the `Commands`) enum + /// which is represented by this function + /// Because of rust limitations this is supposed to be the name of a macro acting as said + /// function. This macro must be in scope of the generated code + pub callback_function: String, } + impl TrixyConfig { - pub fn get_path(&self) -> &std::path::Path { - &self.path.raw + pub fn new>(callback_function: T) -> Self { + Self { + callback_function: callback_function.into(), + host_code_name: "api.rs".into(), + c_header_name: "interface.h".into(), + ..Default::default() + } } - pub fn generate_debug(&self) -> bool { - self.generate_debug.raw - } -} -impl Parse for TrixyConfig { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - Ok(Self { - path: input.parse()?, - languages: input.parse()?, - generate_debug: input.parse()?, - }) + pub fn trixy_path>(self, input_path: T) -> Self { + Self { + trixy_path: input_path.into(), + ..self + } } -} -impl Parse for GenerateDebug { - fn parse(input: syn::parse::ParseStream) -> Result { - let kw: kw::generate_debug = input.parse()?; - let colon: Token![:] = input.parse()?; - let raw = input.parse::()?.value(); - Ok(Self { - generate_debug: kw, - colon, - raw, - }) + pub fn generate_debug(self, generate_debug: bool) -> Self { + Self { + generate_debug, + ..self + } } -} -impl Parse for Path { - fn parse(input: syn::parse::ParseStream) -> Result { - let path: kw::path = input.parse()?; - let colon: Token![:] = input.parse()?; - let raw = PathBuf::from(input.parse::()?.value()); - Ok(Self { path, colon, raw }) + pub fn dist_dir_path>(self, dist_dir_path: T) -> Self { + Self { + dist_dir_path: Some(dist_dir_path.into()), + ..self + } } -} -impl Parse for Languages { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let languages: kw::languages = input.parse()?; - let colon: Token![:] = input.parse()?; - let raw = Punctuated::::parse_separated_nonempty(input)?; - Ok(Self { - languages, - colon, - raw, - }) + pub fn check_dist_dir(self, check_dist_dir: bool) -> Self { + Self { + check_dist_dir, + ..self + } } -} -impl Parse for Language { - fn parse(input: syn::parse::ParseStream) -> Result { - let ident: Ident = input.parse()?; - match &ident.to_string()[..] { - "rust" | "Rust" => Ok(Self::Rust), - "lua" | "Lua" => Ok(Self::Lua), - "c" | "C" => Ok(Self::C), - other => Err(input.error(format!( - "The language: `{}` is not a registered language!", - other - ))), + pub fn host_code_name>(self, output_path: T) -> Self { + Self { + host_code_name: output_path.into(), + ..self + } + } + + pub fn c_header_name>(self, output_path: T) -> Self { + Self { + c_header_name: output_path.into(), + ..self + } + } + + pub fn callback_function>(self, callback_function: T) -> Self { + Self { + callback_function: callback_function.into(), + ..self } } } diff --git a/trixy-macros/src/generate/c_api/header.rs b/trixy-macros/src/generate/c_api/header.rs new file mode 100644 index 0000000..7e7ac7f --- /dev/null +++ b/trixy-macros/src/generate/c_api/header.rs @@ -0,0 +1,293 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +//! This module generates the c header +//! It works by firstly listing the functions and then by grouping them into structures, effectively +//! simulating namespaces in c. + +use proc_macro2::{Ident, Punct, Spacing, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use trixy_lang_parser::command_spec::{ + Attribute, CommandSpec, Function, Identifier, NamedType, Namespace, +}; + +use crate::{ + config::TrixyConfig, + generate::{c_api::mangle_c_function_ident, identifier_to_rust, type_to_rust}, +}; + +const BEGIN_HEADER_GUARD: &'static str = r"#ifndef TRIXY_MAIN_HEADER + #define TRIXY_MAIN_HEADER"; +const END_HEADER_GUARD: &'static str = r"#endif // ifndef TRIXY_MAIN_HEADER"; + +/// This function acts as the core transformative aspect, turning this trixy code into the +/// following c header: +/// +/// *Trixy:* +/// ```text +/// fn fn_alone(); +/// nasp one { +/// fn fn_one(); +/// nasp two { +/// fn fn_two(); +/// nasp three { +/// fn fn_three(); +/// } +/// } +/// } +/// ``` +/// *C header:* +/// ```text +/// #ifndef TRIXY_MAIN_HEADER +/// #define TRIXY_MAIN_HEADER +/// +/// extern int fn_alone(); +/// extern int one_fn_one(); +/// extern int one_two_fn_two(); +/// extern int one_two_three_fn_three(); +/// +/// typedef struct { +/// void (*fn_three)(void); +/// } three_t; +/// typedef struct { +/// void (*fn_two)(void); +/// three_t three; +/// } two_t; +/// typedef struct { +/// void (*fn_one)(void); +/// two_t two; +/// } one_t; +/// +/// const three_t three = { +/// .fn_three = one_two_three_fn_three, +/// }; +/// const two_t two = { +/// .fn_two = one_two_fn_two, +/// .three = three, +/// }; +/// const one_t one = { +/// .fn_one = one_fn_one, +/// .two = two, +/// }; +/// #endif // ifndef TRIXY_MAIN_HEADER +/// ``` +pub fn generate(trixy: &CommandSpec, _config: &TrixyConfig) -> String { + let functions: String = trixy + .functions + .iter() + .map(|r#fn| function_to_header(r#fn, &[])) + .collect(); + let namespaces: String = trixy + .namespaces + .iter() + .map(|nasp| namespace_to_header(nasp, &vec![])) + .collect(); + + let type_defs: String = trixy + .namespaces + .iter() + .rev() + .map(|nasp| namespace_to_full_typedef(nasp)) + .collect::>() + .join("\n"); + + let struct_initializer: TokenStream2 = trixy + .namespaces + .iter() + .map(|nasp| namespace_to_full_struct_init(nasp, &vec![])) + .collect(); + + let output = quote! { + #struct_initializer + } + .to_string(); + format!( + "{}\n\n{}\n\n{}\n{}\n{}\n{}\n\n{}", + BEGIN_HEADER_GUARD, + trixy::traits::errno::ERROR_FUNCTIONS, + functions, + namespaces, + type_defs, + output, + END_HEADER_GUARD + ) +} + +fn function_to_header(function: &Function, namespaces: &[&Identifier]) -> String { + let doc_comments: String = function + .attributes + .iter() + .map(attribute_to_doc_comment) + .collect::(); + let ident = mangle_c_function_ident(function, namespaces); + let inputs: Vec = function.inputs.iter().map(named_type_to_c).collect(); + + let output = quote! { + extern int #ident(#(#inputs),*); + }; + format!("{}{}\n", doc_comments, output) +} + +fn attribute_to_doc_comment(attribute: &Attribute) -> String { + let Attribute::doc(doc_comment) = attribute; + format!("/// {}\n", doc_comment) +} + +fn named_type_to_c(named_type: &NamedType) -> TokenStream2 { + let ident = identifier_to_rust(&named_type.name); + let r#type = type_to_rust(&named_type.r#type); + let c_type = trixy::to_c_name(r#type.to_string()); + quote! { + #c_type #ident + } +} + +fn namespace_to_full_struct_init(nasp: &Namespace, namespaces: &Vec<&Identifier>) -> TokenStream2 { + let mut input_namespaces = namespaces.clone(); + input_namespaces.push(&nasp.name); + + let ident = identifier_to_rust(&nasp.name); + let type_ident = format_ident!("{}_t", ident.to_string()); + let functions: TokenStream2 = nasp + .functions + .iter() + .map(|r#fn| function_to_struct_init(r#fn, &input_namespaces)) + .collect(); + let namespaces: TokenStream2 = nasp + .namespaces + .iter() + .map(namespace_to_struct_init) + .collect(); + + let next_namespace: TokenStream2 = nasp + .namespaces + .iter() + .map(|nasp| namespace_to_full_struct_init(nasp, &input_namespaces)) + .collect(); + + quote! { + #next_namespace + + const #type_ident #ident = { + #functions + #namespaces + }; + } +} +fn function_to_struct_init(function: &Function, namespaces: &[&Identifier]) -> TokenStream2 { + let ident = identifier_to_rust(&function.identifier); + let full_ident = mangle_c_function_ident(function, namespaces); + + quote! { + . #ident = #full_ident, + } +} + +fn namespace_to_struct_init(namespace: &Namespace) -> TokenStream2 { + let ident = identifier_to_rust(&namespace.name); + + quote! { + . #ident = #ident , + } +} + +fn namespace_to_full_typedef(nasp: &Namespace) -> String { + let ident = format_ident!("{}_t", nasp.name.name); + let doc_comments = nasp + .attributes + .iter() + .map(attribute_to_doc_comment) + .collect::(); + + let functions: TokenStream2 = nasp + .functions + .iter() + .map(|r#fn| function_to_typedef(r#fn)) + .collect(); + let namespaces: TokenStream2 = nasp + .namespaces + .iter() + .map(|nasp| namespace_to_typedef(nasp)) + .collect(); + let next_namespace: String = nasp + .namespaces + .iter() + .map(|nasp| namespace_to_full_typedef(nasp)) + .collect::>() + .join("\n"); + + let namespace = quote! { + typedef struct { + #functions + #namespaces + } #ident; + }; + format! {"{}\n{}{}\n", next_namespace, doc_comments, namespace} +} + +fn function_to_typedef(function: &Function) -> TokenStream2 { + let ident = identifier_to_rust(&function.identifier); + + let output = if let Some(output) = &function.output { + let output = output.to_string(); + quote! { #output, } + } else { + TokenStream2::default() + }; + + let inputs: Vec = if function.inputs.is_empty() { + vec![quote! { void }] + } else { + todo!() + }; + + quote! { + int (* #ident ) (#output #(#inputs),*); + } +} + +fn namespace_to_typedef(namespace: &Namespace) -> TokenStream2 { + let ident = identifier_to_rust(&namespace.name); + let type_ident = format_ident!("{}_t", ident.to_string()); + + quote! { + #type_ident #ident ; + } +} + +fn namespace_to_header(nasp: &Namespace, namespaces: &Vec<&Identifier>) -> String { + let mut nasps = namespaces.clone(); + nasps.push(&nasp.name); + + let functions: String = nasp + .functions + .iter() + .map(|r#fn| function_to_header(r#fn, &nasps)) + .collect::>() + .join("\n"); + let namespaces: String = nasp + .namespaces + .iter() + .map(|nasp| namespace_to_header(nasp, &nasps)) + .collect(); + + format! {"{}\n{}", functions, namespaces} +} diff --git a/trixy-macros/src/generate/c_api/host.rs b/trixy-macros/src/generate/c_api/host.rs new file mode 100644 index 0000000..0e8c617 --- /dev/null +++ b/trixy-macros/src/generate/c_api/host.rs @@ -0,0 +1,291 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use trixy_lang_parser::command_spec::{CommandSpec, Function, Identifier, NamedType, Namespace}; + +use crate::{ + config::TrixyConfig, + generate::{ + c_api::mangle_c_function_ident, identifier_to_rust, named_type_to_rust, type_to_rust, + }, +}; + +/// This function generates the main c API provided by Trixy. +/// This works for example like this: +/// Turning this: +/// ```text +/// nasp trinitrix { +/// struct Callback { +/// func: String, +/// timeout: String, +/// }; +/// +/// enum CallbackPriority { +/// High, +/// Medium, +/// Low, +/// }; +/// +/// fn execute_callback(callback: Callback, priority: CallbackPriority); +/// } +/// ``` +/// to this: +/// ```no_run +/// pub extern "C" fn exectute_callback(callback: Callback, priority: CallbackPriority) { +/// /* Here we simply call your handler function, with the command of the function */ +/// } +/// ``` +pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 { + let functions: TokenStream2 = trixy + .functions + .iter() + .map(|r#fn| function_to_c(r#fn, &config, &vec![])) + .collect(); + let namespaced_functions: TokenStream2 = trixy + .namespaces + .iter() + .map(|nasp| namespace_to_c(&nasp, &config, &vec![])) + .collect(); + quote! { + #functions + #namespaced_functions + } +} + +fn namespace_to_c( + namespace: &Namespace, + config: &TrixyConfig, + namespaces: &Vec<&Identifier>, +) -> TokenStream2 { + let mut namespaces = namespaces.clone(); + namespaces.push(&namespace.name); + + let functions: TokenStream2 = namespace + .functions + .iter() + .map(|r#fn| function_to_c(&r#fn, &config, &namespaces)) + .collect(); + let additional_functions: TokenStream2 = namespace + .namespaces + .iter() + .map(|nasp| namespace_to_c(&nasp, &config, &namespaces)) + .collect(); + quote! { + #functions + #additional_functions + } +} + +fn function_to_c( + function: &Function, + config: &TrixyConfig, + namespaces: &Vec<&Identifier>, +) -> TokenStream2 { + let ident = mangle_c_function_ident(function, namespaces); + let inputs: Vec = function + .inputs + .iter() + .map(named_type_to_rust_trixy) + .collect(); + + let callback_function = format_ident!("{}", config.callback_function); + + let command_value: TokenStream2 = function_path_to_rust(&namespaces, &function); + + if let Some(r#type) = &function.output { + let output = type_to_rust(&r#type); + quote! { + #[no_mangle] + pub unsafe extern "C" fn #ident(output: *mut #output, #(#inputs),*) -> core::ffi::c_int { + #callback_function ! (#command_value) + } + } + } else { + quote! { + #[no_mangle] + pub extern "C" fn #ident(#(#inputs),*) -> core::ffi::c_int { + #callback_function ! (#command_value); + return 1; + } + } + } +} + +fn named_type_to_rust_trixy(named_type: &NamedType) -> TokenStream2 { + let ident = identifier_to_rust(&named_type.name); + let type_ident = type_to_rust(&named_type.r#type); + quote! { + #ident : trixy:: #type_ident + } +} + +/// Turns a function in namespaces to the generated host enum path: +/// *trixy code:* +/// ```text +/// fn fn_alone(); +/// nasp one { +/// fn fn_one(); +/// nasp two { +/// fn fn_two(input: String); +/// } +/// } +/// ``` +/// *rust enum path for fn_alone:* +/// ```no_run +/// Commands::fn_alone +/// ``` +/// *rust enum path for fn_one:* +/// ```no_run +/// // `Commands` is just the name for the top-level namespace +/// Commands::One(one::One( +/// One::fn_one +/// )) +/// ``` +/// *rust enum path for fn_two:* +/// ```no_run +/// Commands::One(one::One( +/// one::two::Two(one::two::Two( +/// Two::fn_two {input: String} +/// )))) +/// ``` +fn function_path_to_rust(namespaces: &Vec<&Identifier>, function: &Function) -> TokenStream2 { + let function_ident = { + let ident = format_ident!("{}", function.identifier.name); + if function.inputs.is_empty() { + quote! { + #ident + } + } else { + let inputs: Vec = function + .inputs + .iter() + .map(named_type_to_rust_assignment) + .collect(); + quote! { + #ident { #(#inputs),* } + } + } + }; + if namespaces.is_empty() { + quote! { + Commands:: #function_ident + } + } else { + let nasp_pascal_ident = format_ident!( + "{}", + namespaces + .last() + .expect("We checked") + .name + .to_case(Case::Pascal) + ); + + let namespace_path = nasps_to_path(namespaces); + + let function_call = if !function.inputs.is_empty() { + let inputs: Vec = + function.inputs.iter().map(named_type_to_rust).collect(); + quote! { + #namespace_path :: #nasp_pascal_ident :: #function_ident { #(#inputs),* } + } + } else { + quote! { + #namespace_path :: #nasp_pascal_ident :: #function_ident + } + }; + + let output: TokenStream2 = namespaces + .iter() + .enumerate() + .rev() + .fold(function_call, |acc, (index, nasp)| { + nasp_path_one_part(nasp, &acc, &namespaces, index) + }); + + output + } +} + +fn named_type_to_rust_assignment(named_type: &NamedType) -> TokenStream2 { + let ident = identifier_to_rust(&named_type.name); + quote! { + #ident : trixy::convert!(#ident) + } +} + +/// This function add a namespace component to the [input] value like so: +/// (taking the example from the [function_path_to_rust] function) +/// ```text +/// one::two::Two::fn_two [= ] +/// -> +/// one::One::Two() [= ] +/// -> +/// Commands::One() [= ] +/// ``` +fn nasp_path_one_part( + current_nasp: &Identifier, + input: &TokenStream2, + namespaces: &Vec<&Identifier>, + index: usize, +) -> TokenStream2 { + let namespaces_to_do = &namespaces[..index]; + + let ident_pascal = format_ident!("{}", current_nasp.name.to_case(Case::Pascal)); + + if index == 0 { + quote! { + Commands :: #ident_pascal ( #input ) + } + } else { + let ident_pascal_next = format_ident!( + "{}", + namespaces_to_do + .last() + .expect("We checked the index") + .name + .to_case(Case::Pascal) + ); + let namespace_path = nasps_to_path(namespaces_to_do); + quote! { + #namespace_path :: #ident_pascal_next :: #ident_pascal ( #input ) + } + } +} + +fn nasps_to_path(namespaces: &[&Identifier]) -> TokenStream2 { + namespaces + .iter() + .fold(TokenStream2::default(), |acc, nasp| { + let ident = format_ident!("{}", nasp.name); + if acc.is_empty() { + quote! { + #ident + } + } else { + quote! { + #acc :: #ident + } + } + }) +} diff --git a/trixy-macros/src/generate/c_api/mod.rs b/trixy-macros/src/generate/c_api/mod.rs new file mode 100644 index 0000000..3e25faf --- /dev/null +++ b/trixy-macros/src/generate/c_api/mod.rs @@ -0,0 +1,43 @@ +/* +* Copyright (C) 2023 The Trinitrix Project +* +* This file is part of the Trixy crate for Trinitrix. +* +* Trixy is free software: you can redistribute it and/or modify +* it under the terms of the Lesser GNU General Public License as +* published by the Free Software Foundation, either version 3 of +* the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* and the Lesser GNU General Public License along with this program. +* If not, see . +*/ + + +use proc_macro2::Ident; +use quote::format_ident; +use trixy_lang_parser::command_spec::{Function, Identifier}; + +pub mod header; +pub mod host; + +pub fn mangle_c_function_ident(function: &Function, namespaces: &[&Identifier]) -> Ident { + let namespace_str = namespaces.iter().fold(String::default(), |acc, nasp| { + if acc.is_empty() { + nasp.name.clone() + } else { + format!("{}_{}", acc, nasp.name) + } + }); + + if namespace_str.is_empty() { + format_ident!("{}", &function.identifier.name) + } else { + format_ident!("{}_{}", namespace_str, &function.identifier.name) + } +} diff --git a/trixy-macros/src/generate/host/mod.rs b/trixy-macros/src/generate/host/mod.rs index 82dd30a..f3f04c2 100644 --- a/trixy-macros/src/generate/host/mod.rs +++ b/trixy-macros/src/generate/host/mod.rs @@ -18,6 +18,9 @@ * If not, see . */ + + + //! This module is responsible for generating the rust code used to interface with the api. //! That includes the structs and enums declared in the trixy file and the enum used to describe the //! command being executed. @@ -28,20 +31,23 @@ use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use trixy_lang_parser::command_spec::{ - Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, - NamedType, Namespace, Structure, Type, + Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Namespace, + Structure, }; -use crate::config::TrixyConfig; +use crate::{ + config::TrixyConfig, + generate::{identifier_to_rust, named_type_to_rust}, +}; thread_local! {static DEBUG: OnceCell = OnceCell::new();} /// This function turns, for example, the following trixy input into this rust code: -/// ```rust +/// ```text /// nasp trinitrix { /// struct Callback { -/// func: Function, -/// timeout: Integer, +/// func: String, +/// timeout: String, /// }; /// /// enum CallbackPriority { @@ -52,20 +58,22 @@ thread_local! {static DEBUG: OnceCell = OnceCell::new();} /// /// fn execute_callback(callback: Callback, priority: CallbackPriority); /// } +/// // wrong but helps with syntax highlight: +/// // vim: syntax=rust /// ``` -/// ```rust +/// ```no_run /// #[derive(Debug)] /// pub enum Commands { /// Trinitrix(trinitrix::Trinitrix), /// } /// pub mod trinitrix { -/// #[allow(non_camel_case)] +/// #[allow(non_camel_case_types)] /// #[derive(Debug)] /// struct Callback { -/// func: Function, -/// timeout: Integer, +/// func: String, +/// timeout: String, /// } -/// #[allow(non_camel_case)] +/// #[allow(non_camel_case_types)] /// #[derive(Debug)] /// enum CallbackPriority { /// High, @@ -74,14 +82,14 @@ thread_local! {static DEBUG: OnceCell = OnceCell::new();} /// } /// #[derive(Debug)] /// pub enum Trinitrix { -/// #[allow(non_camel_case)] +/// #[allow(non_camel_case_types)] /// execute_callback { callback: Callback, priority: CallbackPriority }, /// } /// } /// ``` pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 { DEBUG.with(|d| { - d.set(if config.generate_debug() { + d.set(if config.generate_debug { quote! { #[derive(Debug)] } @@ -190,12 +198,21 @@ fn function_to_rust(function: &Function) -> TokenStream2 { .map(attribute_to_doc_comment) .collect(); let ident = identifier_to_rust(&function.identifier); + let inputs: Vec = function.inputs.iter().map(named_type_to_rust).collect(); - quote! { - #doc_comments - #[allow(non_camel_case_types)] - #ident {#(#inputs),*} + if inputs.is_empty() { + quote! { + #doc_comments + #[allow(non_camel_case_types)] + #ident + } + } else { + quote! { + #doc_comments + #[allow(non_camel_case_types)] + #ident {#(#inputs),*} + } } } @@ -216,7 +233,7 @@ fn enumeration_to_rust(enumeration: &Enumeration) -> TokenStream2 { quote! { #doc_comments - #[allow(non_camel_case)] + #[allow(non_camel_case_types)] #debug enum #ident { #(#states),* @@ -241,7 +258,7 @@ fn structure_to_rust(structure: &Structure) -> TokenStream2 { quote! { #doc_comments - #[allow(non_camel_case)] + #[allow(non_camel_case_types)] #debug struct #ident { #(#contents),* @@ -263,13 +280,6 @@ fn doc_identifier_to_rust(doc_identifier: &DocIdentifier) -> TokenStream2 { } } -fn identifier_to_rust(identifier: &Identifier) -> TokenStream2 { - let ident = format_ident!("{}", &identifier.name); - quote! { - #ident - } -} - fn doc_named_type_to_rust(doc_named_type: &DocNamedType) -> TokenStream2 { let doc_comments: TokenStream2 = doc_named_type .attributes @@ -282,19 +292,3 @@ fn doc_named_type_to_rust(doc_named_type: &DocNamedType) -> TokenStream2 { #named_type } } - -fn named_type_to_rust(named_type: &NamedType) -> TokenStream2 { - let ident = identifier_to_rust(&named_type.name); - let r#type = type_to_rust(&named_type.r#type); - quote! { - #ident : #r#type - } -} - -fn type_to_rust(r#type: &Type) -> TokenStream2 { - let ident = identifier_to_rust(&r#type.identifier); - let generics: Vec = r#type.generic_args.iter().map(type_to_rust).collect(); - quote! { - #ident <#(#generics),*> - } -} diff --git a/trixy-macros/src/generate/mod.rs b/trixy-macros/src/generate/mod.rs index 7d134df..1cb91ef 100644 --- a/trixy-macros/src/generate/mod.rs +++ b/trixy-macros/src/generate/mod.rs @@ -18,4 +18,55 @@ * If not, see . */ + + + +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use trixy_lang_parser::command_spec::{CommandSpec, Identifier, NamedType, Type}; + +use crate::config::TrixyConfig; + +pub mod c_api; pub mod host; + +pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 { + // Build the language wrappers + let c_host_api = c_api::host::generate(&trixy, &config); + + // Build the final rust hosting code + let host_rust_code = host::generate(&trixy, &config); + + quote! { + #host_rust_code + /* C api */ + #c_host_api + } +} + +fn identifier_to_rust(identifier: &Identifier) -> TokenStream2 { + let ident = format_ident!("{}", &identifier.name); + quote! { + #ident + } +} +fn named_type_to_rust(named_type: &NamedType) -> TokenStream2 { + let ident = identifier_to_rust(&named_type.name); + let r#type = type_to_rust(&named_type.r#type); + quote! { + #ident : #r#type + } +} +fn type_to_rust(r#type: &Type) -> TokenStream2 { + let ident = identifier_to_rust(&r#type.identifier); + if r#type.generic_args.is_empty() { + quote! { + #ident + } + } else { + let generics: Vec = r#type.generic_args.iter().map(type_to_rust).collect(); + quote! { + #ident <#(#generics),*> + } + } +} diff --git a/trixy-macros/src/lib.rs b/trixy-macros/src/lib.rs index 91124e4..8468d66 100644 --- a/trixy-macros/src/lib.rs +++ b/trixy-macros/src/lib.rs @@ -18,39 +18,91 @@ * If not, see . */ -use std::fs; -use proc_macro::TokenStream; -use quote::quote; -use syn::parse_macro_input; + + +use std::{env, fs, io::Write, path::PathBuf, process::Command}; + use trixy_lang_parser::parse_trixy_lang; use crate::config::TrixyConfig; -mod config; +pub mod config; mod generate; -/// This is the heart of Trixy -/// It mainly does one thing: -/// - Generate a tree of modules from the input trixy file -/// -#[proc_macro] -pub fn trixy_generate(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as TrixyConfig); +const VIM_LINE_RUST: &'static str = "// vim: filetype=rust\n"; +const VIM_LINE_C: &'static str = "// vim: filetype=c\n"; - let source_code = fs::read_to_string(input.get_path()).unwrap_or_else(|err| { - panic! {"Can't read file at path: '{}'. The Error is: '{}'", input.get_path().display(), err}; - }); +impl TrixyConfig { + /// This is the heart of Trixy + /// It mainly does one thing: + /// - Generate a tree of modules from the input trixy file + /// + pub fn generate(&self) { + let source_code = fs::read_to_string(&self.trixy_path).unwrap_or_else(|err| { + panic! {"Can't read file at path: '{}'. The Error is: '{}'", + self.trixy_path.display(), err}; + }); - let trixy_code = parse_trixy_lang(&source_code).unwrap_or_else(|err| { - panic! {"Parsing of the trixy file failed: \n{}", err} - }); + let trixy_code = parse_trixy_lang(&source_code).unwrap_or_else(|err| { + panic! {"Parsing of the trixy file failed: \n{}", err} + }); - // Build the final rust hosting code - let host_rust_code = generate::host::generate(&trixy_code, &input); + // host code + let host_code = prettyplease::unparse( + &syn::parse2(generate::generate(&trixy_code, &self)) + .expect("This code was generated, it should also be parsable"), + ); + let mut host_code_out = fs::File::create(PathBuf::from(format!( + "{}/{}", + env::var("OUT_DIR").expect("The build script should have this define"), + &self.host_code_name.display() + ))) + .expect("This file should always be free to use"); + write!(host_code_out, "{}\n{}", host_code, VIM_LINE_RUST).expect("Write should work"); - let output = quote! { - #host_rust_code - }; - output.into() + // c header + let c_header = generate::c_api::header::generate(&trixy_code, &self); + let c_header_path = PathBuf::from(format!( + "{}/{}", + env::var("OUT_DIR").expect("The build script should have this define"), + &self.c_header_name.display() + )); + let mut c_header_out = + fs::File::create(&c_header_path).expect("This file should always be free to use"); + write!(c_header_out, "{}\n{}", c_header, VIM_LINE_C).expect("Write should work"); + + Command::new("clang-format") + .args(["-i", &c_header_path.to_str().unwrap()]) + .status() + .unwrap_or_else(|err| { + panic!( + "Failed to format the c header file with `clang-format`; Error: `{}`", + err + ) + }); + + if let Some(dist_dir) = &self.dist_dir_path { + if !dist_dir.is_dir() { + fs::create_dir(dist_dir).unwrap_or_else(|err| { + panic! { + "Failed to create the dist directory ('{}') because of: `{}`", + dist_dir.display(), err} + }); + } + if self.check_dist_dir { + if dist_dir.read_dir().iter().count() != 1 { + panic!("Your specified dist dir already has something in it! Set `check_dist_dir` to `false` to override this check"); + } + } + let c_header_dist = PathBuf::from(format!( + "{}/{}", + dist_dir.display(), + self.c_header_name.display() + )); + fs::copy(c_header_path, c_header_dist).unwrap_or_else( + |err| panic! {"Failed to copy the c header to the dist dir because of: `{}`", err}, + ); + } + } }