Skip to main content

hyperstack_interpreter/
ast.rs

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