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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"stageleft_test_macro",
"stageleft_test_downstream",
"stageleft_tool",
"stageleft_test_no_entry",
]

resolver = "2"
Expand Down
1 change: 1 addition & 0 deletions stageleft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ stageleft_macro = { path = "../stageleft_macro", version = "^0.11.0" }
ctor = "0.4.1"

[dev-dependencies]
slotmap = "1.0.0"
trybuild = "1"
32 changes: 32 additions & 0 deletions stageleft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,38 @@ macro_rules! stageleft_no_entry_crate {
};
}

/// Re-exports items that have been generated by macros via `pub use`.
///
/// Must be named `stageleft_export!` to work properly. (Do NOT do `use stageleft::stageleft_export as other_name;`).
///
/// This ensures that macro-generated items that stageleft cannot handle on its own will properly use the items.
///
/// ```rust,ignore
/// stageleft::stageleft_export!(MyKey, MyOtherKey);
/// ```
#[macro_export]
macro_rules! stageleft_export {
(
$(
$name:ident
),* $(,)?
) => {
$(
#[expect(unused_imports, reason = "ensures item exists and is pub")]
pub use self::$name as _;
)*
};
(
mod = $path:path
$(
,
$name:ident
)* $(,)?
) => {
pub use $path :: { $( $name ),* };
};
}

pub trait QuotedContext {
fn create() -> Self;
}
Expand Down
9 changes: 9 additions & 0 deletions stageleft/tests/compile-fail/export_not_pub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
stageleft::stageleft_export!(PubKey, NotPubKey);

#[cfg(stageleft_runtime)]
slotmap::new_key_type! {
pub struct PubKey;
pub(crate) struct NotPubKey;
}

fn main() {}
11 changes: 11 additions & 0 deletions stageleft/tests/compile-fail/export_not_pub.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error[E0364]: `NotPubKey` is only public within the crate, and cannot be re-exported outside
--> tests/compile-fail/export_not_pub.rs:1:29
|
1 | #[stageleft::export(PubKey, NotPubKey)]
| ^^^^^^^^^
|
note: consider marking `NotPubKey` as `pub` in the imported module
--> tests/compile-fail/export_not_pub.rs:1:29
|
1 | #[stageleft::export(PubKey, NotPubKey)]
| ^^^^^^^^^
18 changes: 18 additions & 0 deletions stageleft_test_no_entry/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "stageleft_test_no_entry"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
repository.workspace = true

[package.metadata.release]
release = false

[dependencies]
stageleft = { path = "../stageleft", version = "^0.11.0" }

slotmap = "1.0.0"

[build-dependencies]
stageleft_tool = { path = "../stageleft_tool", version = "^0.11.0" }
5 changes: 5 additions & 0 deletions stageleft_test_no_entry/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fn main() {
// Hack to make sure stageleft actually builds code.
unsafe { std::env::set_var("STAGELEFT_TRYBUILD_BUILD_STAGED", "1") };
stageleft_tool::gen_final!();
}
24 changes: 24 additions & 0 deletions stageleft_test_no_entry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
stageleft::stageleft_no_entry_crate!();

stageleft::stageleft_export!(MyKey, OtherKey);

#[cfg(stageleft_runtime)]
slotmap::new_key_type! {
/// An item generated within a macro.
pub struct MyKey;

/// Just test the macro expansion is delimiting properly.
pub struct OtherKey;
}

/// Test that `stageleft::export` prevents splitbrain of `MyKey` type.
#[allow(dead_code)]
fn splitbrain(st: SplitbrainStruct) {
// This gets turned into `crate::__staged::MyKey`
let _key: MyKey = st.my_key;
}

pub struct SplitbrainStruct {
/// This stays as regular `MyKey` (equiv. to `crate::MyKey`).
my_key: MyKey,
}
42 changes: 36 additions & 6 deletions stageleft_tool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::Path;
use std::{env, fs};

use proc_macro2::Span;
use quote::ToTokens;
use quote::{ToTokens, quote};
use sha2::{Digest, Sha256};
use syn::visit::Visit;
use syn::visit_mut::VisitMut;
Expand Down Expand Up @@ -31,7 +31,7 @@ impl<'a> Visit<'a> for GenMacroVistor {
let is_entry = i
.attrs
.iter()
.any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry");
.any(|a| a.path().to_token_stream().to_string() == "stageleft :: entry"); // TODO(mingwei): use #root?

if is_entry {
let cur_path = &self.current_mod;
Expand Down Expand Up @@ -109,6 +109,7 @@ impl VisitMut for InlineTopLevelMod {
i.items.iter_mut().for_each(|i| {
if let syn::Item::Macro(e) = i
&& e.mac.path.to_token_stream().to_string() == "stageleft :: top_level_mod"
// TODO(mingwei): use #root?
{
let inner = &e.mac.tokens;
*i = parse_quote!(
Expand Down Expand Up @@ -285,7 +286,8 @@ impl VisitMut for GenFinalPubVistor {
fn visit_item_mut(&mut self, i: &mut syn::Item) {
// TODO(shadaj): warn if a pub struct or enum has private fields
// and is not marked for runtime
if let Some(cur_path) = self.current_mod.as_ref() {
let cur_path = self.current_mod.as_ref().unwrap();
{
if let syn::Item::Struct(e) = i {
if is_runtime(&e.attrs) {
e.attrs.insert(
Expand Down Expand Up @@ -363,11 +365,11 @@ impl VisitMut for GenFinalPubVistor {
}
} else if let syn::Item::Macro(m) = i {
if is_runtime(&m.attrs) {
// TODO(mingwei): Remove the item entirely (tricky).
m.attrs.insert(
0,
parse_quote!(#[cfg(all(stageleft_macro, not(stageleft_macro)))]),
);
return;
}

if m.attrs
Expand All @@ -383,6 +385,7 @@ impl VisitMut for GenFinalPubVistor {
} else if let syn::Item::Impl(e) = i {
// TODO(shadaj): emit impls if the struct is private
// currently, we just skip all impls
// TODO(mingwei): Remove the item entirely (tricky).
*i = parse_quote!(
#[cfg(all(stageleft_macro, not(stageleft_macro)))]
#e
Expand All @@ -393,13 +396,40 @@ impl VisitMut for GenFinalPubVistor {
syn::visit_mut::visit_item_mut(self, i);
}

fn visit_item_macro_mut(&mut self, i: &mut syn::ItemMacro) {
let curr_path = self.current_mod.as_ref().unwrap();
if i.mac
.path
.segments
.last()
.is_some_and(|m| m.ident == "stageleft_export")
{
match i.mac.parse_body_with(
syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated,
) {
Ok(idents) => {
// Let the macro do the work, via `mod = ...`.
i.mac.tokens = quote! {
mod = #curr_path, #idents
};
}
Err(err) => {
// `::core::compile_error!("...");`
let compile_err = err.into_compile_error();
*i = parse_quote!(#compile_err);
}
}
}
syn::visit_mut::visit_item_macro_mut(self, i);
}

fn visit_file_mut(&mut self, i: &mut syn::File) {
i.attrs = vec![];
i.items.retain(|i| match i {
syn::Item::Macro(m) => {
m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate"
m.mac.path.to_token_stream().to_string() != "stageleft :: stageleft_crate" // TODO(mingwei): use #root?
&& m.mac.path.to_token_stream().to_string()
!= "stageleft :: stageleft_no_entry_crate"
!= "stageleft :: stageleft_no_entry_crate" // TODO(mingwei): use #root?
}
_ => true,
});
Expand Down
Loading