Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,19 @@ they can become typed machines.
If you add derives, place them below `#[state]` and `#[machine]`:

```rust
# use statum::{machine, state};
# #[state]
# #[derive(Debug, Clone)]
# enum LightState {
# Off,
# }
#[machine]
#[derive(Debug, Clone)]
struct LightSwitch<LightState> {
name: String,
}

# fn main() {}
```

That avoids the common `missing fields marker and state_data` error.
Expand Down
1 change: 1 addition & 0 deletions macro_registry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Simple query example:
```rust
use macro_registry::query::{ItemKind, candidates_in_module};

# let file_path = "/tmp/lib.rs";
let machines = candidates_in_module(
file_path,
"crate::workflow",
Expand Down
4 changes: 4 additions & 0 deletions macro_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ pub mod analysis;
pub mod callsite;
pub mod query;
pub mod registry;

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}
31 changes: 31 additions & 0 deletions module_path_extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ mod cache;
mod parser;
mod pathing;

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}

use std::path::Path;

use crate::cache::{
Expand Down Expand Up @@ -185,6 +189,33 @@ mod tests {
let _ = fs::remove_dir_all(crate_dir);
}

#[test]
fn find_module_path_in_file_separates_sibling_modules_with_similar_shapes() {
let crate_dir = unique_temp_dir("sibling_modules");
let src = crate_dir.join("src");
let lib = src.join("lib.rs");

write_file(
&lib,
"mod alpha {\n mod support {\n pub struct Text;\n }\n\n pub enum WorkflowState {\n Draft,\n }\n\n pub struct Row {\n pub status: &'static str,\n }\n}\n\nmod beta {\n mod support {\n pub struct Text;\n }\n\n pub enum WorkflowState {\n Draft,\n }\n\n pub struct Row {\n pub status: &'static str,\n }\n}\n",
);

assert_eq!(
find_module_path_in_file(&lib.to_string_lossy(), 6, &src).as_deref(),
Some("alpha")
);
assert_eq!(
find_module_path_in_file(&lib.to_string_lossy(), 20, &src).as_deref(),
Some("beta")
);
assert_eq!(
find_module_path_in_file(&lib.to_string_lossy(), 19, &src).as_deref(),
Some("beta")
);

let _ = fs::remove_dir_all(crate_dir);
}

#[test]
fn find_module_path_in_file_ignores_mod_tokens_in_comments_and_raw_strings() {
let crate_dir = unique_temp_dir("comments_and_raw_strings");
Expand Down
57 changes: 44 additions & 13 deletions module_path_extractor/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ fn raw_identifier_len(bytes: &[u8], start: usize) -> Option<usize> {
Some(idx - start)
}

fn char_literal_len(content: &str, bytes: &[u8], start: usize) -> Option<usize> {
if bytes.get(start) != Some(&b'\'') {
return None;
}

let mut idx = start + 1;
if idx >= bytes.len() || bytes[idx] == b'\n' {
return None;
}

if bytes[idx] == b'\\' {
idx += 1;
if idx >= bytes.len() {
return None;
}

if bytes[idx] == b'u' && bytes.get(idx + 1) == Some(&b'{') {
idx += 2;
while idx < bytes.len() && bytes[idx] != b'}' {
if bytes[idx] == b'\n' {
return None;
}
idx += 1;
}
if bytes.get(idx) != Some(&b'}') {
return None;
}
idx += 1;
} else {
idx += 1;
}
} else {
let next = content.get(idx..)?.chars().next()?;
idx += next.len_utf8();
}

(bytes.get(idx) == Some(&b'\'')).then_some(idx - start + 1)
}

fn handle_identifier_token(
token: &str,
expect_mod_ident: &mut bool,
Expand Down Expand Up @@ -98,7 +137,6 @@ fn build_line_module_paths(content: &str) -> Vec<String> {
LineComment,
BlockComment { depth: usize },
String { escaped: bool },
Char { escaped: bool },
RawString { hashes: usize },
}

Expand Down Expand Up @@ -170,17 +208,6 @@ fn build_line_module_paths(content: &str) -> Vec<String> {
i += 1;
continue;
}
Mode::Char { escaped } => {
if byte == b'\\' && !escaped {
mode = Mode::Char { escaped: true };
} else if byte == b'\'' && !escaped {
mode = Mode::Normal;
} else {
mode = Mode::Char { escaped: false };
}
i += 1;
continue;
}
Mode::RawString { hashes } => {
if byte == b'"' {
let mut matched = true;
Expand Down Expand Up @@ -217,8 +244,12 @@ fn build_line_module_paths(content: &str) -> Vec<String> {
i += 1;
continue;
}
if let Some(consumed) = char_literal_len(content, bytes, i) {
i += consumed;
continue;
}
if byte == b'\'' {
mode = Mode::Char { escaped: false };
// Lifetimes and labels are not string delimiters and should not hide module tokens.
i += 1;
continue;
}
Expand Down
4 changes: 4 additions & 0 deletions statum-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
//! - runtime error and result types
//! - projection helpers for event-log style rebuilds

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}

mod introspection;

pub mod projection;
Expand Down
4 changes: 4 additions & 0 deletions statum-examples/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

pub mod showcases;
pub mod toy_demos;

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}
49 changes: 42 additions & 7 deletions statum-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
//! - [`transition`] for validating legal transition impls
//! - [`validators`] for rebuilding typed machines from persisted data

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}

mod syntax;

moddef::moddef!(
Expand All @@ -25,9 +29,16 @@ moddef::moddef!(
}
);

pub(crate) use syntax::{ItemTarget, ModulePath, extract_derives};
pub(crate) use syntax::{
ItemTarget, ModulePath, SourceFingerprint, crate_root_for_file, current_crate_root,
extract_derives, source_file_fingerprint,
};

use crate::{MachinePath, ensure_machine_loaded_by_name, unique_loaded_machine_elsewhere};
use crate::{
LoadedMachineLookupFailure, MachinePath, ambiguous_transition_machine_error,
ambiguous_transition_machine_fallback_error, lookup_loaded_machine_in_module,
lookup_unique_loaded_machine_by_name,
};
use macro_registry::callsite::current_module_path_opt;
use proc_macro::TokenStream;
use proc_macro2::Span;
Expand Down Expand Up @@ -60,7 +71,7 @@ pub fn state(_attr: TokenStream, item: TokenStream) -> TokenStream {
store_state_enum(&enum_info);

// Generate structs and implementations dynamically
let expanded = generate_state_impls(&enum_info.module_path);
let expanded = generate_state_impls(&enum_info);

TokenStream::from(expanded)
}
Expand Down Expand Up @@ -123,10 +134,34 @@ pub fn transition(
};

let machine_path: MachinePath = module_path.clone().into();
// `include!` gives the transition macro the included file as its source context,
// so exact module lookup can miss the already-loaded parent machine.
let machine_info_owned = ensure_machine_loaded_by_name(&machine_path, &tr_impl.machine_name)
.or_else(|| unique_loaded_machine_elsewhere(&tr_impl.machine_name));
let machine_info_owned =
match lookup_loaded_machine_in_module(&machine_path, &tr_impl.machine_name) {
Ok(info) => Some(info),
Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
return ambiguous_transition_machine_error(
&tr_impl.machine_name,
&module_path,
&candidates,
tr_impl.machine_span,
)
.into();
}
Err(LoadedMachineLookupFailure::NotFound) => {
match lookup_unique_loaded_machine_by_name(&tr_impl.machine_name) {
Ok(info) => Some(info),
Err(LoadedMachineLookupFailure::Ambiguous(candidates)) => {
return ambiguous_transition_machine_fallback_error(
&tr_impl.machine_name,
&module_path,
&candidates,
tr_impl.machine_span,
)
.into();
}
Err(LoadedMachineLookupFailure::NotFound) => None,
}
}
};
let machine_info = match machine_info_owned.as_ref() {
Some(info) => info,
None => {
Expand Down
23 changes: 0 additions & 23 deletions statum-macros/src/machine/emission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,9 @@ use crate::state::{ParsedEnumInfo, ParsedVariantInfo};
use crate::{EnumInfo, to_snake_case};

use super::metadata::{ParsedMachineInfo, field_type_alias_name, is_rust_analyzer};
use super::registry::get_machine_map;
use super::{MachineInfo, transition_slice_ident};

pub fn generate_machine_impls(machine_info: &MachineInfo, item: &ItemStruct) -> proc_macro2::TokenStream {
let map_guard = match get_machine_map().read() {
Ok(guard) => guard,
Err(_) => {
let message = format!(
"Internal error: machine metadata lock poisoned while generating `{}` in module `{}`.",
machine_info.name, machine_info.module_path
);
return quote! {
compile_error!(#message);
};
}
};
let Some(machine_info) = map_guard.get(&machine_info.module_path) else {
let message = format!(
"Internal error: machine metadata for `{}` in module `{}` was not cached during code generation.\nTry re-running `cargo check` and make sure `#[machine]` is applied in that module.",
machine_info.name, machine_info.module_path
);
return quote! {
compile_error!(#message);
};
};

let state_enum = match machine_info.get_matching_state_enum() {
Ok(enum_info) => enum_info,
Err(err) => return err,
Expand Down
Loading
Loading