Skip to main content

arete_interpreter/
ast.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::marker::PhantomData;
5
6pub use arete_idl::snapshot::*;
7
8/// Current AST version for SerializableStreamSpec and SerializableStackSpec
9///
10/// ⚠️ IMPORTANT: This constant is duplicated in arete-macros/src/ast/types.rs due to
11/// circular dependency between proc-macro crates and their output crates.
12/// When bumping this version, you MUST also update the constant in the
13/// arete-macros crate. A test in versioned.rs verifies they stay in sync.
14// 0.0.2: enum variants may carry `fields` (data-carrying enum variants).
15// 0.0.3: field metadata may carry integer-kind and raw/canonical name hints.
16// 0.0.4: instruction args may carry amount-hint metadata.
17// Additive over 0.0.1 — every older file deserializes unchanged.
18pub const CURRENT_AST_VERSION: &str = "0.0.4";
19
20/// Older versions this build can still deserialize directly (all changes
21/// since were additive with serde defaults).
22pub const COMPATIBLE_AST_VERSIONS: &[&str] = &["0.0.1", "0.0.2", "0.0.3"];
23
24fn default_ast_version() -> String {
25    CURRENT_AST_VERSION.to_string()
26}
27
28pub fn idl_type_snapshot_to_rust_string(ty: &IdlTypeSnapshot) -> String {
29    match ty {
30        IdlTypeSnapshot::Simple(s) => map_simple_idl_type(s),
31        IdlTypeSnapshot::Array(arr) => {
32            if arr.array.len() == 2 {
33                match (&arr.array[0], &arr.array[1]) {
34                    (IdlArrayElementSnapshot::TypeName(t), IdlArrayElementSnapshot::Size(size)) => {
35                        format!("[{}; {}]", map_simple_idl_type(t), size)
36                    }
37                    (
38                        IdlArrayElementSnapshot::Type(nested),
39                        IdlArrayElementSnapshot::Size(size),
40                    ) => {
41                        format!("[{}; {}]", idl_type_snapshot_to_rust_string(nested), size)
42                    }
43                    _ => "Vec<u8>".to_string(),
44                }
45            } else {
46                "Vec<u8>".to_string()
47            }
48        }
49        IdlTypeSnapshot::Option(opt) => {
50            format!("Option<{}>", idl_type_snapshot_to_rust_string(&opt.option))
51        }
52        IdlTypeSnapshot::Vec(vec) => {
53            format!("Vec<{}>", idl_type_snapshot_to_rust_string(&vec.vec))
54        }
55        IdlTypeSnapshot::HashMap(map) => {
56            let key_type = idl_type_snapshot_to_rust_string(&map.hash_map.0);
57            let val_type = idl_type_snapshot_to_rust_string(&map.hash_map.1);
58            format!("std::collections::HashMap<{}, {}>", key_type, val_type)
59        }
60        IdlTypeSnapshot::Defined(def) => match &def.defined {
61            IdlDefinedInnerSnapshot::Named { name } => name.clone(),
62            IdlDefinedInnerSnapshot::Simple(s) => s.clone(),
63        },
64    }
65}
66
67fn map_simple_idl_type(idl_type: &str) -> String {
68    match idl_type {
69        "u8" => "u8".to_string(),
70        "u16" => "u16".to_string(),
71        "u32" => "u32".to_string(),
72        "u64" => "u64".to_string(),
73        "u128" => "u128".to_string(),
74        "i8" => "i8".to_string(),
75        "i16" => "i16".to_string(),
76        "i32" => "i32".to_string(),
77        "i64" => "i64".to_string(),
78        "i128" => "i128".to_string(),
79        "bool" => "bool".to_string(),
80        "string" => "String".to_string(),
81        "publicKey" | "pubkey" => "solana_pubkey::Pubkey".to_string(),
82        "bytes" => "Vec<u8>".to_string(),
83        _ => idl_type.to_string(),
84    }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct FieldPath {
89    pub segments: Vec<String>,
90    pub offsets: Option<Vec<usize>>,
91}
92
93impl FieldPath {
94    pub fn new(segments: &[&str]) -> Self {
95        FieldPath {
96            segments: segments.iter().map(|s| s.to_string()).collect(),
97            offsets: None,
98        }
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub enum Transformation {
104    HexEncode,
105    HexDecode,
106    Base58Encode,
107    Base58Decode,
108    ToString,
109    ToNumber,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum PopulationStrategy {
114    SetOnce,
115    LastWrite,
116    Append,
117    Merge,
118    Max,
119    /// Sum numeric values (accumulator pattern for aggregations)
120    Sum,
121    /// Count occurrences (increments by 1 for each update)
122    Count,
123    /// Track minimum value
124    Min,
125    /// Track unique values and store the count
126    /// Internally maintains a HashSet, exposes only the count
127    UniqueCount,
128}
129
130// ============================================================================
131// Computed Field Expression AST
132// ============================================================================
133
134/// Specification for a computed/derived field
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ComputedFieldSpec {
137    /// Target field path (e.g., "trading.total_volume")
138    pub target_path: String,
139    /// Expression AST
140    pub expression: ComputedExpr,
141    /// Result type (e.g., "Option<u64>", "Option<f64>")
142    pub result_type: String,
143}
144
145// ============================================================================
146// Resolver Specifications
147// ============================================================================
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
150#[serde(rename_all = "lowercase")]
151pub enum ResolverType {
152    Token,
153    Url(UrlResolverConfig),
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
157#[serde(rename_all = "lowercase")]
158pub enum HttpMethod {
159    #[default]
160    Get,
161    Post,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
165pub enum UrlTemplatePart {
166    Literal(String),
167    FieldRef(String),
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
171pub enum UrlSource {
172    FieldPath(String),
173    Template(Vec<UrlTemplatePart>),
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
177pub struct UrlResolverConfig {
178    pub url_source: UrlSource,
179    #[serde(default)]
180    pub method: HttpMethod,
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub extract_path: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct ResolverExtractSpec {
187    pub target_path: String,
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub source_path: Option<String>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub transform: Option<Transformation>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
195pub enum ResolveStrategy {
196    #[default]
197    SetOnce,
198    LastWrite,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct ResolverCondition {
203    pub field_path: String,
204    pub op: ComparisonOp,
205    pub value: Value,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ResolverSpec {
210    pub resolver: ResolverType,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub input_path: Option<String>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub input_value: Option<Value>,
215    #[serde(default)]
216    pub strategy: ResolveStrategy,
217    pub extracts: Vec<ResolverExtractSpec>,
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub condition: Option<ResolverCondition>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub schedule_at: Option<String>,
222}
223
224/// AST for computed field expressions
225/// Supports a subset of Rust expressions needed for computed fields:
226/// - Field references (possibly from other sections)
227/// - Unwrap with defaults
228/// - Basic arithmetic and comparisons
229/// - Type casts
230/// - Method calls
231/// - Let bindings and conditionals
232/// - Byte array operations
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum ComputedExpr {
235    // Existing variants
236    /// Reference to a field: "field_name" or "section.field_name"
237    FieldRef {
238        path: String,
239    },
240
241    /// Unwrap with default: expr.unwrap_or(default)
242    UnwrapOr {
243        expr: Box<ComputedExpr>,
244        default: serde_json::Value,
245    },
246
247    /// Binary operation: left op right
248    Binary {
249        op: BinaryOp,
250        left: Box<ComputedExpr>,
251        right: Box<ComputedExpr>,
252    },
253
254    /// Type cast: expr as type
255    Cast {
256        expr: Box<ComputedExpr>,
257        to_type: String,
258    },
259
260    /// Method call: expr.method(args)
261    MethodCall {
262        expr: Box<ComputedExpr>,
263        method: String,
264        args: Vec<ComputedExpr>,
265    },
266
267    /// Computation provided by a resolver
268    ResolverComputed {
269        resolver: String,
270        method: String,
271        args: Vec<ComputedExpr>,
272    },
273
274    /// Literal value: numbers, booleans, strings
275    Literal {
276        value: serde_json::Value,
277    },
278
279    /// Parenthesized expression for grouping
280    Paren {
281        expr: Box<ComputedExpr>,
282    },
283
284    // Variable reference (for let bindings)
285    Var {
286        name: String,
287    },
288
289    // Let binding: let name = value; body
290    Let {
291        name: String,
292        value: Box<ComputedExpr>,
293        body: Box<ComputedExpr>,
294    },
295
296    // Conditional: if condition { then_branch } else { else_branch }
297    If {
298        condition: Box<ComputedExpr>,
299        then_branch: Box<ComputedExpr>,
300        else_branch: Box<ComputedExpr>,
301    },
302
303    // Option constructors
304    None,
305    Some {
306        value: Box<ComputedExpr>,
307    },
308
309    // Byte/array operations
310    Slice {
311        expr: Box<ComputedExpr>,
312        start: usize,
313        end: usize,
314    },
315    Index {
316        expr: Box<ComputedExpr>,
317        index: usize,
318    },
319
320    // Byte conversion functions
321    U64FromLeBytes {
322        bytes: Box<ComputedExpr>,
323    },
324    U64FromBeBytes {
325        bytes: Box<ComputedExpr>,
326    },
327
328    // Byte array literals: [0u8; 32] or [1, 2, 3]
329    ByteArray {
330        bytes: Vec<u8>,
331    },
332
333    // Closure for map operations: |x| body
334    Closure {
335        param: String,
336        body: Box<ComputedExpr>,
337    },
338
339    // Unary operations
340    Unary {
341        op: UnaryOp,
342        expr: Box<ComputedExpr>,
343    },
344
345    // JSON array to bytes conversion (for working with captured byte arrays)
346    JsonToBytes {
347        expr: Box<ComputedExpr>,
348    },
349
350    // Context access - slot and timestamp from the update that triggered evaluation
351    /// Access the slot number from the current update context
352    ContextSlot,
353    /// Access the unix timestamp from the current update context
354    ContextTimestamp,
355
356    /// Keccak256 hash function for computing Ethereum-compatible hashes
357    /// Takes a byte array expression and returns the 32-byte hash as a Vec<u8>
358    Keccak256 {
359        expr: Box<ComputedExpr>,
360    },
361}
362
363/// Binary operators for computed expressions
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub enum BinaryOp {
366    // Arithmetic
367    Add,
368    Sub,
369    Mul,
370    Div,
371    Mod,
372    // Comparison
373    Gt,
374    Lt,
375    Gte,
376    Lte,
377    Eq,
378    Ne,
379    // Logical
380    And,
381    Or,
382    // Bitwise
383    Xor,
384    BitAnd,
385    BitOr,
386    Shl,
387    Shr,
388}
389
390/// Unary operators for computed expressions
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub enum UnaryOp {
393    Not,
394    ReverseBits,
395}
396
397/// Serializable version of StreamSpec without phantom types
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct SerializableStreamSpec {
400    /// AST schema version for backward compatibility
401    /// Uses semver format (e.g., "0.0.1")
402    #[serde(default = "default_ast_version")]
403    pub ast_version: String,
404    pub state_name: String,
405    /// Program ID (Solana address) - extracted from IDL
406    #[serde(default)]
407    pub program_id: Option<String>,
408    /// Embedded IDL for AST-only compilation
409    #[serde(default)]
410    pub idl: Option<IdlSnapshot>,
411    pub identity: IdentitySpec,
412    pub handlers: Vec<SerializableHandlerSpec>,
413    pub sections: Vec<EntitySection>,
414    pub field_mappings: BTreeMap<String, FieldTypeInfo>,
415    pub resolver_hooks: Vec<ResolverHook>,
416    pub instruction_hooks: Vec<InstructionHook>,
417    #[serde(default)]
418    pub resolver_specs: Vec<ResolverSpec>,
419    /// Computed field paths (legacy, for backward compatibility)
420    #[serde(default)]
421    pub computed_fields: Vec<String>,
422    /// Computed field specifications with full expression AST
423    #[serde(default)]
424    pub computed_field_specs: Vec<ComputedFieldSpec>,
425    /// Deterministic content hash (SHA256 of canonical JSON, excluding this field)
426    /// Used for deduplication and version tracking
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub content_hash: Option<String>,
429    /// View definitions for derived/projected views
430    #[serde(default)]
431    pub views: Vec<ViewDef>,
432}
433
434impl SerializableStreamSpec {
435    /// Normalize legacy event names emitted by older AST generators.
436    pub fn normalize_event_names(&mut self) {
437        for handler in &mut self.handlers {
438            handler.normalize_event_names();
439        }
440
441        for hook in &mut self.instruction_hooks {
442            hook.instruction_type =
443                crate::event_type_helpers::canonicalize_event_type_name(&hook.instruction_type);
444        }
445    }
446}
447
448#[derive(Debug, Clone)]
449pub struct TypedStreamSpec<S> {
450    pub state_name: String,
451    pub identity: IdentitySpec,
452    pub handlers: Vec<TypedHandlerSpec<S>>,
453    pub sections: Vec<EntitySection>, // NEW: Complete structural information
454    pub field_mappings: BTreeMap<String, FieldTypeInfo>, // NEW: All field type info by target path
455    pub resolver_hooks: Vec<ResolverHook>, // NEW: Resolver hooks for PDA key resolution
456    pub instruction_hooks: Vec<InstructionHook>, // NEW: Instruction hooks for PDA registration
457    pub resolver_specs: Vec<ResolverSpec>,
458    pub computed_fields: Vec<String>, // List of computed field paths
459    _phantom: PhantomData<S>,
460}
461
462impl<S> TypedStreamSpec<S> {
463    pub fn new(
464        state_name: String,
465        identity: IdentitySpec,
466        handlers: Vec<TypedHandlerSpec<S>>,
467    ) -> Self {
468        TypedStreamSpec {
469            state_name,
470            identity,
471            handlers,
472            sections: Vec::new(),
473            field_mappings: BTreeMap::new(),
474            resolver_hooks: Vec::new(),
475            instruction_hooks: Vec::new(),
476            resolver_specs: Vec::new(),
477            computed_fields: Vec::new(),
478            _phantom: PhantomData,
479        }
480    }
481
482    /// Enhanced constructor with type information
483    pub fn with_type_info(
484        state_name: String,
485        identity: IdentitySpec,
486        handlers: Vec<TypedHandlerSpec<S>>,
487        sections: Vec<EntitySection>,
488        field_mappings: BTreeMap<String, FieldTypeInfo>,
489    ) -> Self {
490        TypedStreamSpec {
491            state_name,
492            identity,
493            handlers,
494            sections,
495            field_mappings,
496            resolver_hooks: Vec::new(),
497            instruction_hooks: Vec::new(),
498            resolver_specs: Vec::new(),
499            computed_fields: Vec::new(),
500            _phantom: PhantomData,
501        }
502    }
503
504    pub fn with_resolver_specs(mut self, resolver_specs: Vec<ResolverSpec>) -> Self {
505        self.resolver_specs = resolver_specs;
506        self
507    }
508
509    /// Get type information for a specific field path
510    pub fn get_field_type(&self, path: &str) -> Option<&FieldTypeInfo> {
511        self.field_mappings.get(path)
512    }
513
514    /// Get all fields for a specific section
515    pub fn get_section_fields(&self, section_name: &str) -> Option<&Vec<FieldTypeInfo>> {
516        self.sections
517            .iter()
518            .find(|s| s.name == section_name)
519            .map(|s| &s.fields)
520    }
521
522    /// Get all section names
523    pub fn get_section_names(&self) -> Vec<&String> {
524        self.sections.iter().map(|s| &s.name).collect()
525    }
526
527    /// Convert to serializable format
528    pub fn to_serializable(&self) -> SerializableStreamSpec {
529        let mut spec = SerializableStreamSpec {
530            ast_version: CURRENT_AST_VERSION.to_string(),
531            state_name: self.state_name.clone(),
532            program_id: None,
533            idl: None,
534            identity: self.identity.clone(),
535            handlers: self.handlers.iter().map(|h| h.to_serializable()).collect(),
536            sections: self.sections.clone(),
537            field_mappings: self.field_mappings.clone(),
538            resolver_hooks: self.resolver_hooks.clone(),
539            instruction_hooks: self.instruction_hooks.clone(),
540            resolver_specs: self.resolver_specs.clone(),
541            computed_fields: self.computed_fields.clone(),
542            computed_field_specs: Vec::new(),
543            content_hash: None,
544            views: Vec::new(),
545        };
546        spec.content_hash = Some(spec.compute_content_hash());
547        spec
548    }
549
550    /// Create from serializable format
551    pub fn from_serializable(mut spec: SerializableStreamSpec) -> Self {
552        spec.normalize_event_names();
553        TypedStreamSpec {
554            state_name: spec.state_name,
555            identity: spec.identity,
556            handlers: spec
557                .handlers
558                .into_iter()
559                .map(|h| TypedHandlerSpec::from_serializable(h))
560                .collect(),
561            sections: spec.sections,
562            field_mappings: spec.field_mappings,
563            resolver_hooks: spec.resolver_hooks,
564            instruction_hooks: spec.instruction_hooks,
565            resolver_specs: spec.resolver_specs,
566            computed_fields: spec.computed_fields,
567            _phantom: PhantomData,
568        }
569    }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct IdentitySpec {
574    pub primary_keys: Vec<String>,
575    pub lookup_indexes: Vec<LookupIndexSpec>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct LookupIndexSpec {
580    pub field_name: String,
581    pub temporal_field: Option<String>,
582}
583
584// ============================================================================
585// Level 1: Declarative Hook Extensions
586// ============================================================================
587
588/// Declarative resolver hook specification
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ResolverHook {
591    /// Account type this resolver applies to (e.g., "BondingCurveState")
592    pub account_type: String,
593
594    /// Resolution strategy
595    pub strategy: ResolverStrategy,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub enum ResolverStrategy {
600    /// Look up PDA in reverse lookup table, queue if not found
601    PdaReverseLookup {
602        lookup_name: String,
603        /// Instruction discriminators to queue until (8 bytes each)
604        queue_discriminators: Vec<Vec<u8>>,
605    },
606
607    /// Extract primary key directly from account data (future)
608    DirectField { field_path: FieldPath },
609}
610
611/// Declarative instruction hook specification
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct InstructionHook {
614    /// Instruction type this hook applies to (e.g., "CreateIxState")
615    pub instruction_type: String,
616
617    /// Actions to perform when this instruction is processed
618    pub actions: Vec<HookAction>,
619
620    /// Lookup strategy for finding the entity
621    pub lookup_by: Option<FieldPath>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub enum HookAction {
626    /// Register a PDA mapping for reverse lookup
627    RegisterPdaMapping {
628        pda_field: FieldPath,
629        seed_field: FieldPath,
630        lookup_name: String,
631    },
632
633    /// Set a field value (for #[track_from])
634    SetField {
635        target_field: String,
636        source: MappingSource,
637        condition: Option<ConditionExpr>,
638    },
639
640    /// Increment a field value (for conditional aggregations)
641    IncrementField {
642        target_field: String,
643        increment_by: i64,
644        condition: Option<ConditionExpr>,
645    },
646}
647
648/// Simple condition expression (Level 1 - basic comparisons only)
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct ConditionExpr {
651    /// Expression as string (will be parsed and validated)
652    pub expression: String,
653
654    /// Parsed representation (for validation and execution)
655    pub parsed: Option<ParsedCondition>,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
659pub enum ParsedCondition {
660    /// Binary comparison: field op value
661    Comparison {
662        field: FieldPath,
663        op: ComparisonOp,
664        value: serde_json::Value,
665    },
666
667    /// Logical AND/OR
668    Logical {
669        op: LogicalOp,
670        conditions: Vec<ParsedCondition>,
671    },
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
675pub enum ComparisonOp {
676    Equal,
677    NotEqual,
678    GreaterThan,
679    GreaterThanOrEqual,
680    LessThan,
681    LessThanOrEqual,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub enum LogicalOp {
686    And,
687    Or,
688}
689
690/// Serializable version of HandlerSpec without phantom types
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct SerializableHandlerSpec {
693    pub source: SourceSpec,
694    pub key_resolution: KeyResolutionStrategy,
695    pub mappings: Vec<SerializableFieldMapping>,
696    pub conditions: Vec<Condition>,
697    pub emit: bool,
698}
699
700impl SerializableHandlerSpec {
701    pub fn normalize_event_names(&mut self) {
702        let SourceSpec::Source { type_name, .. } = &mut self.source;
703        *type_name = crate::event_type_helpers::canonicalize_event_type_name(type_name);
704
705        for mapping in &mut self.mappings {
706            if let Some(when) = &mut mapping.when {
707                *when = crate::event_type_helpers::canonicalize_event_type_name(when);
708            }
709            if let Some(stop) = &mut mapping.stop {
710                *stop = crate::event_type_helpers::canonicalize_event_type_name(stop);
711            }
712        }
713    }
714}
715
716#[derive(Debug, Clone)]
717pub struct TypedHandlerSpec<S> {
718    pub source: SourceSpec,
719    pub key_resolution: KeyResolutionStrategy,
720    pub mappings: Vec<TypedFieldMapping<S>>,
721    pub conditions: Vec<Condition>,
722    pub emit: bool,
723    _phantom: PhantomData<S>,
724}
725
726impl<S> TypedHandlerSpec<S> {
727    pub fn new(
728        source: SourceSpec,
729        key_resolution: KeyResolutionStrategy,
730        mappings: Vec<TypedFieldMapping<S>>,
731        emit: bool,
732    ) -> Self {
733        TypedHandlerSpec {
734            source,
735            key_resolution,
736            mappings,
737            conditions: vec![],
738            emit,
739            _phantom: PhantomData,
740        }
741    }
742
743    /// Convert to serializable format
744    pub fn to_serializable(&self) -> SerializableHandlerSpec {
745        SerializableHandlerSpec {
746            source: self.source.clone(),
747            key_resolution: self.key_resolution.clone(),
748            mappings: self.mappings.iter().map(|m| m.to_serializable()).collect(),
749            conditions: self.conditions.clone(),
750            emit: self.emit,
751        }
752    }
753
754    /// Create from serializable format
755    pub fn from_serializable(spec: SerializableHandlerSpec) -> Self {
756        TypedHandlerSpec {
757            source: spec.source,
758            key_resolution: spec.key_resolution,
759            mappings: spec
760                .mappings
761                .into_iter()
762                .map(|m| TypedFieldMapping::from_serializable(m))
763                .collect(),
764            conditions: spec.conditions,
765            emit: spec.emit,
766            _phantom: PhantomData,
767        }
768    }
769}
770
771#[derive(Debug, Clone, Serialize, Deserialize)]
772pub enum KeyResolutionStrategy {
773    Embedded {
774        primary_field: FieldPath,
775    },
776    Lookup {
777        primary_field: FieldPath,
778    },
779    Computed {
780        primary_field: FieldPath,
781        compute_partition: ComputeFunction,
782    },
783    TemporalLookup {
784        lookup_field: FieldPath,
785        timestamp_field: FieldPath,
786        index_name: String,
787    },
788}
789
790#[derive(Debug, Clone, Serialize, Deserialize)]
791pub enum SourceSpec {
792    Source {
793        program_id: Option<String>,
794        discriminator: Option<Vec<u8>>,
795        type_name: String,
796        #[serde(default, skip_serializing_if = "Option::is_none")]
797        serialization: Option<IdlSerializationSnapshot>,
798        /// True when this handler listens to an account-state event (not an
799        /// instruction or custom event).  Set at code-generation time from
800        /// the structural source kind so the compiler does not need to rely
801        /// on naming-convention heuristics.
802        #[serde(default)]
803        is_account: bool,
804    },
805}
806
807/// Serializable version of FieldMapping without phantom types
808#[derive(Debug, Clone, Serialize, Deserialize)]
809pub struct SerializableFieldMapping {
810    pub target_path: String,
811    pub source: MappingSource,
812    pub transform: Option<Transformation>,
813    pub population: PopulationStrategy,
814    #[serde(default, skip_serializing_if = "Option::is_none")]
815    pub condition: Option<ConditionExpr>,
816    #[serde(default, skip_serializing_if = "Option::is_none")]
817    pub when: Option<String>,
818    #[serde(default, skip_serializing_if = "Option::is_none")]
819    pub stop: Option<String>,
820    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
821    pub emit: bool,
822}
823
824fn default_emit() -> bool {
825    true
826}
827
828fn default_instruction_discriminant_size() -> usize {
829    8
830}
831
832fn is_true(value: &bool) -> bool {
833    *value
834}
835
836#[derive(Debug, Clone)]
837pub struct TypedFieldMapping<S> {
838    pub target_path: String,
839    pub source: MappingSource,
840    pub transform: Option<Transformation>,
841    pub population: PopulationStrategy,
842    pub condition: Option<ConditionExpr>,
843    pub when: Option<String>,
844    pub stop: Option<String>,
845    pub emit: bool,
846    _phantom: PhantomData<S>,
847}
848
849impl<S> TypedFieldMapping<S> {
850    pub fn new(target_path: String, source: MappingSource, population: PopulationStrategy) -> Self {
851        TypedFieldMapping {
852            target_path,
853            source,
854            transform: None,
855            population,
856            condition: None,
857            when: None,
858            stop: None,
859            emit: true,
860            _phantom: PhantomData,
861        }
862    }
863
864    pub fn with_transform(mut self, transform: Transformation) -> Self {
865        self.transform = Some(transform);
866        self
867    }
868
869    pub fn with_condition(mut self, condition: ConditionExpr) -> Self {
870        self.condition = Some(condition);
871        self
872    }
873
874    pub fn with_when(mut self, when: String) -> Self {
875        self.when = Some(when);
876        self
877    }
878
879    pub fn with_stop(mut self, stop: String) -> Self {
880        self.stop = Some(stop);
881        self
882    }
883
884    pub fn with_emit(mut self, emit: bool) -> Self {
885        self.emit = emit;
886        self
887    }
888
889    /// Convert to serializable format
890    pub fn to_serializable(&self) -> SerializableFieldMapping {
891        SerializableFieldMapping {
892            target_path: self.target_path.clone(),
893            source: self.source.clone(),
894            transform: self.transform.clone(),
895            population: self.population.clone(),
896            condition: self.condition.clone(),
897            when: self.when.clone(),
898            stop: self.stop.clone(),
899            emit: self.emit,
900        }
901    }
902
903    /// Create from serializable format
904    pub fn from_serializable(mapping: SerializableFieldMapping) -> Self {
905        TypedFieldMapping {
906            target_path: mapping.target_path,
907            source: mapping.source,
908            transform: mapping.transform,
909            population: mapping.population,
910            condition: mapping.condition,
911            when: mapping.when,
912            stop: mapping.stop,
913            emit: mapping.emit,
914            _phantom: PhantomData,
915        }
916    }
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920pub enum MappingSource {
921    FromSource {
922        path: FieldPath,
923        default: Option<Value>,
924        transform: Option<Transformation>,
925    },
926    Constant(Value),
927    Computed {
928        inputs: Vec<FieldPath>,
929        function: ComputeFunction,
930    },
931    FromState {
932        path: String,
933    },
934    AsEvent {
935        fields: Vec<Box<MappingSource>>,
936    },
937    WholeSource,
938    /// Similar to WholeSource but with field-level transformations
939    /// Used by #[capture] macro to apply transforms to specific fields in an account
940    AsCapture {
941        field_transforms: BTreeMap<String, Transformation>,
942    },
943    /// From instruction context (timestamp, slot, signature)
944    /// Used by #[track_from] with special fields like __timestamp
945    FromContext {
946        field: String,
947    },
948}
949
950impl MappingSource {
951    pub fn with_transform(self, transform: Transformation) -> Self {
952        match self {
953            MappingSource::FromSource {
954                path,
955                default,
956                transform: _,
957            } => MappingSource::FromSource {
958                path,
959                default,
960                transform: Some(transform),
961            },
962            other => other,
963        }
964    }
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
968pub enum ComputeFunction {
969    Sum,
970    Concat,
971    Format(String),
972    Custom(String),
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct Condition {
977    pub field: FieldPath,
978    pub operator: ConditionOp,
979    pub value: Value,
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
983pub enum ConditionOp {
984    Equals,
985    NotEquals,
986    GreaterThan,
987    LessThan,
988    Contains,
989    Exists,
990}
991
992/// Language-agnostic type information for fields
993#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct FieldTypeInfo {
995    pub field_name: String,
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub raw_name: Option<String>,
998    #[serde(default, skip_serializing_if = "Option::is_none")]
999    pub canonical_name: Option<String>,
1000    pub rust_type_name: String, // Full Rust type: "Option<i64>", "Vec<Value>", etc.
1001    pub base_type: BaseType,    // Fundamental type classification
1002    #[serde(default, skip_serializing_if = "Option::is_none")]
1003    pub integer_kind: Option<IntegerKind>,
1004    pub is_optional: bool,           // true for Option<T>
1005    pub is_array: bool,              // true for Vec<T>
1006    pub inner_type: Option<String>,  // For Option<T> or Vec<T>, store the inner type
1007    pub source_path: Option<String>, // Path to source field if this is mapped
1008    /// Resolved type information for complex types (instructions, accounts, custom types)
1009    #[serde(default)]
1010    pub resolved_type: Option<ResolvedStructType>,
1011    #[serde(default = "default_emit", skip_serializing_if = "is_true")]
1012    pub emit: bool,
1013}
1014
1015/// Resolved structure type with field information from IDL
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct ResolvedStructType {
1018    pub type_name: String,
1019    pub fields: Vec<ResolvedField>,
1020    pub is_instruction: bool,
1021    pub is_account: bool,
1022    pub is_event: bool,
1023    /// If true, this is an enum type and enum_variants should be used instead of fields
1024    #[serde(default)]
1025    pub is_enum: bool,
1026    /// For enum types, list of variant names
1027    #[serde(default)]
1028    pub enum_variants: Vec<String>,
1029}
1030
1031/// A resolved field within a complex type
1032#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct ResolvedField {
1034    pub field_name: String,
1035    #[serde(default, skip_serializing_if = "Option::is_none")]
1036    pub raw_name: Option<String>,
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub canonical_name: Option<String>,
1039    pub field_type: String,
1040    pub base_type: BaseType,
1041    #[serde(default, skip_serializing_if = "Option::is_none")]
1042    pub integer_kind: Option<IntegerKind>,
1043    pub is_optional: bool,
1044    pub is_array: bool,
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1048pub enum IntegerKind {
1049    U8,
1050    U16,
1051    U32,
1052    U64,
1053    U128,
1054    Usize,
1055    I8,
1056    I16,
1057    I32,
1058    I64,
1059    I128,
1060    Isize,
1061}
1062
1063impl IntegerKind {
1064    pub fn from_rust_type(type_str: &str) -> Option<Self> {
1065        match type_str.trim() {
1066            "u8" => Some(Self::U8),
1067            "u16" => Some(Self::U16),
1068            "u32" => Some(Self::U32),
1069            "u64" => Some(Self::U64),
1070            "u128" => Some(Self::U128),
1071            "usize" => Some(Self::Usize),
1072            "i8" => Some(Self::I8),
1073            "i16" => Some(Self::I16),
1074            "i32" => Some(Self::I32),
1075            "i64" => Some(Self::I64),
1076            "i128" => Some(Self::I128),
1077            "isize" => Some(Self::Isize),
1078            _ => None,
1079        }
1080    }
1081
1082    pub fn is_bigint(self) -> bool {
1083        matches!(self, Self::U64 | Self::U128 | Self::I64 | Self::I128)
1084    }
1085}
1086
1087/// Language-agnostic base type classification
1088#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1089pub enum BaseType {
1090    // Numeric types
1091    Integer, // i8, i16, i32, i64, u8, u16, u32, u64, usize, isize
1092    Float,   // f32, f64
1093    // Text types
1094    String, // String, &str
1095    // Boolean
1096    Boolean, // bool
1097    // Complex types
1098    Object, // Custom structs, HashMap, etc.
1099    Array,  // Vec<T>, arrays
1100    Binary, // Bytes, binary data
1101    // Special types
1102    Timestamp, // Detected from field names ending in _at, _time, etc.
1103    Pubkey,    // Solana public key (Base58 encoded)
1104    Any,       // serde_json::Value, unknown types
1105}
1106
1107/// Represents a logical section/group of fields in the entity
1108#[derive(Debug, Clone, Serialize, Deserialize)]
1109pub struct EntitySection {
1110    pub name: String,
1111    pub fields: Vec<FieldTypeInfo>,
1112    pub is_nested_struct: bool,
1113    pub parent_field: Option<String>, // If this section comes from a nested struct field
1114}
1115
1116impl FieldTypeInfo {
1117    pub fn new(field_name: String, rust_type_name: String) -> Self {
1118        let (base_type, integer_kind, is_optional, is_array, inner_type) =
1119            Self::analyze_rust_type(&rust_type_name);
1120        let canonical_name = to_camel_case_owned(&field_name);
1121
1122        FieldTypeInfo {
1123            field_name: field_name.clone(),
1124            raw_name: Some(field_name.clone()),
1125            canonical_name: Some(canonical_name),
1126            rust_type_name,
1127            base_type: Self::infer_semantic_type(&field_name, base_type),
1128            integer_kind,
1129            is_optional,
1130            is_array,
1131            inner_type,
1132            source_path: None,
1133            resolved_type: None,
1134            emit: true,
1135        }
1136    }
1137
1138    pub fn with_source_path(mut self, source_path: String) -> Self {
1139        self.source_path = Some(source_path);
1140        self
1141    }
1142
1143    pub fn raw_field_name(&self) -> &str {
1144        self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1145    }
1146
1147    pub fn canonical_field_name(&self) -> String {
1148        self.canonical_name
1149            .clone()
1150            .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1151    }
1152
1153    pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1154        self.integer_kind.or_else(|| {
1155            IntegerKind::from_rust_type(
1156                self.inner_type
1157                    .as_deref()
1158                    .unwrap_or(self.rust_type_name.as_str()),
1159            )
1160        })
1161    }
1162
1163    /// Analyze a Rust type string and extract structural information
1164    fn analyze_rust_type(
1165        rust_type: &str,
1166    ) -> (BaseType, Option<IntegerKind>, bool, bool, Option<String>) {
1167        let type_str = rust_type.trim();
1168
1169        // Handle Option<T>
1170        if let Some(inner) = Self::extract_generic_inner(type_str, "Option") {
1171            let (inner_base_type, inner_integer_kind, _, inner_is_array, inner_inner_type) =
1172                Self::analyze_rust_type(&inner);
1173            return (
1174                inner_base_type,
1175                inner_integer_kind,
1176                true,
1177                inner_is_array,
1178                inner_inner_type.or(Some(inner)),
1179            );
1180        }
1181
1182        // Handle Vec<T>
1183        if let Some(inner) = Self::extract_generic_inner(type_str, "Vec") {
1184            let (_inner_base_type, inner_integer_kind, inner_is_optional, _, inner_inner_type) =
1185                Self::analyze_rust_type(&inner);
1186            return (
1187                BaseType::Array,
1188                inner_integer_kind,
1189                inner_is_optional,
1190                true,
1191                inner_inner_type.or(Some(inner)),
1192            );
1193        }
1194
1195        // Handle primitive types
1196        let integer_kind = IntegerKind::from_rust_type(type_str);
1197        let base_type = match integer_kind {
1198            Some(_) => BaseType::Integer,
1199            None => match type_str {
1200                "f32" | "f64" => BaseType::Float,
1201                "bool" => BaseType::Boolean,
1202                "String" | "&str" | "str" => BaseType::String,
1203                "Value" | "serde_json::Value" => BaseType::Any,
1204                "Pubkey" | "solana_pubkey::Pubkey" => BaseType::Pubkey,
1205                _ => {
1206                    // Check for binary types
1207                    if type_str.contains("Bytes") || type_str.contains("bytes") {
1208                        BaseType::Binary
1209                    } else if type_str.contains("Pubkey") {
1210                        BaseType::Pubkey
1211                    } else {
1212                        BaseType::Object
1213                    }
1214                }
1215            },
1216        };
1217
1218        (base_type, integer_kind, false, false, None)
1219    }
1220
1221    /// Extract inner type from generic like "Option<T>" -> "T"
1222    fn extract_generic_inner(type_str: &str, generic_name: &str) -> Option<String> {
1223        let pattern = format!("{}<", generic_name);
1224        if type_str.starts_with(&pattern) && type_str.ends_with('>') {
1225            let start = pattern.len();
1226            let end = type_str.len() - 1;
1227            if end > start {
1228                return Some(type_str[start..end].trim().to_string());
1229            }
1230        }
1231        None
1232    }
1233
1234    /// Infer semantic type based on field name patterns
1235    fn infer_semantic_type(field_name: &str, base_type: BaseType) -> BaseType {
1236        let lower_name = field_name.to_lowercase();
1237
1238        // If already classified as integer, check if it should be timestamp
1239        if base_type == BaseType::Integer
1240            && (lower_name.ends_with("_at")
1241                || lower_name.ends_with("_time")
1242                || lower_name.contains("timestamp")
1243                || lower_name.contains("created")
1244                || lower_name.contains("settled")
1245                || lower_name.contains("activated"))
1246        {
1247            return BaseType::Timestamp;
1248        }
1249
1250        base_type
1251    }
1252}
1253
1254impl ResolvedField {
1255    pub fn raw_field_name(&self) -> &str {
1256        self.raw_name.as_deref().unwrap_or(self.field_name.as_str())
1257    }
1258
1259    pub fn canonical_field_name(&self) -> String {
1260        self.canonical_name
1261            .clone()
1262            .unwrap_or_else(|| to_camel_case_owned(self.raw_field_name()))
1263    }
1264
1265    pub fn effective_integer_kind(&self) -> Option<IntegerKind> {
1266        self.integer_kind
1267            .or_else(|| IntegerKind::from_rust_type(self.field_type.as_str()))
1268    }
1269}
1270
1271fn to_camel_case_owned(s: &str) -> String {
1272    let mut result = String::new();
1273    let mut uppercase_next = false;
1274
1275    for ch in s.chars() {
1276        if matches!(ch, '_' | '-' | '.') {
1277            uppercase_next = true;
1278            continue;
1279        }
1280
1281        if result.is_empty() {
1282            result.extend(ch.to_lowercase());
1283            continue;
1284        }
1285
1286        if uppercase_next {
1287            result.extend(ch.to_uppercase());
1288            uppercase_next = false;
1289        } else {
1290            result.push(ch);
1291        }
1292    }
1293
1294    result
1295}
1296
1297pub trait FieldAccessor<S> {
1298    fn path(&self) -> String;
1299}
1300
1301// ============================================================================
1302// SerializableStreamSpec Implementation
1303// ============================================================================
1304
1305impl SerializableStreamSpec {
1306    /// Compute deterministic content hash (SHA256 of canonical JSON).
1307    ///
1308    /// The hash is computed over the entire spec except the content_hash field itself,
1309    /// ensuring the same AST always produces the same hash regardless of when it was
1310    /// generated or by whom.
1311    pub fn compute_content_hash(&self) -> String {
1312        use sha2::{Digest, Sha256};
1313
1314        // Clone and clear the hash field for computation
1315        let mut spec_for_hash = self.clone();
1316        spec_for_hash.content_hash = None;
1317
1318        // Serialize to JSON (serde_json produces consistent output for the same struct)
1319        let json =
1320            serde_json::to_string(&spec_for_hash).expect("Failed to serialize spec for hashing");
1321
1322        // Compute SHA256 hash
1323        let mut hasher = Sha256::new();
1324        hasher.update(json.as_bytes());
1325        let result = hasher.finalize();
1326
1327        // Return hex-encoded hash
1328        hex::encode(result)
1329    }
1330
1331    /// Verify that the content_hash matches the computed hash.
1332    /// Returns true if hash is valid or not set.
1333    pub fn verify_content_hash(&self) -> bool {
1334        match &self.content_hash {
1335            Some(hash) => {
1336                let computed = self.compute_content_hash();
1337                hash == &computed
1338            }
1339            None => true, // No hash to verify
1340        }
1341    }
1342
1343    /// Set the content_hash field to the computed hash.
1344    pub fn with_content_hash(mut self) -> Self {
1345        self.content_hash = Some(self.compute_content_hash());
1346        self
1347    }
1348}
1349
1350// ============================================================================
1351// PDA and Instruction Types — For SDK code generation
1352// ============================================================================
1353
1354/// PDA (Program-Derived Address) definition for the stack-level registry.
1355/// PDAs defined here can be referenced by instructions via `pdaRef`.
1356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1357pub struct PdaDefinition {
1358    /// Human-readable name (e.g., "miner", "bondingCurve")
1359    pub name: String,
1360
1361    /// Seeds for PDA derivation, in order
1362    pub seeds: Vec<PdaSeedDef>,
1363
1364    /// Program ID that owns this PDA.
1365    /// If None, uses the stack's primary programId.
1366    #[serde(default, skip_serializing_if = "Option::is_none")]
1367    pub program_id: Option<String>,
1368}
1369
1370/// Single seed in a PDA derivation.
1371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1372#[serde(tag = "type", rename_all = "camelCase")]
1373pub enum PdaSeedDef {
1374    /// Static string seed: "miner" → "miner".as_bytes()
1375    Literal { value: String },
1376
1377    /// Static byte array (for non-UTF8 seeds)
1378    Bytes { value: Vec<u8> },
1379
1380    /// Reference to an instruction argument: arg("roundId") → args.roundId as bytes
1381    ArgRef {
1382        arg_name: String,
1383        /// Optional type hint for serialization (e.g., "u64", "pubkey")
1384        #[serde(default, skip_serializing_if = "Option::is_none")]
1385        arg_type: Option<String>,
1386    },
1387
1388    /// Reference to another account in the instruction: account("mint") → accounts.mint pubkey
1389    AccountRef { account_name: String },
1390}
1391
1392/// How an instruction account's address is determined.
1393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1394#[serde(tag = "category", rename_all = "camelCase")]
1395pub enum AccountResolution {
1396    /// Must sign the transaction (uses wallet.publicKey)
1397    Signer,
1398
1399    /// Fixed known address (e.g., System Program, Token Program)
1400    Known { address: String },
1401
1402    /// Reference to a PDA in the stack's pdas registry
1403    PdaRef { pda_name: String },
1404
1405    /// Inline PDA definition (for one-off PDAs not in the registry)
1406    PdaInline {
1407        seeds: Vec<PdaSeedDef>,
1408        #[serde(default, skip_serializing_if = "Option::is_none")]
1409        program_id: Option<String>,
1410    },
1411
1412    /// User must provide at call time via options.accounts
1413    UserProvided,
1414}
1415
1416/// Account metadata for an instruction.
1417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1418pub struct InstructionAccountDef {
1419    /// Account name (e.g., "user", "mint", "bondingCurve")
1420    pub name: String,
1421
1422    /// Whether this account must sign the transaction
1423    #[serde(default)]
1424    pub is_signer: bool,
1425
1426    /// Whether this account is writable
1427    #[serde(default)]
1428    pub is_writable: bool,
1429
1430    /// How this account's address is resolved
1431    pub resolution: AccountResolution,
1432
1433    /// Whether this account can be omitted (optional accounts)
1434    #[serde(default)]
1435    pub is_optional: bool,
1436
1437    /// Documentation from IDL
1438    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1439    pub docs: Vec<String>,
1440}
1441
1442/// Argument definition for an instruction.
1443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1444#[serde(rename_all = "camelCase")]
1445pub struct InstructionAmountHint {
1446    pub decimals_source: AmountDecimalsSource,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1450#[serde(
1451    tag = "kind",
1452    rename_all = "camelCase",
1453    rename_all_fields = "camelCase"
1454)]
1455pub enum AmountDecimalsSource {
1456    ArgMint { arg_name: String },
1457    ArgDecimals { arg_name: String },
1458    KnownAccount { account_name: String },
1459    Constant { decimals: u8 },
1460}
1461
1462/// Argument definition for an instruction.
1463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1464pub struct InstructionArgDef {
1465    /// Argument name
1466    pub name: String,
1467
1468    /// Type from IDL (e.g., "u64", "bool", "pubkey", "Option<u64>")
1469    #[serde(rename = "type")]
1470    pub arg_type: String,
1471
1472    /// Documentation from IDL
1473    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1474    pub docs: Vec<String>,
1475
1476    /// Optional amount-resolution metadata for semantic SDK wrappers.
1477    #[serde(default, skip_serializing_if = "Option::is_none")]
1478    pub amount_hint: Option<InstructionAmountHint>,
1479}
1480
1481/// Full instruction definition in the AST.
1482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1483pub struct InstructionDef {
1484    /// Instruction name (e.g., "buy", "sell", "automate")
1485    pub name: String,
1486
1487    /// Discriminator bytes (8 bytes for Anchor, 1 byte for Steel)
1488    pub discriminator: Vec<u8>,
1489
1490    /// Size of discriminator in bytes (for buffer allocation)
1491    #[serde(default = "default_instruction_discriminant_size")]
1492    pub discriminator_size: usize,
1493
1494    /// Accounts required by this instruction, in order
1495    pub accounts: Vec<InstructionAccountDef>,
1496
1497    /// Arguments for this instruction, in order
1498    pub args: Vec<InstructionArgDef>,
1499
1500    /// Error definitions specific to this instruction
1501    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1502    pub errors: Vec<IdlErrorSnapshot>,
1503
1504    /// Program ID for this instruction (usually same as stack's programId)
1505    #[serde(default, skip_serializing_if = "Option::is_none")]
1506    pub program_id: Option<String>,
1507
1508    /// Documentation from IDL
1509    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1510    pub docs: Vec<String>,
1511}
1512
1513// ============================================================================
1514// Stack Spec — Unified multi-entity AST format
1515// ============================================================================
1516
1517/// A unified stack specification containing all entities.
1518/// Written to `.arete/{StackName}.stack.json`.
1519#[derive(Debug, Clone, Serialize, Deserialize)]
1520pub struct SerializableStackSpec {
1521    /// AST schema version for backward compatibility
1522    /// Uses semver format (e.g., "0.0.1")
1523    #[serde(default = "default_ast_version")]
1524    pub ast_version: String,
1525    /// Stack name (PascalCase, derived from module ident)
1526    pub stack_name: String,
1527    /// Program IDs (one per IDL, in order)
1528    #[serde(default)]
1529    pub program_ids: Vec<String>,
1530    /// IDL snapshots (one per program)
1531    #[serde(default)]
1532    pub idls: Vec<IdlSnapshot>,
1533    /// All entity specifications in this stack
1534    pub entities: Vec<SerializableStreamSpec>,
1535    /// PDA registry - defines all PDAs for the stack, grouped by program name
1536    /// Outer key is program name (e.g., "ore", "entropy"), inner key is PDA name
1537    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1538    pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1539    /// Instruction definitions for SDK code generation
1540    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1541    pub instructions: Vec<InstructionDef>,
1542    /// Deterministic content hash of the entire stack
1543    #[serde(default, skip_serializing_if = "Option::is_none")]
1544    pub content_hash: Option<String>,
1545}
1546
1547impl SerializableStackSpec {
1548    /// Normalize legacy event names emitted by older AST generators.
1549    pub fn normalize_event_names(&mut self) {
1550        for entity in &mut self.entities {
1551            entity.normalize_event_names();
1552        }
1553    }
1554
1555    /// Compute deterministic content hash (SHA256 of canonical JSON).
1556    pub fn compute_content_hash(&self) -> String {
1557        use sha2::{Digest, Sha256};
1558        let mut spec_for_hash = self.clone();
1559        spec_for_hash.content_hash = None;
1560        let json = serde_json::to_string(&spec_for_hash)
1561            .expect("Failed to serialize stack spec for hashing");
1562        let mut hasher = Sha256::new();
1563        hasher.update(json.as_bytes());
1564        hex::encode(hasher.finalize())
1565    }
1566
1567    pub fn with_content_hash(mut self) -> Self {
1568        self.content_hash = Some(self.compute_content_hash());
1569        self
1570    }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575    use super::{
1576        HookAction, IdentitySpec, InstructionHook, MappingSource, PopulationStrategy,
1577        SerializableFieldMapping, SerializableHandlerSpec, SerializableStackSpec,
1578        SerializableStreamSpec, SourceSpec, TypedStreamSpec, CURRENT_AST_VERSION,
1579    };
1580    use serde_json::Value;
1581
1582    fn legacy_stream_spec() -> SerializableStreamSpec {
1583        SerializableStreamSpec {
1584            ast_version: CURRENT_AST_VERSION.to_string(),
1585            state_name: "PumpfunToken".to_string(),
1586            program_id: None,
1587            idl: None,
1588            identity: IdentitySpec {
1589                primary_keys: vec!["id.mint".to_string()],
1590                lookup_indexes: vec![],
1591            },
1592            handlers: vec![SerializableHandlerSpec {
1593                source: SourceSpec::Source {
1594                    program_id: None,
1595                    discriminator: None,
1596                    type_name: "pump::buyIxState".to_string(),
1597                    serialization: None,
1598                    is_account: false,
1599                },
1600                key_resolution: super::KeyResolutionStrategy::Embedded {
1601                    primary_field: super::FieldPath::new(&["accounts", "mint"]),
1602                },
1603                mappings: vec![SerializableFieldMapping {
1604                    target_path: "info.last_buy_at".to_string(),
1605                    source: MappingSource::Constant(Value::Null),
1606                    transform: None,
1607                    population: PopulationStrategy::SetOnce,
1608                    condition: None,
1609                    when: Some("pump::sellIxState".to_string()),
1610                    stop: Some("pump::buy_exact_sol_inIxState".to_string()),
1611                    emit: true,
1612                }],
1613                conditions: vec![],
1614                emit: true,
1615            }],
1616            sections: vec![],
1617            field_mappings: Default::default(),
1618            resolver_hooks: vec![],
1619            instruction_hooks: vec![InstructionHook {
1620                instruction_type: "pump::buyIxState".to_string(),
1621                actions: vec![HookAction::SetField {
1622                    target_field: "info.last_buy_at".to_string(),
1623                    source: MappingSource::Constant(Value::Null),
1624                    condition: None,
1625                }],
1626                lookup_by: None,
1627            }],
1628            resolver_specs: vec![],
1629            computed_fields: vec![],
1630            computed_field_specs: vec![],
1631            content_hash: None,
1632            views: vec![],
1633        }
1634    }
1635
1636    #[test]
1637    fn typed_stream_spec_from_serializable_normalizes_legacy_instruction_event_names() {
1638        let typed = TypedStreamSpec::<Value>::from_serializable(legacy_stream_spec());
1639
1640        let handler = &typed.handlers[0];
1641        let SourceSpec::Source { type_name, .. } = &handler.source;
1642        assert_eq!(type_name, "pump::BuyIxState");
1643        assert_eq!(
1644            handler.mappings[0].when.as_deref(),
1645            Some("pump::SellIxState")
1646        );
1647        assert_eq!(
1648            handler.mappings[0].stop.as_deref(),
1649            Some("pump::BuyExactSolInIxState")
1650        );
1651        assert_eq!(
1652            typed.instruction_hooks[0].instruction_type,
1653            "pump::BuyIxState"
1654        );
1655    }
1656
1657    #[test]
1658    fn stack_spec_normalize_event_names_updates_all_entities() {
1659        let mut stack = SerializableStackSpec {
1660            ast_version: CURRENT_AST_VERSION.to_string(),
1661            stack_name: "PumpStack".to_string(),
1662            program_ids: vec![],
1663            idls: vec![],
1664            entities: vec![legacy_stream_spec()],
1665            pdas: Default::default(),
1666            instructions: vec![],
1667            content_hash: None,
1668        };
1669
1670        stack.normalize_event_names();
1671
1672        let SourceSpec::Source { type_name, .. } = &stack.entities[0].handlers[0].source;
1673        assert_eq!(type_name, "pump::BuyIxState");
1674        assert_eq!(
1675            stack.entities[0].instruction_hooks[0].instruction_type,
1676            "pump::BuyIxState"
1677        );
1678    }
1679
1680    #[test]
1681    fn field_type_info_recognizes_128_bit_integers() {
1682        let unsigned = super::FieldTypeInfo::new("amount".to_string(), "u128".to_string());
1683        assert_eq!(unsigned.base_type, super::BaseType::Integer);
1684        assert!(!unsigned.is_optional);
1685        assert_eq!(unsigned.integer_kind, Some(super::IntegerKind::U128));
1686        assert_eq!(unsigned.raw_field_name(), "amount");
1687        assert_eq!(unsigned.canonical_field_name(), "amount");
1688
1689        let signed = super::FieldTypeInfo::new("delta".to_string(), "Option<i128>".to_string());
1690        assert_eq!(signed.base_type, super::BaseType::Integer);
1691        assert!(signed.is_optional);
1692        assert_eq!(signed.integer_kind, Some(super::IntegerKind::I128));
1693        assert_eq!(signed.inner_type.as_deref(), Some("i128"));
1694    }
1695
1696    #[test]
1697    fn field_type_info_preserves_timestamp_integer_kind_and_names() {
1698        let timestamp =
1699            super::FieldTypeInfo::new("last_updated_at".to_string(), "Option<i64>".to_string());
1700
1701        assert_eq!(timestamp.base_type, super::BaseType::Timestamp);
1702        assert_eq!(timestamp.integer_kind, Some(super::IntegerKind::I64));
1703        assert!(timestamp.is_optional);
1704        assert_eq!(timestamp.raw_field_name(), "last_updated_at");
1705        assert_eq!(timestamp.canonical_field_name(), "lastUpdatedAt");
1706    }
1707}
1708
1709// ============================================================================
1710// View Pipeline Types - Composable View Definitions
1711// ============================================================================
1712
1713/// Sort order for view transforms
1714#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1715#[serde(rename_all = "lowercase")]
1716pub enum SortOrder {
1717    #[default]
1718    Asc,
1719    Desc,
1720}
1721
1722/// Comparison operators for predicates
1723#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1724pub enum CompareOp {
1725    Eq,
1726    Ne,
1727    Gt,
1728    Gte,
1729    Lt,
1730    Lte,
1731}
1732
1733/// Value in a predicate comparison
1734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1735pub enum PredicateValue {
1736    /// Literal JSON value
1737    Literal(serde_json::Value),
1738    /// Dynamic runtime value (e.g., "now()" for current timestamp)
1739    Dynamic(String),
1740    /// Reference to another field
1741    Field(FieldPath),
1742}
1743
1744/// Predicate for filtering entities
1745#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1746pub enum Predicate {
1747    /// Field comparison: field op value
1748    Compare {
1749        field: FieldPath,
1750        op: CompareOp,
1751        value: PredicateValue,
1752    },
1753    /// Logical AND of predicates
1754    And(Vec<Predicate>),
1755    /// Logical OR of predicates
1756    Or(Vec<Predicate>),
1757    /// Negation
1758    Not(Box<Predicate>),
1759    /// Field exists (is not null)
1760    Exists { field: FieldPath },
1761}
1762
1763/// Transform operation in a view pipeline
1764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1765pub enum ViewTransform {
1766    /// Filter entities matching a predicate
1767    Filter { predicate: Predicate },
1768
1769    /// Sort entities by a field
1770    Sort {
1771        key: FieldPath,
1772        #[serde(default)]
1773        order: SortOrder,
1774    },
1775
1776    /// Take first N entities (after sort)
1777    Take { count: usize },
1778
1779    /// Skip first N entities
1780    Skip { count: usize },
1781
1782    /// Take only the first entity (after sort) - produces Single output
1783    First,
1784
1785    /// Take only the last entity (after sort) - produces Single output
1786    Last,
1787
1788    /// Get entity with maximum value for field - produces Single output
1789    MaxBy { key: FieldPath },
1790
1791    /// Get entity with minimum value for field - produces Single output
1792    MinBy { key: FieldPath },
1793}
1794
1795/// Source for a view definition
1796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1797pub enum ViewSource {
1798    /// Derive directly from entity mutations
1799    Entity { name: String },
1800    /// Derive from another view's output
1801    View { id: String },
1802}
1803
1804/// Output mode for a view
1805#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1806pub enum ViewOutput {
1807    /// Multiple entities (list-like semantics)
1808    #[default]
1809    Collection,
1810    /// Single entity (state-like semantics)
1811    Single,
1812    /// Keyed lookup by a specific field
1813    Keyed { key_field: FieldPath },
1814}
1815
1816/// Definition of a view in the pipeline
1817#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1818pub struct ViewDef {
1819    /// Unique view identifier (e.g., "OreRound/latest")
1820    pub id: String,
1821
1822    /// Source this view derives from
1823    pub source: ViewSource,
1824
1825    /// Pipeline of transforms to apply (in order)
1826    #[serde(default)]
1827    pub pipeline: Vec<ViewTransform>,
1828
1829    /// Output mode for this view
1830    #[serde(default)]
1831    pub output: ViewOutput,
1832}
1833
1834impl ViewDef {
1835    /// Create a new list view for an entity
1836    pub fn list(entity_name: &str) -> Self {
1837        ViewDef {
1838            id: format!("{}/list", entity_name),
1839            source: ViewSource::Entity {
1840                name: entity_name.to_string(),
1841            },
1842            pipeline: vec![],
1843            output: ViewOutput::Collection,
1844        }
1845    }
1846
1847    /// Create a new state view for an entity
1848    pub fn state(entity_name: &str, key_field: &[&str]) -> Self {
1849        ViewDef {
1850            id: format!("{}/state", entity_name),
1851            source: ViewSource::Entity {
1852                name: entity_name.to_string(),
1853            },
1854            pipeline: vec![],
1855            output: ViewOutput::Keyed {
1856                key_field: FieldPath::new(key_field),
1857            },
1858        }
1859    }
1860
1861    /// Check if this view produces a single entity
1862    pub fn is_single(&self) -> bool {
1863        matches!(self.output, ViewOutput::Single)
1864    }
1865
1866    /// Check if any transform in the pipeline produces a single result
1867    pub fn has_single_transform(&self) -> bool {
1868        self.pipeline.iter().any(|t| {
1869            matches!(
1870                t,
1871                ViewTransform::First
1872                    | ViewTransform::Last
1873                    | ViewTransform::MaxBy { .. }
1874                    | ViewTransform::MinBy { .. }
1875            )
1876        })
1877    }
1878}
1879
1880#[macro_export]
1881macro_rules! define_accessor {
1882    ($name:ident, $state:ty, $path:expr) => {
1883        pub struct $name;
1884
1885        impl $crate::ast::FieldAccessor<$state> for $name {
1886            fn path(&self) -> String {
1887                $path.to_string()
1888            }
1889        }
1890    };
1891}