feat(trixy-macros): Add rust host code generation

This commit is contained in:
Benedikt Peetz 2023-12-24 11:28:30 +01:00
parent 1d49bdf2cf
commit ed96a50bd4
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
29 changed files with 655 additions and 164 deletions

View File

@ -30,3 +30,5 @@ convert_case = "0.6.0"
proc-macro2 = "1.0.70" proc-macro2 = "1.0.70"
quote = "1.0.33" quote = "1.0.33"
syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] } syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] }
trixy-lang_parser = { path = "./trixy-lang_parser" }
trixy-macros = { path = "./trixy-macros" }

View File

@ -1,118 +0,0 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use command_enum_parsing::DataCommandEnum;
use config::TrixyConfig;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::parse_macro_input;
use crate::trixy_lang::parse_trixy_lang;
mod command_enum_parsing;
mod config;
mod generate;
mod trixy_lang;
/// This is the heart of the command api
/// It mainly does two things:
/// - Generate a command enum
/// - Wrap the enum in all supported languages (only lua for now)
/// - Generate wrapper lua function for each command
/// - Generate a `add_lua_functions_to_globals` function, which adds
/// the rust wrapper functions to the lua globals.
///
/// The input and output values of the wrapped functions are derived from the values specified in
/// the input to the `parse_command_enum` proc macro.
///
/// For example this rust code:
/// ```no_run
/// parse_command_enum! {
/// /// Greets the user
/// greet: fn(String) -> String,
/// }
/// ```
/// results in this expanded code:
/// ```no_run
/// #[derive(Debug)]
/// pub enum Command {
/// Greet(String),
/// }
/// pub fn add_lua_functions_to_globals(
/// lua: mlua::Lua,
/// tx: tokio::sync::mpsc::Sender<Event>,
/// ) -> mlua::Lua {
/// lua.set_app_data(tx);
/// let globals = lua.globals();
/// {
/// let wrapped_lua_function_greet = lua
/// .create_async_function(greet)
/// .expect(
/// format!(
/// "The function: `{}` should be defined",
/// "greet",
/// )
/// );
/// globals
/// .set("greet", wrapped_lua_function_greet)
/// .expect("Setting a static global value should work");
/// }
/// drop(globals);
/// lua
/// }
/// async fn greet(lua: &mlua::Lua, input: String) -> Result<String, mlua::Error> {
/// let (callback_tx, callback_rx) = tokio::sync::oneshot::channel::<String>();
/// let tx: core::cell::Ref<tokio::sync::mpsc::Sender<Event>> = lua
/// .app_data_ref()
/// .expect("This should exist, it was set before");
/// (*tx)
/// .send(Event::CommandEvent(Command::Greet(input.clone()), Some(callback_tx)))
/// .await
/// .expect("This should work, as the receiver is not dropped");
/// match callback_rx.await {
/// Ok(output) => {
/// return Ok(output);
/// }
/// Err(err) => {
/// return Err(mlua::Error::ExternalError(std::sync::Arc::new(err)));
/// }
/// };
/// }
/// ```
#[proc_macro]
pub fn trixy_generate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as TrixyConfig);
let trixy_code = parse_trixy_lang(input.get_path());
todo!()
// // Build the language wrappers
// let lua_wrapper: TokenStream2 = generate::lua_wrapper(&input);
//
// // Build the final enum
// let command_enum = generate::command_enum(&input);
// let output = quote! {
// #command_enum
// #lua_wrapper
// };
// output.into()
}

View File

@ -26,5 +26,6 @@ edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"] }
convert_case = "0.6.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
thiserror = "1.0.50" thiserror = "1.0.50"

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
nasp trinitrix {
/// This enum can't be called Trinitrix, as that's already the name of the namespace
/// (if it's in Pascal case)
enum Trinitrix {
High,
Medium,
Low,
};
fn execute_callback(priority: Trinitrix);
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
/// other doc comment
fn hi(name: String) -> String;
/// struct comment
struct ho {
ident: String,
/// also a doc comment
codebase: Vec<String>,
};
/// Some doc comment
nasp trinitrix {
fn hi(name: String) -> String;
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -18,7 +18,6 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module contains the already type checked types. //! This module contains the already type checked types.
use std::fmt::Display; use std::fmt::Display;
@ -91,7 +90,7 @@ pub struct Function {
pub attributes: Vec<Attribute>, pub attributes: Vec<Attribute>,
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct Type { pub struct Type {
pub identifier: Identifier, pub identifier: Identifier,
pub generic_args: Vec<Type>, pub generic_args: Vec<Type>,
@ -110,6 +109,12 @@ pub struct DocNamedType {
pub attributes: Vec<Attribute>, pub attributes: Vec<Attribute>,
} }
impl From<&DocNamedType> for NamedType {
fn from(value: &DocNamedType) -> Self {
Self { name: value.name.to_owned(), r#type: value.r#type.to_owned() }
}
}
impl From<TokenKind> for Identifier { impl From<TokenKind> for Identifier {
fn from(value: TokenKind) -> Self { fn from(value: TokenKind) -> Self {
match value { match value {
@ -156,6 +161,14 @@ pub struct DocIdentifier {
pub attributes: Vec<Attribute>, pub attributes: Vec<Attribute>,
} }
impl From<&DocIdentifier> for Identifier {
fn from(value: &DocIdentifier) -> Self {
Self {
name: value.name.to_owned(),
}
}
}
/// A const version of [Identifier] /// A const version of [Identifier]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConstIdentifier { pub struct ConstIdentifier {

View File

@ -21,3 +21,5 @@
pub mod checked; pub mod checked;
pub mod unchecked; pub mod unchecked;
pub use checked::*;

View File

@ -18,22 +18,25 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use core::fmt; use core::fmt;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
lexing::{error::SpannedLexingError, TokenSpan}, lexing::{error::SpannedLexingError, TokenSpan},
parsing::checked::error::SpannedParsingError, parsing::{self},
}; };
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum TrixyError { pub enum TrixyError {
#[error(transparent)] #[error(transparent)]
Lexing(#[from] SpannedLexingError), Lexing(#[from] SpannedLexingError),
#[error(transparent)] #[error(transparent)]
Parsing(#[from] SpannedParsingError), Parsing(#[from] parsing::unchecked::error::SpannedParsingError),
#[error(transparent)]
Processing(#[from] parsing::checked::error::SpannedParsingError),
} }
/// The context of an Error. /// The context of an Error.

View File

@ -18,22 +18,23 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use error::TrixyError; use error::TrixyError;
use crate::lexing::TokenStream; use crate::lexing::TokenStream;
use self::command_spec::checked::CommandSpec; use self::command_spec::checked::CommandSpec;
mod command_spec; pub mod command_spec;
pub mod error; pub mod error;
pub mod lexing; pub mod lexing;
pub mod parsing; pub mod parsing;
pub fn parse_trixy_lang(input: &str) -> Result<CommandSpec, Box<TrixyError>> { pub fn parse_trixy_lang(input: &str) -> Result<CommandSpec, Box<TrixyError>> {
let input_tokens = TokenStream::lex(input) let input_tokens = TokenStream::lex(input)
.map_err(|err| Box::new(err.into()))? .map_err(Into::<TrixyError>::into)?
.parse() .parse_unchecked()
.map_err(Into::<TrixyError>::into)?
.process(input.to_owned())
.map_err(Into::<TrixyError>::into)?; .map_err(Into::<TrixyError>::into)?;
Ok(input_tokens) Ok(input_tokens)
} }

View File

@ -18,10 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{fs, process::exit}; use std::{fs, process::exit};
use trixy_lang_parser::lexing::TokenStream; use trixy_lang_parser::{lexing::TokenStream, parse_trixy_lang};
use std::path::PathBuf; use std::path::PathBuf;
@ -56,6 +55,13 @@ pub enum Command {
/// The file containing the trixy code to process /// The file containing the trixy code to process
file: PathBuf, file: PathBuf,
}, },
/// Act on the file as the library would do it
Library {
#[clap(value_parser)]
/// The file containing the trixy code to process
file: PathBuf,
},
} }
pub fn main() { pub fn main() {
@ -127,5 +133,13 @@ pub fn main() {
}; };
println!("{:#?}", processed); println!("{:#?}", processed);
} }
Command::Library { file } => {
let input = fs::read_to_string(file).unwrap();
let parsed = parse_trixy_lang(&input).unwrap_or_else(|err| {
eprintln!("{}", err);
exit(1)
});
println!("{:#?}", parsed);
}
} }
} }

View File

@ -18,7 +18,6 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use thiserror::Error; use thiserror::Error;
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};
@ -36,6 +35,18 @@ pub enum ParsingError {
TypeNotDeclared { r#type: Identifier, span: TokenSpan }, TypeNotDeclared { r#type: Identifier, span: TokenSpan },
#[error(transparent)] #[error(transparent)]
PreParseError(#[from] OldSpannedParsingError), PreParseError(#[from] OldSpannedParsingError),
#[error("The enum ('{name}') has the same name as it's namespace")]
EnumWithNamespaceName {
name: Identifier,
enum_span: TokenSpan,
namespace_span: TokenSpan,
},
#[error("The enum ('{name}') has the same name as it's namespace if it's in Pascal case")]
EnumWithNamespaceNamePascal {
name: Identifier,
enum_span: TokenSpan,
namespace_span: TokenSpan,
},
} }
impl ParsingError { impl ParsingError {
@ -43,6 +54,8 @@ impl ParsingError {
match self { match self {
ParsingError::TypeNotDeclared { span, .. } => span, ParsingError::TypeNotDeclared { span, .. } => span,
ParsingError::PreParseError(err) => err.source.span(), ParsingError::PreParseError(err) => err.source.span(),
ParsingError::EnumWithNamespaceName { enum_span, .. } => enum_span,
ParsingError::EnumWithNamespaceNamePascal { enum_span, .. } => enum_span,
} }
} }
} }
@ -52,6 +65,8 @@ impl AdditionalHelp for ParsingError {
match self { match self {
ParsingError::TypeNotDeclared { .. } => "This type should have been mentioned in the namespaces above, or in the namespace of this type usage".to_owned(), ParsingError::TypeNotDeclared { .. } => "This type should have been mentioned in the namespaces above, or in the namespace of this type usage".to_owned(),
ParsingError::PreParseError(err) => ErrorContextDisplay::source(err).additional_help(), ParsingError::PreParseError(err) => ErrorContextDisplay::source(err).additional_help(),
ParsingError::EnumWithNamespaceNamePascal {..}
| ParsingError::EnumWithNamespaceName {..} => "Change the name of this Enumeration as the generation process in trixy-macros needs to use this name".to_owned(),
} }
} }
} }

View File

@ -18,9 +18,10 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::mem; use std::mem;
use convert_case::{Case, Casing};
use crate::{ use crate::{
command_spec::{ command_spec::{
checked::{ checked::{
@ -35,7 +36,7 @@ use crate::{
}, },
}, },
error::ErrorContext, error::ErrorContext,
lexing::{TokenKind, TokenStream}, lexing::{TokenKind, TokenSpan},
}; };
use self::error::{ParsingError, SpannedParsingError}; use self::error::{ParsingError, SpannedParsingError};
@ -51,29 +52,6 @@ struct Parser {
original_file: String, original_file: String,
} }
impl TokenStream {
pub fn parse(mut self) -> Result<CommandSpec, SpannedParsingError> {
let original_file = mem::take(&mut self.original_file);
let unchecked = self.parse_unchecked().map_err(|err| {
let span = *err.source.span();
SpannedParsingError {
source: Box::new(ParsingError::from(err)),
context: ErrorContext::from_span(span, &original_file),
}
})?;
let checked = Parser {
command_spec: unchecked,
structures: vec![],
enumerations: vec![],
original_file,
}
.parse()?;
Ok(checked)
}
}
impl UncheckedCommandSpec { impl UncheckedCommandSpec {
pub fn process(self, original_file: String) -> Result<CommandSpec, SpannedParsingError> { pub fn process(self, original_file: String) -> Result<CommandSpec, SpannedParsingError> {
let checked = Parser { let checked = Parser {
@ -111,6 +89,7 @@ impl Parser {
&mut self, &mut self,
namespace: UncheckedNamespace, namespace: UncheckedNamespace,
) -> Result<Namespace, ParsingError> { ) -> Result<Namespace, ParsingError> {
let namespace_span = namespace.name.span;
let name = match namespace.name.kind { let name = match namespace.name.kind {
TokenKind::Identifier(ident) => Identifier { name: ident }, TokenKind::Identifier(ident) => Identifier { name: ident },
// This is not really used, so the value put here does not matter // This is not really used, so the value put here does not matter
@ -123,7 +102,7 @@ impl Parser {
let mut enumerations = vec![]; let mut enumerations = vec![];
let mut enumerations_counter = 0; let mut enumerations_counter = 0;
for enumeration in namespace.enumerations { for enumeration in namespace.enumerations {
enumerations.push(self.process_enumeration(enumeration)?); enumerations.push(self.process_enumeration(enumeration, &name, namespace_span)?);
enumerations_counter += 1; enumerations_counter += 1;
} }
let mut structures = vec![]; let mut structures = vec![];
@ -186,10 +165,27 @@ impl Parser {
fn process_enumeration( fn process_enumeration(
&mut self, &mut self,
mut enumeration: UncheckedEnumeration, mut enumeration: UncheckedEnumeration,
namespace_name: &Identifier,
namespace_span: TokenSpan,
) -> Result<Enumeration, ParsingError> { ) -> Result<Enumeration, ParsingError> {
self.enumerations.push(enumeration.clone()); self.enumerations.push(enumeration.clone());
let identifier = mem::take(&mut enumeration.identifier.kind).into(); let enum_span = enumeration.identifier.span;
let identifier: Identifier = mem::take(&mut enumeration.identifier.kind).into();
if &identifier == namespace_name {
return Err(ParsingError::EnumWithNamespaceName {
name: identifier.clone(),
enum_span,
namespace_span,
});
}
if identifier.name == namespace_name.name.to_case(Case::Pascal) {
return Err(ParsingError::EnumWithNamespaceNamePascal {
name: identifier.clone(),
enum_span,
namespace_span,
});
}
let mut states = vec![]; let mut states = vec![];
for mut state in enumeration.states { for mut state in enumeration.states {

View File

@ -20,4 +20,4 @@
pub mod checked; pub mod checked;
mod unchecked; pub mod unchecked;

View File

@ -254,7 +254,7 @@ impl Parser {
} }
while self.expect_peek(token![Comma]) { while self.expect_peek(token![Comma]) {
self.expect(token![Comma])?; self.expect(token![Comma])?;
if self.expect_peek(token![Ident]) { if self.expect_peek(token![Ident]) || self.expect_peek(token![DocComment]) {
contents.push(self.parse_doc_named_type()?); contents.push(self.parse_doc_named_type()?);
} else { } else {
break; break;

6
trixy-macros/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# build
/target
/result
# This crate is a library
Cargo.lock

33
trixy-macros/Cargo.toml Normal file
View File

@ -0,0 +1,33 @@
# Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
#
# 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 <https://www.gnu.org/licenses/>.
[package]
name = "trixy-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
convert_case = "0.6.0"
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" }

View File

@ -0,0 +1 @@
api_correct.tri

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
nasp trinitrix {
struct Callback {
func: String,
timeout: String,
};
enum CallbackPriority {
High,
Medium,
Low,
};
fn execute_callback(callback: Callback, priority: CallbackPriority);
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
fn print(message: CommandTransferValue);
nasp trinitrix { {}
fn hi honner(name: String) -> String; ;
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -0,0 +1,9 @@
use trixy_macros::trixy_generate;
fn main() {
trixy_generate! {
path: "./examples/main/api.tri"
languages: rust, lua, c
generate_debug: false
}
}

View File

@ -18,24 +18,25 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module is responsible for parsing the config passed to the macro call: //! This module is responsible for parsing the config passed to the macro call:
//! For example: //! For example:
//! ```no_run //! ```no_run
//! trixy_generate! { //! trixy_generate! {
//! path: ./trintrix_command_interface.tri //! path: "./path/to/trixy/file.tri"
//! languages: rust, lua, c //! languages: rust, lua, c
//! generate_debug: false
//! } //! }
//! ``` //! ```
use std::path::PathBuf; use std::path::PathBuf;
use proc_macro2::Ident; use proc_macro2::Ident;
use syn::{parse::Parse, punctuated::Punctuated, LitStr, Result, Token}; use syn::{parse::Parse, punctuated::Punctuated, LitBool, LitStr, Result, Token};
mod kw { mod kw {
syn::custom_keyword!(path); syn::custom_keyword!(path);
syn::custom_keyword!(languages); syn::custom_keyword!(languages);
syn::custom_keyword!(generate_debug);
} }
#[derive(Debug)] #[derive(Debug)]
@ -63,6 +64,15 @@ struct Path {
raw: PathBuf, raw: PathBuf,
} }
#[derive(Debug)]
struct GenerateDebug {
#[allow(dead_code)]
generate_debug: kw::generate_debug,
#[allow(dead_code)]
colon: Token![:],
raw: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub struct TrixyConfig { pub struct TrixyConfig {
/// The Path to the base command interface config file /// The Path to the base command interface config file
@ -70,10 +80,17 @@ pub struct TrixyConfig {
/// The languages the commands should be exposed in /// The languages the commands should be exposed in
languages: Languages, languages: Languages,
/// Should the macro generate Debug trait implementation for each enum
/// These are very useful but completely obscure the `cargo expand` ouput
generate_debug: GenerateDebug,
} }
impl TrixyConfig { impl TrixyConfig {
pub fn get_path(&self) -> PathBuf { pub fn get_path(&self) -> &std::path::Path {
self.path.raw &self.path.raw
}
pub fn generate_debug(&self) -> bool {
self.generate_debug.raw
} }
} }
@ -82,6 +99,20 @@ impl Parse for TrixyConfig {
Ok(Self { Ok(Self {
path: input.parse()?, path: input.parse()?,
languages: input.parse()?, languages: input.parse()?,
generate_debug: input.parse()?,
})
}
}
impl Parse for GenerateDebug {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
let kw: kw::generate_debug = input.parse()?;
let colon: Token![:] = input.parse()?;
let raw = input.parse::<LitBool>()?.value();
Ok(Self {
generate_debug: kw,
colon,
raw,
}) })
} }
} }

View File

@ -0,0 +1,280 @@
//! 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.
use std::cell::OnceCell;
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,
};
use crate::config::TrixyConfig;
thread_local! {static DEBUG: OnceCell<TokenStream2> = OnceCell::new();}
/// This function turns, for example, the following trixy input into this rust code:
/// ```rust
/// nasp trinitrix {
/// struct Callback {
/// func: Function,
/// timeout: Integer,
/// };
///
/// enum CallbackPriority {
/// High,
/// Medium,
/// Low,
/// };
///
/// fn execute_callback(callback: Callback, priority: CallbackPriority);
/// }
/// ```
/// ```rust
/// #[derive(Debug)]
/// pub enum Commands {
/// Trinitrix(trinitrix::Trinitrix),
/// }
/// pub mod trinitrix {
/// #[allow(non_camel_case)]
/// #[derive(Debug)]
/// struct Callback {
/// func: Function,
/// timeout: Integer,
/// }
/// #[allow(non_camel_case)]
/// #[derive(Debug)]
/// enum CallbackPriority {
/// High,
/// Medium,
/// Low,
/// }
/// #[derive(Debug)]
/// pub enum Trinitrix {
/// #[allow(non_camel_case)]
/// execute_callback { callback: Callback, priority: CallbackPriority },
/// }
/// }
/// ```
pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 {
DEBUG.with(|d| {
d.set(if config.generate_debug() {
quote! {
#[derive(Debug)]
}
} else {
TokenStream2::default()
})
.expect("The cell should always be empty at this point");
});
let modules: TokenStream2 = trixy.namespaces.iter().map(namespace_to_module).collect();
let structures: TokenStream2 = trixy.structures.iter().map(structure_to_rust).collect();
let enumerations: TokenStream2 = trixy.enumerations.iter().map(enumeration_to_rust).collect();
let functions: Vec<TokenStream2> = trixy.functions.iter().map(function_to_rust).collect();
let namespace_modules: Vec<TokenStream2> = trixy
.namespaces
.iter()
.map(namespace_to_module_enum)
.collect();
let debug = get_debug_sate();
quote! {
#structures
#enumerations
#debug
pub enum Commands {
#(#functions,)*
#(#namespace_modules),*
}
#modules
}
}
fn namespace_to_module(namespace: &Namespace) -> TokenStream2 {
let ident = identifier_to_rust(&namespace.name);
let enum_ident = format_ident!("{}", &namespace.name.name.to_case(Case::Pascal));
let doc_comments: TokenStream2 = namespace
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect();
let structures: TokenStream2 = namespace.structures.iter().map(structure_to_rust).collect();
let enumerations: TokenStream2 = namespace
.enumerations
.iter()
.map(enumeration_to_rust)
.collect();
let functions: Vec<TokenStream2> = namespace.functions.iter().map(function_to_rust).collect();
let namespace_modules: Vec<TokenStream2> = namespace
.namespaces
.iter()
.map(namespace_to_module_enum)
.collect();
let namespaces: TokenStream2 = namespace
.namespaces
.iter()
.map(namespace_to_module)
.collect();
let debug = get_debug_sate();
quote! {
#doc_comments
pub mod #ident {
#structures
#enumerations
#debug
pub enum #enum_ident {
#(#functions,)*
#(#namespace_modules),*
}
#namespaces
}
}
}
fn namespace_to_module_enum(namespace: &Namespace) -> TokenStream2 {
let pascal_ident = format_ident!("{}", namespace.name.name.to_case(Case::Pascal));
let ident = identifier_to_rust(&namespace.name);
quote! {
#pascal_ident(#ident :: #pascal_ident)
}
}
fn attribute_to_doc_comment(attribute: &Attribute) -> TokenStream2 {
let Attribute::doc(doc_comment) = attribute;
quote! {
#[doc = #doc_comment]
}
}
fn get_debug_sate() -> TokenStream2 {
let debug = DEBUG.with(|d| {
d.get()
.expect("The cell should contain something at this point")
.clone()
});
debug
}
fn function_to_rust(function: &Function) -> TokenStream2 {
let doc_comments: TokenStream2 = function
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect();
let ident = identifier_to_rust(&function.identifier);
let inputs: Vec<TokenStream2> = function.inputs.iter().map(named_type_to_rust).collect();
quote! {
#doc_comments
#[allow(non_camel_case_types)]
#ident {#(#inputs),*}
}
}
fn enumeration_to_rust(enumeration: &Enumeration) -> TokenStream2 {
let doc_comments: TokenStream2 = enumeration
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect();
let ident = identifier_to_rust(&enumeration.identifier);
let states: Vec<TokenStream2> = enumeration
.states
.iter()
.map(doc_identifier_to_rust)
.collect();
let debug = get_debug_sate();
quote! {
#doc_comments
#[allow(non_camel_case)]
#debug
enum #ident {
#(#states),*
}
}
}
fn structure_to_rust(structure: &Structure) -> TokenStream2 {
let doc_comments: TokenStream2 = structure
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect();
let ident = identifier_to_rust(&structure.identifier);
let contents: Vec<TokenStream2> = structure
.contents
.iter()
.map(doc_named_type_to_rust)
.collect();
let debug = get_debug_sate();
quote! {
#doc_comments
#[allow(non_camel_case)]
#debug
struct #ident {
#(#contents),*
}
}
}
fn doc_identifier_to_rust(doc_identifier: &DocIdentifier) -> TokenStream2 {
let doc_comments: TokenStream2 = doc_identifier
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect();
let identifier = identifier_to_rust(&doc_identifier.into());
quote! {
#doc_comments
#identifier
}
}
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
.iter()
.map(attribute_to_doc_comment)
.collect();
let named_type = named_type_to_rust(&doc_named_type.into());
quote! {
#doc_comments
#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<TokenStream2> = r#type.generic_args.iter().map(type_to_rust).collect();
quote! {
#ident <#(#generics),*>
}
}

View File

@ -0,0 +1 @@
pub mod host;

56
trixy-macros/src/lib.rs Normal file
View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
use std::fs;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use trixy_lang_parser::parse_trixy_lang;
use crate::config::TrixyConfig;
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);
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};
});
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);
let output = quote! {
#host_rust_code
};
output.into()
}