hyperstack_interpreter/
resolvers.rs

1use std::collections::HashMap;
2
3/// Context provided to primary key resolver functions
4pub struct ResolveContext<'a> {
5    #[allow(dead_code)]
6    pub(crate) state_id: u32,
7    pub(crate) slot: u64,
8    pub(crate) signature: String,
9    pub(crate) reverse_lookups:
10        &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
11}
12
13impl<'a> ResolveContext<'a> {
14    /// Create a new ResolveContext (primarily for use by generated code)
15    pub fn new(
16        state_id: u32,
17        slot: u64,
18        signature: String,
19        reverse_lookups: &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
20    ) -> Self {
21        Self {
22            state_id,
23            slot,
24            signature,
25            reverse_lookups,
26        }
27    }
28
29    /// Try to reverse lookup a PDA address to find the seed value
30    /// This is typically used to find the primary key from a PDA account address
31    pub fn pda_reverse_lookup(&mut self, pda_address: &str) -> Option<String> {
32        // Default lookup name - could be made configurable
33        let lookup_name = "default_pda_lookup";
34
35        if let Some(lookup_table) = self.reverse_lookups.get_mut(lookup_name) {
36            let result = lookup_table.lookup(pda_address);
37            if result.is_some() {
38                tracing::debug!("✓ PDA reverse lookup hit: {} -> {:?}", pda_address, result);
39            } else {
40                tracing::debug!("✗ PDA reverse lookup miss: {}", pda_address);
41            }
42            result
43        } else {
44            tracing::debug!("✗ PDA reverse lookup table '{}' not found", lookup_name);
45            None
46        }
47    }
48
49    pub fn slot(&self) -> u64 {
50        self.slot
51    }
52
53    pub fn signature(&self) -> &str {
54        &self.signature
55    }
56}
57
58/// Result of attempting to resolve a primary key
59pub enum KeyResolution {
60    /// Primary key successfully resolved
61    Found(String),
62
63    /// Queue this update until we see one of these instruction discriminators
64    /// The discriminators identify which instructions can populate the reverse lookup
65    QueueUntil(&'static [u8]),
66
67    /// Skip this update entirely (don't queue)
68    Skip,
69}
70
71/// Context provided to instruction hook functions
72pub struct InstructionContext<'a> {
73    pub(crate) accounts: HashMap<String, String>,
74    #[allow(dead_code)]
75    pub(crate) state_id: u32,
76    pub(crate) reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
77    pub(crate) pending_updates: Vec<crate::vm::PendingAccountUpdate>,
78    // Fields for metrics context integration
79    pub(crate) registers: Option<&'a mut Vec<crate::vm::RegisterValue>>,
80    pub(crate) state_reg: Option<crate::vm::Register>,
81    #[allow(dead_code)]
82    pub(crate) compiled_paths: Option<&'a HashMap<String, crate::metrics_context::CompiledPath>>,
83    pub(crate) instruction_data: Option<&'a serde_json::Value>,
84    pub(crate) slot: Option<u64>,
85    pub(crate) signature: Option<String>,
86    pub(crate) timestamp: Option<i64>,
87    // Track which fields were modified by imperative hooks
88    pub(crate) dirty_fields: std::collections::HashSet<String>,
89}
90
91pub trait ReverseLookupUpdater {
92    fn update(
93        &mut self,
94        pda_address: String,
95        seed_value: String,
96    ) -> Vec<crate::vm::PendingAccountUpdate>;
97    fn flush_pending(&mut self, pda_address: &str) -> Vec<crate::vm::PendingAccountUpdate>;
98}
99
100impl<'a> InstructionContext<'a> {
101    /// Create a new InstructionContext (primarily for use by generated code)
102    pub fn new(
103        accounts: HashMap<String, String>,
104        state_id: u32,
105        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
106    ) -> Self {
107        Self {
108            accounts,
109            state_id,
110            reverse_lookup_tx,
111            pending_updates: Vec::new(),
112            registers: None,
113            state_reg: None,
114            compiled_paths: None,
115            instruction_data: None,
116            slot: None,
117            signature: None,
118            timestamp: None,
119            dirty_fields: std::collections::HashSet::new(),
120        }
121    }
122
123    /// Create InstructionContext with metrics support
124    #[allow(clippy::too_many_arguments)]
125    pub fn with_metrics(
126        accounts: HashMap<String, String>,
127        state_id: u32,
128        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
129        registers: &'a mut Vec<crate::vm::RegisterValue>,
130        state_reg: crate::vm::Register,
131        compiled_paths: &'a HashMap<String, crate::metrics_context::CompiledPath>,
132        instruction_data: &'a serde_json::Value,
133        slot: Option<u64>,
134        signature: Option<String>,
135        timestamp: i64,
136    ) -> Self {
137        Self {
138            accounts,
139            state_id,
140            reverse_lookup_tx,
141            pending_updates: Vec::new(),
142            registers: Some(registers),
143            state_reg: Some(state_reg),
144            compiled_paths: Some(compiled_paths),
145            instruction_data: Some(instruction_data),
146            slot,
147            signature,
148            timestamp: Some(timestamp),
149            dirty_fields: std::collections::HashSet::new(),
150        }
151    }
152
153    /// Get an account address by its name from the instruction
154    pub fn account(&self, name: &str) -> Option<String> {
155        self.accounts.get(name).cloned()
156    }
157
158    /// Register a reverse lookup: PDA address -> seed value
159    /// This also flushes any pending account updates for this PDA
160    ///
161    /// The pending account updates are accumulated internally and can be retrieved
162    /// via `take_pending_updates()` after all hooks have executed.
163    pub fn register_pda_reverse_lookup(&mut self, pda_address: &str, seed_value: &str) {
164        tracing::info!(
165            "📝 Registering PDA reverse lookup: {} -> {}",
166            pda_address,
167            seed_value
168        );
169        let pending = self
170            .reverse_lookup_tx
171            .update(pda_address.to_string(), seed_value.to_string());
172        if !pending.is_empty() {
173            tracing::info!(
174                "   🔄 Flushed {} pending account update(s) for this PDA",
175                pending.len()
176            );
177        }
178        self.pending_updates.extend(pending);
179    }
180
181    /// Take all accumulated pending updates
182    ///
183    /// This should be called after all instruction hooks have executed to retrieve
184    /// any pending account updates that need to be reprocessed.
185    pub fn take_pending_updates(&mut self) -> Vec<crate::vm::PendingAccountUpdate> {
186        std::mem::take(&mut self.pending_updates)
187    }
188
189    /// Get the fields modified by imperative hooks
190    pub fn dirty_fields(&self) -> &std::collections::HashSet<String> {
191        &self.dirty_fields
192    }
193
194    /// Get the current state register value (for generating mutations)
195    pub fn state_value(&self) -> Option<&serde_json::Value> {
196        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
197            Some(&registers[state_reg])
198        } else {
199            None
200        }
201    }
202
203    /// Get a field value from the entity state
204    /// This allows reading aggregated values or other entity fields
205    pub fn get<T: serde::de::DeserializeOwned>(&self, field_path: &str) -> Option<T> {
206        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
207            let state = &registers[state_reg];
208            self.get_nested_value(state, field_path)
209                .and_then(|v| serde_json::from_value(v.clone()).ok())
210        } else {
211            None
212        }
213    }
214
215    /// Set a field value in the entity state
216    pub fn set<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
217        if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
218            let serialized = serde_json::to_value(value).ok();
219            if let Some(val) = serialized {
220                Self::set_nested_value_static(&mut registers[state_reg], field_path, val);
221                // Track this field as dirty so it gets included in mutations
222                self.dirty_fields.insert(field_path.to_string());
223                println!("      ✓ Set field '{}' and marked as dirty", field_path);
224            }
225        } else {
226            println!("      ⚠️  Cannot set field '{}': metrics not configured (registers={}, state_reg={:?})", 
227                field_path, self.registers.is_some(), self.state_reg);
228        }
229    }
230
231    /// Increment a numeric field (creates it as 0 if it doesn't exist)
232    pub fn increment(&mut self, field_path: &str, amount: i64) {
233        let current = self.get::<i64>(field_path).unwrap_or(0);
234        self.set(field_path, current + amount);
235    }
236
237    /// Helper to get nested value from JSON
238    fn get_nested_value<'b>(
239        &self,
240        value: &'b serde_json::Value,
241        path: &str,
242    ) -> Option<&'b serde_json::Value> {
243        let mut current = value;
244        for segment in path.split('.') {
245            current = current.get(segment)?;
246        }
247        Some(current)
248    }
249
250    /// Helper to set nested value in JSON
251    fn set_nested_value_static(
252        value: &mut serde_json::Value,
253        path: &str,
254        new_value: serde_json::Value,
255    ) {
256        let segments: Vec<&str> = path.split('.').collect();
257        if segments.is_empty() {
258            return;
259        }
260
261        // Navigate to parent
262        let mut current = value;
263        for segment in &segments[..segments.len() - 1] {
264            if !current.is_object() {
265                *current = serde_json::json!({});
266            }
267            let obj = current.as_object_mut().unwrap();
268            current = obj
269                .entry(segment.to_string())
270                .or_insert(serde_json::json!({}));
271        }
272
273        // Set final value
274        if !current.is_object() {
275            *current = serde_json::json!({});
276        }
277        if let Some(obj) = current.as_object_mut() {
278            obj.insert(segments[segments.len() - 1].to_string(), new_value);
279        }
280    }
281
282    /// Access instruction data field
283    pub fn data<T: serde::de::DeserializeOwned>(&self, field: &str) -> Option<T> {
284        self.instruction_data
285            .and_then(|data| data.get(field))
286            .and_then(|v| serde_json::from_value(v.clone()).ok())
287    }
288
289    /// Get the current timestamp
290    pub fn timestamp(&self) -> i64 {
291        self.timestamp.unwrap_or(0)
292    }
293
294    /// Get the current slot
295    pub fn slot(&self) -> Option<u64> {
296        self.slot
297    }
298
299    /// Get the current signature
300    pub fn signature(&self) -> Option<&str> {
301        self.signature.as_deref()
302    }
303}