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