Skip to content
Open
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
30 changes: 8 additions & 22 deletions fastsim-core/fastsim-proc-macros/src/cumu_method_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ pub(crate) fn cumu_method_derive(input: TokenStream) -> TokenStream {
abort_call_site!("`SetCumulative` works only on Named Field structs.")
};

let struct_has_state = fields.iter().any(|x| *x.ident.as_ref().unwrap() == "state");
let ident_str = ident.to_string();
let struct_is_state = ident_str.contains("State");
let struct_is_state = item_struct
.attrs
.iter()
.any(|attr| attr.path().is_ident("is_state"));
// A field is recursed into if it's marked `#[has_state]` (its type *contains* nested
// state) or `#[is_state]` (its type itself *is* a state struct). Both are handled
// identically today; the distinction is kept explicit to allow future divergence.
let fields_with_state_vec: Vec<bool> = fields
.iter()
.map(|field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("has_state"))
.any(|attr| attr.path().is_ident("has_state") || attr.path().is_ident("is_state"))
})
.collect();

Expand Down Expand Up @@ -106,24 +110,6 @@ pub(crate) fn cumu_method_derive(input: TokenStream) -> TokenStream {
}
}
});
} else if struct_has_state {
impl_block.extend::<TokenStream2>(quote! {
// this tells the compiler that the `SetCumulative` trait is not manually derived
#[automatically_derived]
impl SetCumulative for #ident {
fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
self.state.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
#(self.#fields_with_state.set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;)*
Ok(())
}

fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
self.state.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
#(self.#fields_with_state.reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;)*
Ok(())
}
}
});
} else {
impl_block.extend::<TokenStream2>(quote! {
// this tells the compiler that the `SetCumulative` trait is not manually derived
Expand Down
20 changes: 16 additions & 4 deletions fastsim-core/fastsim-proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,28 @@ pub fn history_vec_derive(input: TokenStream) -> TokenStream {
history_vec_derive::history_vec_derive(input)
}

#[proc_macro_derive(StateMethods, attributes(has_state))]
#[proc_macro_derive(StateMethods, attributes(has_state, is_state))]
/// Generates remaining `StateMethods` child traits that work for struct and any
/// nested fields with the `#[has_state]` attribute.
/// nested fields marked `#[has_state]` or `#[is_state]`.
///
/// - `#[is_state]` on a struct itself marks it as a leaf state struct (e.g.
/// `FooState`), which changes how that struct's own impls are generated.
/// - `#[is_state]` on a field marks that the field's type *is* a state struct
/// (e.g. a `state: FooState` field) and should be recursed into.
/// - `#[has_state]` on a field marks that the field's type merely *contains*
/// nested state (e.g. a sub-component like `fc: FuelConverter`) and should
/// be recursed into.
///
/// `#[has_state]` and `#[is_state]` on a field are handled identically today;
/// the distinction is kept explicit so the two cases can diverge later.
pub fn state_methods_derive(input: TokenStream) -> TokenStream {
sm_derive::state_methods_derive(input)
}

#[proc_macro_derive(SetCumulative, attributes(has_state))]
#[proc_macro_derive(SetCumulative, attributes(has_state, is_state))]
/// Generate `SetCumulative` trait impl that work for struct and any nested
/// fields with the `#[has_state]` attribute.
/// fields marked `#[has_state]` or `#[is_state]`. See [`state_methods_derive`]
/// for how these attributes are used.
pub fn cumu_method_derive(input: TokenStream) -> TokenStream {
cumu_method_derive::cumu_method_derive(input)
}
91 changes: 55 additions & 36 deletions fastsim-core/fastsim-proc-macros/src/sm_derive.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::imports::*;
use crate::utilities::TokenStreamIterator;

lazy_static! {
static ref ENERGY_REGEX: Regex = Regex::new(r"energy_(\w+)").unwrap();
Expand All @@ -15,45 +16,59 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream {
abort_call_site!("`StateMethods` works only on Named Field structs.")
};

let struct_has_state = fields.iter().any(|x| *x.ident.as_ref().unwrap() == "state");
let ident_str = ident.to_string();
let struct_is_state = ident_str.contains("State");
let struct_is_state = item_struct
.attrs
.iter()
.any(|attr| attr.path().is_ident("is_state"));
let struct_has_save_interval = fields
.iter()
.any(|x| *x.ident.as_ref().unwrap() == "save_interval");
// A field is recursed into if it's marked `#[has_state]` (its type *contains* nested
// state) or `#[is_state]` (its type itself *is* a state struct). Both are handled
// identically today; the distinction is kept explicit to allow future divergence.
let fields_with_state_vec: Vec<bool> = fields
.iter()
.map(|field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("has_state"))
.any(|attr| attr.path().is_ident("has_state") || attr.path().is_ident("is_state"))
})
.collect();

// fields that contain nested `state` fields
// fields that participate in nested state-tracking, i.e. fields explicitly marked
// `#[has_state]` or `#[is_state]` (this includes the primary `state` field itself,
// when present, since it must also carry `#[is_state]`)
let fields_with_state = fields
.iter()
.zip(fields_with_state_vec)
.filter(|(_f, hsv)| *hsv)
.map(|(f, _hsv)| f.ident.as_ref().unwrap())
.collect::<Vec<_>>();

// whether this struct owns a primary `state: ...` field, as opposed to merely
// containing other has_state/is_state sub-component fields
let struct_has_state = fields_with_state.iter().any(|f| *f == "state");

// Types of fields tagged `#[is_state]` (as opposed to `#[has_state]`). Each such type
// is asserted below to implement the `IsState` marker trait, which is only implemented
// for structs that themselves derive `#[is_state]`. This turns a field mistagged
// `#[is_state]` (whose type isn't actually a state struct) into a compile error
// instead of silent drift, since both tags are otherwise handled identically by this
// derive. It does not catch the opposite mistake (`#[has_state]` on a field whose type
// happens to be a state struct) since that's not observably wrong today.
let is_state_field_types: Vec<&syn::Type> = fields
.iter()
.filter(|f| f.attrs.iter().any(|attr| attr.path().is_ident("is_state")))
.map(|f| &f.ty)
.collect();

let all_fields = fields
.iter()
.map(|f| f.ident.as_ref().unwrap())
.collect::<Vec<_>>();

let (self_step, self_reset_step): (TokenStream2, TokenStream2) = if struct_has_state {
(
quote! {
self.state.step(|| format!("{}\n{}", loc(), #ident_str))?;
},
quote! {
self.state.reset_step(|| format!("{}\n{}", loc(), #ident_str))?;
},
)
} else if struct_is_state {
let (self_step, self_reset_step): (TokenStream2, TokenStream2) = if struct_is_state {
(
quote! {
self.i.increment(1, || format_dbg!())?;
Expand Down Expand Up @@ -107,27 +122,6 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream {
}
}
});
} else if struct_has_state {
impl_block.extend::<TokenStream2>(quote! {
#[automatically_derived]
impl TrackedStateMethods for #ident {
fn check_and_reset<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
self.state.check_and_reset(|| format!("{}", loc()))?;
#(
self.#fields_with_state.check_and_reset(|| format!("{}\n field in `{}` has not been updated", loc(), stringify!(#fields_with_state)))?;
)*
Ok(())
}

fn mark_fresh<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
self.state.mark_fresh(|| format!("{}", loc()))?;
#(
self.#fields_with_state.mark_fresh(|| format!("{}\n field in `{}` has already been updated", loc(), stringify!(#fields_with_state)))?;
)*
Ok(())
}
}
});
} else {
impl_block.extend::<TokenStream2>(quote! {
#[automatically_derived]
Expand All @@ -153,6 +147,31 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream {
impl StateMethods for #ident {}
});

if struct_is_state {
impl_block.extend::<TokenStream2>(quote! {
#[automatically_derived]
impl IsState for #ident {}
});
}

// Compile-time check that every `#[is_state]` field's type actually implements
// `IsState` (i.e. that type's own struct derives `StateMethods` with `#[is_state]`
// on it). `const _` items are anonymous, so this is safe to emit once per field with
// no naming collisions.
impl_block.extend::<TokenStream2>(
is_state_field_types
.iter()
.map(|ty| {
quote! {
const _: fn() = || {
fn assert_impl_is_state<T: IsState>() {}
assert_impl_is_state::<#ty>();
};
}
})
.concat(),
);

if struct_has_save_interval {
impl_block.extend::<TokenStream2>(quote! {
#[automatically_derived]
Expand Down
6 changes: 6 additions & 0 deletions fastsim-core/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ impl<T: Clone + Sub<T, Output = T> + Default> Diff<T> for Vec<T> {
}
}

/// Marker trait implemented for leaf state structs (i.e. structs derived with
/// `#[is_state]`, e.g. `FooState`). Used to give a compile error, rather than silent
/// drift, when a field tagged `#[is_state]` in `#[derive(StateMethods)]` /
/// `#[derive(SetCumulative)]` does not actually point at a state struct.
pub trait IsState {}

/// Super trait to ensure that related traits are implemented together
pub trait StateMethods: SetCumulative + SaveState + Step + TrackedStateMethods {}

Expand Down
2 changes: 2 additions & 0 deletions fastsim-core/src/vehicle/cabin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub struct LumpedCabin {
/// cabin width, modeled as a flat plate
pub width: si::Length,
#[serde(default)]
#[is_state]
pub state: LumpedCabinState,
#[serde(default, skip_serializing_if = "LumpedCabinStateHistoryVec::is_empty")]
pub history: LumpedCabinStateHistoryVec,
Expand Down Expand Up @@ -327,6 +328,7 @@ impl LumpedCabin {
)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct LumpedCabinState {
/// time step counter
pub i: TrackedState<usize>,
Expand Down
4 changes: 4 additions & 0 deletions fastsim-core/src/vehicle/conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct DfcoControls {
pub save_interval: Option<usize>,
/// current state of control variables
#[serde(default)]
#[is_state]
pub state: DfcoState,
/// history of current state
#[serde(default, skip_serializing_if = "DfcoStateHistoryVec::is_empty")]
Expand Down Expand Up @@ -149,6 +150,7 @@ impl DfcoControls {
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct DfcoState {
/// time step index
pub i: TrackedState<usize>,
Expand Down Expand Up @@ -612,6 +614,7 @@ pub struct ConvStartStopControl {
pub save_interval: Option<usize>,
/// current state of control variables
#[serde(default)]
#[is_state]
pub state: ConvStartStopState,
/// history of current state
#[serde(
Expand Down Expand Up @@ -741,6 +744,7 @@ impl ConvStartStopControl {
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct ConvStartStopState {
/// time step index
pub i: TrackedState<usize>,
Expand Down
4 changes: 4 additions & 0 deletions fastsim-core/src/vehicle/hev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ impl Mass for HybridElectricVehicle {
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct RGWDBState {
/// time step index
pub i: TrackedState<usize>,
Expand Down Expand Up @@ -902,6 +903,7 @@ pub struct RESGreedyWithDynamicBuffers {
pub temp_fc_allowed_off: Option<si::Temperature>,
/// current state of control variables
#[serde(default)]
#[is_state]
pub state: RGWDBState,
/// history of current state
#[serde(default, skip_serializing_if = "RGWDBStateHistoryVec::is_empty")]
Expand Down Expand Up @@ -1219,6 +1221,7 @@ for an HEV equipped with thermal models or superfluous otherwise",
)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct StartStopState {
/// time step index
pub i: TrackedState<usize>,
Expand Down Expand Up @@ -1295,6 +1298,7 @@ pub struct HEVStartStopControl {
pub save_interval: Option<usize>,
/// current state of control variables
#[serde(default)]
#[is_state]
pub state: StartStopState,
/// history of current state
#[serde(default, skip_serializing_if = "StartStopStateHistoryVec::is_empty")]
Expand Down
2 changes: 2 additions & 0 deletions fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct HVACSystemForLumpedCabin {
pub pwr_aux_for_hvac_max: si::Power,
/// coefficient of performance of vapor compression cycle
#[serde(default)]
#[is_state]
pub state: HVACSystemForLumpedCabinState,
#[serde(
default,
Expand Down Expand Up @@ -507,6 +508,7 @@ impl SerdeAPI for CabinHeatSource {}
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(default)]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct HVACSystemForLumpedCabinState {
/// time step counter
pub i: TrackedState<usize>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub struct HVACSystemForLumpedCabinAndRES {
pub pwr_aux_for_hvac_res_max: si::Power,
/// coefficient of performance of vapor compression cycle
#[serde(default)]
#[is_state]
pub state: HVACSystemForLumpedCabinAndRESState,
#[serde(
default,
Expand Down Expand Up @@ -1160,6 +1161,7 @@ impl HVACSystemForLumpedCabinAndRES {
#[serde(default)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct HVACSystemForLumpedCabinAndRESState {
/// time step counter
pub i: TrackedState<usize>,
Expand Down
3 changes: 2 additions & 1 deletion fastsim-core/src/vehicle/powertrain/electric_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub struct ElectricMachine {
pub save_interval: Option<usize>,
/// struct for tracking current state
#[serde(default)]
#[is_state]
pub state: ElectricMachineState,
/// Custom vector of [Self::state]
#[serde(
Expand Down Expand Up @@ -873,7 +874,7 @@ impl EMBuilder {
#[serde(default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]

#[is_state]
pub struct ElectricMachineState {
/// time step index
pub i: TrackedState<usize>,
Expand Down
4 changes: 4 additions & 0 deletions fastsim-core/src/vehicle/powertrain/fuel_converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct FuelConverter {
pub pwr_idle_fuel: si::Power,
/// struct for tracking current state
#[serde(default)]
#[is_state]
pub state: FuelConverterState,
/// Custom vector of [Self::state]
#[serde(
Expand Down Expand Up @@ -561,6 +562,7 @@ pub struct FCBuilder {
#[serde(default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[is_state]
pub struct FuelConverterState {
/// time step index
pub i: TrackedState<usize>,
Expand Down Expand Up @@ -791,6 +793,7 @@ pub struct FuelConverterThermal {
pub fc_eff_model: FCTempEffModel,
/// struct for tracking current state
#[serde(default)]
#[is_state]
pub state: FuelConverterThermalState,
/// Custom vector of [Self::state]
#[serde(
Expand Down Expand Up @@ -1137,6 +1140,7 @@ impl Default for FuelConverterThermal {
#[serde(default)]
#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
#[serde(deny_unknown_fields)]
#[is_state]
pub struct FuelConverterThermalState {
/// time step index
pub i: TrackedState<usize>,
Expand Down
Loading
Loading