From 46587ff8223a6072549be7265dfa29745a95e333 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Wed, 28 Jan 2026 15:21:45 +0100 Subject: [PATCH] rsx proc macro --- Cargo.toml | 4 ++ macros/Cargo.toml | 13 ++++ macros/src/emit.rs | 98 ++++++++++++++++++++++++++ macros/src/lib.rs | 10 +++ macros/src/parse.rs | 129 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +- src/macros.rs | 165 -------------------------------------------- 7 files changed, 255 insertions(+), 167 deletions(-) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/emit.rs create mode 100644 macros/src/lib.rs create mode 100644 macros/src/parse.rs delete mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index a40db29..6aec650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ cc = "1.0" [dependencies] crossterm = "0.28.1" figlet-rs = "0.1.5" +osui-macros = { path = "macros" } [profile.dev] opt-level = 1 @@ -42,3 +43,6 @@ overflow-checks = false lto = true panic = 'abort' codegen-units = 1 + +[workspace] +members = [".", "macros"] diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..3337994 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "osui-macros" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/macros/src/emit.rs b/macros/src/emit.rs new file mode 100644 index 0000000..d909d2a --- /dev/null +++ b/macros/src/emit.rs @@ -0,0 +1,98 @@ +use crate::parse::*; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; + +pub fn emit_rsx(root: RsxRoot) -> TokenStream { + let nodes = root.nodes.iter().map(emit_node); + + quote! {{ + let mut r = osui::frontend::Rsx::new(); + #(#nodes)* + r + }} +} + +fn emit_deps(deps: &[Dep]) -> TokenStream { + deps.iter() + .map(|d| { + let ident = &d.ident; + if let Some(pat) = &d.pat { + quote!( let #pat = #ident.clone(); ) + } else { + quote!( let #ident = #ident.clone(); ) + } + }) + .collect() +} + +fn emit_node(node: &RsxNode) -> TokenStream { + match node { + RsxNode::Text(text) => quote! { + { + let scope = osui::component::Scope::new(); + scope.view(Arc::new(move |ctx| { + ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) + })); + r.static_scope(scope); + } + }, + + RsxNode::Component(name) => quote! { + { + let scope = osui::component::Scope::new(); + scope.child(#name, None); + r.static_scope(scope); + } + }, + + RsxNode::Mount(m) => quote! { + #m.mount(); + }, + + RsxNode::If { + deps, + cond, + children, + } => { + let deps_emit = emit_deps(deps); + let kids = children.iter().map(emit_node); + + quote! { + { + #deps_emit + r.dynamic_scope(move |scope| { + if #cond { + if scope.children.lock().unwrap().is_empty() { + #(#kids)* + } + } else { + scope.children.lock().unwrap().clear(); + } + }); + } + } + } + + RsxNode::For { + deps, + pat, + expr, + children, + } => { + let deps_emit = emit_deps(deps); + let kids = children.iter().map(emit_node); + + quote! { + { + #deps_emit + r.dynamic_scope(move |scope| { + scope.children.lock().unwrap().clear(); + for #pat in #expr { + #(#kids)* + } + }); + } + } + } + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..bb4e672 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,10 @@ +use proc_macro::TokenStream; + +mod emit; +mod parse; + +#[proc_macro] +pub fn rsx(input: TokenStream) -> TokenStream { + let ast = syn::parse_macro_input!(input as parse::RsxRoot); + emit::emit_rsx(ast).into() +} diff --git a/macros/src/parse.rs b/macros/src/parse.rs new file mode 100644 index 0000000..fc4ee19 --- /dev/null +++ b/macros/src/parse.rs @@ -0,0 +1,129 @@ +use syn::braced; +use syn::{ + Expr, Ident, LitStr, Pat, Result, Token, + parse::{Parse, ParseStream}, +}; + +pub struct RsxRoot { + pub nodes: Vec, +} + +impl Parse for RsxRoot { + fn parse(input: ParseStream) -> Result { + let mut nodes = Vec::new(); + while !input.is_empty() { + nodes.push(input.parse()?); + } + Ok(Self { nodes }) + } +} + +pub enum RsxNode { + Text(LitStr), + Component(Ident), + Mount(Ident), + If { + deps: Vec, + cond: Expr, + children: Vec, + }, + For { + deps: Vec, + pat: Pat, + expr: Expr, + children: Vec, + }, +} + +pub struct Dep { + pub ident: Ident, + pub pat: Option, +} + +fn parse_deps(input: ParseStream) -> Result> { + let mut deps = Vec::new(); + + if input.peek(Token![%]) { + input.parse::()?; + loop { + let ident: Ident = input.parse()?; + let pat = if input.peek(Token![as]) { + input.parse::()?; + // Patterns in syn 2.0 are parsed via `Pat::parse_multi` + // instead of implementing the `Parse` trait directly. + Some(Pat::parse_multi(input)?) + } else { + None + }; + + deps.push(Dep { ident, pat }); + + if !input.peek(Token![,]) { + break; + } + input.parse::()?; + } + } + + Ok(deps) +} + +impl Parse for RsxNode { + fn parse(input: ParseStream) -> Result { + if input.peek(Token![!]) { + input.parse::()?; + let mount: Ident = input.parse()?; + return Ok(RsxNode::Mount(mount)); + } + + let deps = parse_deps(input)?; + + if input.peek(Token![if]) { + input.parse::()?; + let cond: Expr = input.parse()?; + + let content; + braced!(content in input); + + let children = parse_children(&content)?; + return Ok(RsxNode::If { + deps, + cond, + children, + }); + } + + if input.peek(Token![for]) { + input.parse::()?; + // Parse a possibly multi-pattern (e.g. with `|`) for the `for` binding. + let pat: Pat = Pat::parse_multi(input)?; + input.parse::()?; + let expr: Expr = input.parse()?; + + let content; + braced!(content in input); + + let children = parse_children(&content)?; + return Ok(RsxNode::For { + deps, + pat, + expr, + children, + }); + } + + if input.peek(LitStr) { + return Ok(RsxNode::Text(input.parse()?)); + } + + Ok(RsxNode::Component(input.parse()?)) + } +} + +fn parse_children(input: ParseStream) -> Result> { + let mut nodes = Vec::new(); + while !input.is_empty() { + nodes.push(input.parse()?); + } + Ok(nodes) +} diff --git a/src/lib.rs b/src/lib.rs index 690fac9..8b6da82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ use crate::render::DrawContext; pub mod component; pub mod engine; pub mod frontend; -pub mod macros; pub mod render; pub mod state; @@ -13,9 +12,9 @@ pub mod prelude { pub use crate::component::*; pub use crate::engine::*; pub use crate::render::*; - pub use crate::rsx; pub use crate::state::*; pub use crate::View; + pub use osui_macros::rsx; } pub type View = Arc; diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 23c3e7a..0000000 --- a/src/macros.rs +++ /dev/null @@ -1,165 +0,0 @@ -#[macro_export] -macro_rules! rsx { - () => { - $crate::frontend::Rsx::new() - }; - - ($($rsx:tt)+) => {{ - let mut r = $crate::frontend::Rsx::new(); - { - $crate::rsx_scope!(r, $($rsx)+); - } - r - }}; -} - -#[macro_export] -macro_rules! rsx_scope { - ($rsx:expr, for $(%$($dep:ident $(as $dp:pat,)?),+)? ($p:pat in $v:expr) {$($inner:tt)*} $($rest:tt)*) => { - { - $($($crate::rsx_dep!($dep);)+)? - - $rsx.dynamic_scope({ - $($($crate::rsx_dep!($dep $(as $dp)?);)+)? - move |scope| { - scope.children.lock().unwrap().clear(); - for $p in $v { - $crate::rsx_child!(scope, $($inner)*); - } - } - }, vec![$($(Box::new($dep)),+)?]); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, if $(%$($dep:ident $(as $dp:pat,)?),+)? ($st:expr) {$($inner:tt)*} $($rest:tt)*) => { - { - $($($crate::rsx_dep!($dep);)+)? - - $rsx.dynamic_scope({ - $($($crate::rsx_dep!($dep $(as $dp)?);)+)? - move |scope| { - if $st { - if scope.children.lock().unwrap().len() == 0 { - $crate::rsx_child!(scope, $($inner)*); - } - } else { - scope.children.lock().unwrap().clear(); - } - } - }, vec![$($(Box::new($dep)),+)?]); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal @$d:expr; $($rest:tt)*) => { - { - let scope = $crate::component::Scope::new(); - - $($($crate::rsx_dep!($dep $(as $dp)?);)+)? - - $crate::rsx_child!(scope, $text @$d;); - - $rsx.static_scope(scope); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal $($rest:tt)*) => { - { - let scope = $crate::component::Scope::new(); - - $($($crate::rsx_dep!($dep $(as $dp)?);)+)? - - $crate::rsx_child!(scope, $text); - - $rsx.static_scope(scope); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => { - { - let scope = $crate::component::Scope::new(); - - $crate::rsx_child!(scope, $component ($ctx, $view) $body); - - $rsx.static_scope(scope); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, $component:ident $($rest:tt)*) => { - { - let scope = $crate::component::Scope::new(); - - $crate::rsx_child!(scope, $component); - - $rsx.static_scope(scope); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr, !mount $mount:ident $($rest:tt)*) => { - { - $mount.mount(); - } - - $crate::rsx_scope!($rsx, $($rest)*); - }; - - ($rsx:expr,) => {}; -} - -#[macro_export] -macro_rules! rsx_child { - ($scope:expr, $text:literal @$d:expr; $($rest:tt)*) => { - { - $scope.view(Arc::new(move |ctx| ctx.draw_text($d, &format!($text)))); - } - - $crate::rsx_child!($scope, $($rest)*); - }; - - ($scope:expr, $text:literal $($rest:tt)*) => { - { - $scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!($text)))); - } - - $crate::rsx_child!($scope, $($rest)*); - }; - - ($scope:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => { - { - $scope.child($component, Some(Arc::new(|$ctx, $view| $body))); - } - - $crate::rsx_child!($scope, $($rest)*); - }; - - ($scope:expr, $component:ident $($rest:tt)*) => { - { - $scope.child($component, None); - } - $crate::rsx_child!($scope, $($rest)*); - }; - - ($rsx:expr,) => {}; -} - -#[macro_export] -macro_rules! rsx_dep { - ($dep:ident as $dp:pat) => { - let $dp = $dep.clone(); - }; - - ($dep:ident) => { - let $dep = $dep.clone(); - }; -}