Skip to main content

vihaco_runtime/
generated.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4use eyre::Result;
5
6use crate::Effects;
7
8pub trait GeneratedComponent {
9    type Instruction;
10    type Message;
11    type Effect;
12
13    fn execute_generated(
14        &mut self,
15        inst: &Self::Instruction,
16        msg: Self::Message,
17    ) -> Result<Effects<Self::Effect>>;
18}
19
20/// The instruction types exposed by a device to its two execution layers.
21pub trait HasInstructionSet {
22    /// The instruction consumed by the runtime device.
23    type Runtime;
24
25    /// The instruction parsed from source syntax.
26    type Syntax;
27}
28
29/// A device that executes instructions.
30///
31/// With `typed-declarations`, capabilities are independent of an executable
32/// instruction set. Without it, components retain their instruction-set bound.
33#[cfg(not(feature = "typed-declarations"))]
34pub trait Component: HasInstructionSet {}
35
36/// An execution-capable device, independent of its executable instruction set.
37#[cfg(feature = "typed-declarations")]
38pub trait Component {}
39
40/// A machine assembled from device fields, with an executable instruction set.
41#[cfg(not(feature = "typed-declarations"))]
42pub trait Composite: HasInstructionSet {}
43
44/// A machine assembled from device fields, independent of executable selection.
45#[cfg(feature = "typed-declarations")]
46pub trait Composite {}
47
48pub fn expect_exactly_one_effect<E>(effects: Effects<E>) -> Result<E> {
49    match effects {
50        Effects::None => Err(eyre::eyre!("expected exactly one effect, got 0")),
51        Effects::One(effect) => Ok(effect),
52        Effects::Many(mut effects) => match effects.len() {
53            0 => Err(eyre::eyre!("expected exactly one effect, got 0")),
54            1 => Ok(effects.pop().expect("one effect is present")),
55            effect_count => Err(eyre::eyre!(
56                "expected exactly one effect, got {}",
57                effect_count
58            )),
59        },
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct CompositeMetadata {
65    pub devices: &'static [vihaco_abi::metadata::DeviceMetadata],
66    pub source_symbol_aliases: &'static [vihaco_abi::metadata::SourceSymbolAliasMetadata],
67}
68
69impl CompositeMetadata {
70    pub fn devices(&self) -> &'static [vihaco_abi::metadata::DeviceMetadata] {
71        self.devices
72    }
73
74    pub fn device_by_name(
75        &self,
76        name: &str,
77    ) -> Option<&'static vihaco_abi::metadata::DeviceMetadata> {
78        self.devices.iter().find(|device| device.name == name)
79    }
80
81    pub fn source_symbol_aliases(
82        &self,
83    ) -> &'static [vihaco_abi::metadata::SourceSymbolAliasMetadata] {
84        self.source_symbol_aliases
85    }
86
87    pub fn source_symbol_device_code(&self, name: &str) -> Option<u8> {
88        if let Some(alias) = self
89            .source_symbol_aliases
90            .iter()
91            .find(|alias| alias.name == name)
92        {
93            return Some(alias.device_code);
94        }
95        self.device_by_name(name).map(|device| device.code)
96    }
97
98    pub fn validate_source_symbols<I, V, Ty, Info>(
99        &self,
100        module: &vihaco_module::module::LocalModule<I, V, Ty, Info>,
101    ) -> Result<()> {
102        let unresolved: Vec<String> = module
103            .source_symbols
104            .iter()
105            .filter(|symbol| self.source_symbol_device_code(&symbol.name).is_none())
106            .map(|symbol| format!("{}: {}", symbol.index, symbol.name))
107            .collect();
108
109        if unresolved.is_empty() {
110            return Ok(());
111        }
112
113        Err(eyre::eyre!(
114            "module declares unresolved source symbols for this machine: {}",
115            unresolved.join(", ")
116        ))
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::expect_exactly_one_effect;
123    use crate::Effects;
124
125    #[test]
126    fn expect_exactly_one_effect_accepts_single_effect() {
127        let effect = expect_exactly_one_effect(Effects::one(7)).unwrap();
128
129        assert_eq!(effect, 7);
130    }
131
132    #[test]
133    fn expect_exactly_one_effect_rejects_zero_effects() {
134        let err = expect_exactly_one_effect::<u8>(Effects::none()).unwrap_err();
135
136        assert_eq!(err.to_string(), "expected exactly one effect, got 0");
137    }
138
139    #[test]
140    fn expect_exactly_one_effect_rejects_multiple_effects() {
141        let err = expect_exactly_one_effect(Effects::from(vec![1, 2])).unwrap_err();
142
143        assert_eq!(err.to_string(), "expected exactly one effect, got 2");
144    }
145
146    #[test]
147    fn expect_exactly_one_effect_accepts_one_item_in_many() {
148        let effects = Effects::Many([7].into_iter().collect());
149
150        assert_eq!(expect_exactly_one_effect(effects).unwrap(), 7);
151    }
152
153    #[test]
154    fn expect_exactly_one_effect_rejects_empty_many() {
155        let effects = Effects::Many([].into_iter().collect());
156        let err = expect_exactly_one_effect::<u8>(effects).unwrap_err();
157
158        assert_eq!(err.to_string(), "expected exactly one effect, got 0");
159    }
160}