diff --git a/fastsim-core/fastsim-proc-macros/src/cumu_method_derive.rs b/fastsim-core/fastsim-proc-macros/src/cumu_method_derive.rs index ed789c82..2cd1c385 100644 --- a/fastsim-core/fastsim-proc-macros/src/cumu_method_derive.rs +++ b/fastsim-core/fastsim-proc-macros/src/cumu_method_derive.rs @@ -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 = 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(); @@ -106,24 +110,6 @@ pub(crate) fn cumu_method_derive(input: TokenStream) -> TokenStream { } } }); - } else if struct_has_state { - impl_block.extend::(quote! { - // this tells the compiler that the `SetCumulative` trait is not manually derived - #[automatically_derived] - impl SetCumulative for #ident { - fn set_cumulative 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 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::(quote! { // this tells the compiler that the `SetCumulative` trait is not manually derived diff --git a/fastsim-core/fastsim-proc-macros/src/lib.rs b/fastsim-core/fastsim-proc-macros/src/lib.rs index 83c4156a..a4cdbd76 100755 --- a/fastsim-core/fastsim-proc-macros/src/lib.rs +++ b/fastsim-core/fastsim-proc-macros/src/lib.rs @@ -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) } diff --git a/fastsim-core/fastsim-proc-macros/src/sm_derive.rs b/fastsim-core/fastsim-proc-macros/src/sm_derive.rs index eef164f1..a7f36a22 100644 --- a/fastsim-core/fastsim-proc-macros/src/sm_derive.rs +++ b/fastsim-core/fastsim-proc-macros/src/sm_derive.rs @@ -1,4 +1,5 @@ use crate::imports::*; +use crate::utilities::TokenStreamIterator; lazy_static! { static ref ENERGY_REGEX: Regex = Regex::new(r"energy_(\w+)").unwrap(); @@ -15,23 +16,29 @@ 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 = 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) @@ -39,21 +46,29 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream { .map(|(f, _hsv)| f.ident.as_ref().unwrap()) .collect::>(); + // 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::>(); - 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!())?; @@ -107,27 +122,6 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream { } } }); - } else if struct_has_state { - impl_block.extend::(quote! { - #[automatically_derived] - impl TrackedStateMethods for #ident { - fn check_and_reset 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 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::(quote! { #[automatically_derived] @@ -153,6 +147,31 @@ pub(crate) fn state_methods_derive(input: TokenStream) -> TokenStream { impl StateMethods for #ident {} }); + if struct_is_state { + impl_block.extend::(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::( + is_state_field_types + .iter() + .map(|ty| { + quote! { + const _: fn() = || { + fn assert_impl_is_state() {} + assert_impl_is_state::<#ty>(); + }; + } + }) + .concat(), + ); + if struct_has_save_interval { impl_block.extend::(quote! { #[automatically_derived] diff --git a/fastsim-core/src/traits.rs b/fastsim-core/src/traits.rs index 8c0f256a..d2db82b8 100644 --- a/fastsim-core/src/traits.rs +++ b/fastsim-core/src/traits.rs @@ -335,6 +335,12 @@ impl + Default> Diff for Vec { } } +/// 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 {} diff --git a/fastsim-core/src/vehicle/cabin.rs b/fastsim-core/src/vehicle/cabin.rs index 3e671f4e..36417c4d 100644 --- a/fastsim-core/src/vehicle/cabin.rs +++ b/fastsim-core/src/vehicle/cabin.rs @@ -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, @@ -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, diff --git a/fastsim-core/src/vehicle/conv.rs b/fastsim-core/src/vehicle/conv.rs index 26ffcf1c..5c3824d2 100644 --- a/fastsim-core/src/vehicle/conv.rs +++ b/fastsim-core/src/vehicle/conv.rs @@ -23,6 +23,7 @@ pub struct DfcoControls { pub save_interval: Option, /// current state of control variables #[serde(default)] + #[is_state] pub state: DfcoState, /// history of current state #[serde(default, skip_serializing_if = "DfcoStateHistoryVec::is_empty")] @@ -149,6 +150,7 @@ impl DfcoControls { )] #[non_exhaustive] #[serde(deny_unknown_fields)] +#[is_state] pub struct DfcoState { /// time step index pub i: TrackedState, @@ -612,6 +614,7 @@ pub struct ConvStartStopControl { pub save_interval: Option, /// current state of control variables #[serde(default)] + #[is_state] pub state: ConvStartStopState, /// history of current state #[serde( @@ -741,6 +744,7 @@ impl ConvStartStopControl { )] #[non_exhaustive] #[serde(deny_unknown_fields)] +#[is_state] pub struct ConvStartStopState { /// time step index pub i: TrackedState, diff --git a/fastsim-core/src/vehicle/hev.rs b/fastsim-core/src/vehicle/hev.rs index b5c88b44..72cdd29a 100644 --- a/fastsim-core/src/vehicle/hev.rs +++ b/fastsim-core/src/vehicle/hev.rs @@ -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, @@ -902,6 +903,7 @@ pub struct RESGreedyWithDynamicBuffers { pub temp_fc_allowed_off: Option, /// current state of control variables #[serde(default)] + #[is_state] pub state: RGWDBState, /// history of current state #[serde(default, skip_serializing_if = "RGWDBStateHistoryVec::is_empty")] @@ -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, @@ -1295,6 +1298,7 @@ pub struct HEVStartStopControl { pub save_interval: Option, /// current state of control variables #[serde(default)] + #[is_state] pub state: StartStopState, /// history of current state #[serde(default, skip_serializing_if = "StartStopStateHistoryVec::is_empty")] diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs index 73c4e98b..2d1a310b 100644 --- a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin.rs @@ -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, @@ -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, diff --git a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs index f1285c93..43d77f6d 100644 --- a/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs +++ b/fastsim-core/src/vehicle/hvac/hvac_sys_for_lumped_cabin_and_res.rs @@ -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, @@ -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, diff --git a/fastsim-core/src/vehicle/powertrain/electric_machine.rs b/fastsim-core/src/vehicle/powertrain/electric_machine.rs index fe323864..4f2d1292 100755 --- a/fastsim-core/src/vehicle/powertrain/electric_machine.rs +++ b/fastsim-core/src/vehicle/powertrain/electric_machine.rs @@ -39,6 +39,7 @@ pub struct ElectricMachine { pub save_interval: Option, /// struct for tracking current state #[serde(default)] + #[is_state] pub state: ElectricMachineState, /// Custom vector of [Self::state] #[serde( @@ -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, diff --git a/fastsim-core/src/vehicle/powertrain/fuel_converter.rs b/fastsim-core/src/vehicle/powertrain/fuel_converter.rs index e2f8eea3..a9727dd8 100755 --- a/fastsim-core/src/vehicle/powertrain/fuel_converter.rs +++ b/fastsim-core/src/vehicle/powertrain/fuel_converter.rs @@ -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( @@ -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, @@ -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( @@ -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, diff --git a/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs b/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs index f96dd07b..aada81da 100644 --- a/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs +++ b/fastsim-core/src/vehicle/powertrain/reversible_energy_storage.rs @@ -38,6 +38,7 @@ pub struct ReversibleEnergyStorage { pub max_soc: si::Ratio, /// struct for tracking current state #[serde(default)] + #[is_state] pub state: ReversibleEnergyStorageState, /// Custom vector of [Self::state] #[serde( @@ -874,6 +875,7 @@ pub enum SpecificEnergySideEffect { #[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))] #[serde(default)] /// ReversibleEnergyStorage state variables +#[is_state] pub struct ReversibleEnergyStorageState { // limits /// max output power for propulsion during positive traction @@ -1122,6 +1124,7 @@ pub struct RESLumpedThermal { pub conductance_to_cab: si::ThermalConductance, /// current state #[serde(default)] + #[is_state] pub state: RESLumpedThermalState, /// history of state #[serde( @@ -1248,6 +1251,7 @@ impl RESLumpedThermal { )] #[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))] #[serde(deny_unknown_fields)] +#[is_state] pub struct RESLumpedThermalState { /// time step index pub i: TrackedState, diff --git a/fastsim-core/src/vehicle/powertrain/transmission.rs b/fastsim-core/src/vehicle/powertrain/transmission.rs index 1d89028b..6c1bfa34 100644 --- a/fastsim-core/src/vehicle/powertrain/transmission.rs +++ b/fastsim-core/src/vehicle/powertrain/transmission.rs @@ -15,6 +15,7 @@ pub struct Transmission { pub eff_interp: InterpolatorEnum, /// struct for tracking current state #[serde(default)] + #[is_state] pub state: TransmissionState, /// Custom vector of [Self::state] #[serde(default, skip_serializing_if = "TransmissionStateHistoryVec::is_empty")] @@ -215,6 +216,7 @@ impl Mass for Transmission { #[non_exhaustive] #[serde(default)] #[serde(deny_unknown_fields)] +#[is_state] pub struct TransmissionState { /// time step index pub i: TrackedState, diff --git a/fastsim-core/src/vehicle/vehicle_model.rs b/fastsim-core/src/vehicle/vehicle_model.rs index d54dfe37..d768e31d 100644 --- a/fastsim-core/src/vehicle/vehicle_model.rs +++ b/fastsim-core/src/vehicle/vehicle_model.rs @@ -75,6 +75,7 @@ pub struct Vehicle { pub(crate) save_interval: Option, /// current state of vehicle #[serde(default)] + #[is_state] pub state: VehicleState, /// Vector-like history of [Self::state] #[serde(default, skip_serializing_if = "VehicleStateHistoryVec::is_empty")] @@ -1300,6 +1301,7 @@ impl Vehicle { #[non_exhaustive] #[serde(default)] #[serde(deny_unknown_fields)] +#[is_state] pub struct VehicleState { /// time step index pub i: TrackedState,