Skip to main content

arete_interpreter/
rust.rs

1use crate::ast::*;
2use crate::typescript_instructions::{
3    dedupe_errors_by_code, normalize_seed_arg_type, split_generic,
4};
5use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
6
7#[derive(Debug, Clone)]
8pub struct RustOutput {
9    pub cargo_toml: String,
10    pub lib_rs: String,
11    pub types_rs: String,
12    pub entity_rs: String,
13    /// Generated program SDK module (`programs.rs`). `None` when the stack
14    /// spec declares no instructions.
15    pub programs_rs: Option<String>,
16}
17
18/// Rust output for a standalone program SDK.
19///
20/// Unlike [`RustOutput`], this deliberately has no entity/view module or
21/// generated `Stack` implementation. The exported
22/// aggregate implements `arete_sdk::ProgramSdk`, so it can be connected on
23/// its own or added to a session alongside live stacks.
24#[derive(Debug, Clone)]
25pub struct RustProgramOutput {
26    pub cargo_toml: String,
27    pub lib_rs: String,
28    pub types_rs: String,
29    pub programs_rs: String,
30}
31
32impl RustOutput {
33    pub fn full_lib(&self) -> String {
34        let mut output = format!(
35            "{}\n\n// types.rs\n{}\n\n// entity.rs\n{}",
36            self.lib_rs, self.types_rs, self.entity_rs
37        );
38        if let Some(programs) = &self.programs_rs {
39            output.push_str("\n\n// programs.rs\n");
40            output.push_str(programs);
41        }
42        output
43    }
44
45    pub fn mod_rs(&self) -> String {
46        self.lib_rs.clone()
47    }
48}
49
50#[derive(Debug, Clone)]
51pub struct RustConfig {
52    pub crate_name: String,
53    pub sdk_version: String,
54    pub module_mode: bool,
55    /// WebSocket URL for the stack. If None, generates a placeholder comment.
56    pub url: Option<String>,
57}
58
59impl Default for RustConfig {
60    fn default() -> Self {
61        Self {
62            crate_name: "generated-stack".to_string(),
63            sdk_version: "0.4".to_string(),
64            module_mode: false,
65            url: None,
66        }
67    }
68}
69
70pub fn compile_serializable_spec(
71    spec: SerializableStreamSpec,
72    entity_name: String,
73    config: Option<RustConfig>,
74) -> Result<RustOutput, String> {
75    let config = config.unwrap_or_default();
76    let compiler = RustCompiler::new(spec, entity_name, config);
77    Ok(compiler.compile())
78}
79
80pub fn write_rust_crate(
81    output: &RustOutput,
82    crate_dir: &std::path::Path,
83) -> Result<(), std::io::Error> {
84    std::fs::create_dir_all(crate_dir.join("src"))?;
85    std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
86    std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
87    std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
88    std::fs::write(crate_dir.join("src/entity.rs"), &output.entity_rs)?;
89    if let Some(programs) = &output.programs_rs {
90        std::fs::write(crate_dir.join("src/programs.rs"), programs)?;
91    }
92    Ok(())
93}
94
95pub fn write_rust_module(
96    output: &RustOutput,
97    module_dir: &std::path::Path,
98) -> Result<(), std::io::Error> {
99    std::fs::create_dir_all(module_dir)?;
100    std::fs::write(module_dir.join("mod.rs"), output.mod_rs())?;
101    std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
102    std::fs::write(module_dir.join("entity.rs"), &output.entity_rs)?;
103    if let Some(programs) = &output.programs_rs {
104        std::fs::write(module_dir.join("programs.rs"), programs)?;
105    }
106    Ok(())
107}
108
109pub fn write_rust_program_crate(
110    output: &RustProgramOutput,
111    crate_dir: &std::path::Path,
112) -> Result<(), std::io::Error> {
113    std::fs::create_dir_all(crate_dir.join("src"))?;
114    std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
115    std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
116    std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
117    std::fs::write(crate_dir.join("src/programs.rs"), &output.programs_rs)?;
118    Ok(())
119}
120
121pub fn write_rust_program_module(
122    output: &RustProgramOutput,
123    module_dir: &std::path::Path,
124) -> Result<(), std::io::Error> {
125    std::fs::create_dir_all(module_dir)?;
126    std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
127    std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
128    std::fs::write(module_dir.join("programs.rs"), &output.programs_rs)?;
129    Ok(())
130}
131
132/// Runtime envelope a resolved-struct field arrives in. Mirror of the
133/// TypeScript generator's `EventWrapper<T>` / `CaptureWrapper<T>` selection in
134/// `field_type_info_to_typescript` (and of `python::WrapperKind`).
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub(crate) enum WrapperKind {
137    None,
138    Capture,
139    Event,
140}
141
142/// Target paths fed by an `AsCapture` mapping. Mirror of the TypeScript
143/// generator's `is_capture_field` and of `python::capture_field_targets`:
144/// those fields arrive wrapped in a `CaptureWrapper` envelope
145/// (`{timestamp, account_address, data, slot?, signature?}`) rather than as
146/// the bare account struct.
147pub(crate) fn capture_field_targets(spec: &SerializableStreamSpec) -> HashSet<String> {
148    let mut targets = HashSet::new();
149    for handler in &spec.handlers {
150        for mapping in &handler.mappings {
151            if matches!(&mapping.source, MappingSource::AsCapture { .. }) {
152                targets.insert(mapping.target_path.clone());
153            }
154        }
155    }
156    targets
157}
158
159/// Which runtime envelope a resolved-struct field arrives in. Mirror of the
160/// TypeScript generator: `#[capture]`-fed account fields arrive as
161/// `CaptureWrapper<T>` and event/instruction-list fields as `EventWrapper<T>`,
162/// never as the bare struct.
163pub(crate) fn wrapper_kind_for(
164    field: &FieldTypeInfo,
165    resolved: &ResolvedStructType,
166    capture_fields: &HashSet<String>,
167) -> WrapperKind {
168    if resolved.is_event || (resolved.is_instruction && field.is_array) {
169        return WrapperKind::Event;
170    }
171    if resolved.is_account
172        && (capture_fields.contains(&field.field_name)
173            || capture_fields.contains(field.raw_field_name()))
174    {
175        return WrapperKind::Capture;
176    }
177    WrapperKind::None
178}
179
180/// The runtime envelopes every generated `types.rs` carries. Mirrors
181/// `arete_interpreter::{EventWrapper, CaptureWrapper}` and the TypeScript
182/// `EventWrapper<T>` / `CaptureWrapper<T>` interfaces: capture/event-fed fields
183/// arrive wrapped on the wire, so the generated field types name the envelope
184/// and expose the provenance (`timestamp`, `account_address`, `slot`,
185/// `signature`) alongside the decoded `data`.
186const WRAPPER_TYPES: &str = r#"/// Wrapper for event data that includes context metadata.
187/// Events are automatically wrapped in this structure at runtime.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct EventWrapper<T> {
190    /// Unix timestamp when the event was processed.
191    #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
192    pub timestamp: i64,
193    /// The event-specific data.
194    pub data: T,
195    /// Optional blockchain slot number.
196    #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")]
197    pub slot: Option<u64>,
198    /// Optional transaction signature.
199    #[serde(default)]
200    pub signature: Option<String>,
201}
202
203impl<T: Default> Default for EventWrapper<T> {
204    fn default() -> Self {
205        Self {
206            timestamp: 0,
207            data: T::default(),
208            slot: None,
209            signature: None,
210        }
211    }
212}
213
214/// Wrapper for account data captured with `#[capture]`, including context
215/// metadata. Captured accounts are automatically wrapped in this structure at
216/// runtime.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct CaptureWrapper<T> {
219    /// Unix timestamp when the account was captured.
220    #[serde(default, deserialize_with = "serde_utils::deserialize_i64")]
221    pub timestamp: i64,
222    /// The account address (base58 encoded public key).
223    #[serde(default)]
224    pub account_address: String,
225    /// The captured account data.
226    pub data: T,
227    /// Optional blockchain slot number.
228    #[serde(default, deserialize_with = "serde_utils::deserialize_option_u64")]
229    pub slot: Option<u64>,
230    /// Optional transaction signature.
231    #[serde(default)]
232    pub signature: Option<String>,
233}
234
235impl<T: Default> Default for CaptureWrapper<T> {
236    fn default() -> Self {
237        Self {
238            timestamp: 0,
239            account_address: String::new(),
240            data: T::default(),
241            slot: None,
242            signature: None,
243        }
244    }
245}
246"#;
247
248/// Rust definitions for the builtin resolver output types a generated SDK can
249/// name, in emission order. Mirror of the `typescript_interface()` blocks the
250/// resolvers in [`crate::resolvers`] register: `SlotHashBytes` (the
251/// `{ bytes }` wire shape of `ResolvedSlotHash`) and `TokenMetadata`. Field
252/// names are the snake_case wire keys the runtime emits, so no rename
253/// attributes are needed.
254///
255/// `KeccakRngValue` is deliberately absent even though it is a registered
256/// resolver output type. It is a `u64`, and TypeScript models it as
257/// `export type KeccakRngValue = string` only because the canonical numeric
258/// rule (docs/internal/sdk-core-api.md ยง2) puts `u64` on the wire as a decimal
259/// string. Rust decodes that back to a real `u64` via
260/// `serde_utils::deserialize_option_*_u64`, so `KeccakRngValue`-typed fields
261/// keep their integer typing instead of degrading to `String`.
262const BUILTIN_RESOLVER_STRUCTS: &[(&str, &str)] = &[
263    (
264        "SlotHashBytes",
265        r#"/// Slot hash resolved by the builtin `SlotHash` resolver.
266#[derive(Debug, Clone, Serialize, Deserialize, Default)]
267pub struct SlotHashBytes {
268    /// 32-byte slot hash.
269    #[serde(default)]
270    pub bytes: Vec<u8>,
271}"#,
272    ),
273    (
274        "TokenMetadata",
275        r#"/// Token metadata resolved by the builtin `TokenMetadata` resolver.
276#[derive(Debug, Clone, Serialize, Deserialize, Default)]
277pub struct TokenMetadata {
278    #[serde(default)]
279    pub mint: String,
280    #[serde(default)]
281    pub name: Option<String>,
282    #[serde(default)]
283    pub symbol: Option<String>,
284    #[serde(default)]
285    pub decimals: Option<u8>,
286    #[serde(default)]
287    pub logo_uri: Option<String>,
288}"#,
289    ),
290];
291
292/// The generated struct name for a builtin resolver output type named by a
293/// field's `inner_type`, or `None`. The registry is the authority (mirror of
294/// `typescript::is_builtin_resolver_type`), narrowed to the types
295/// [`BUILTIN_RESOLVER_STRUCTS`] can express as a Rust struct.
296pub(crate) fn builtin_resolver_struct(inner_type: Option<&str>) -> Option<&'static str> {
297    let inner = inner_type?;
298    if !crate::resolvers::is_resolver_output_type(inner) {
299        return None;
300    }
301    BUILTIN_RESOLVER_STRUCTS
302        .iter()
303        .find(|(name, _)| *name == inner)
304        .map(|(name, _)| *name)
305}
306
307/// Render the builtin resolver structs a generated `types.rs` actually
308/// references. Each block is followed by a blank line.
309fn render_builtin_resolver_structs(used: &BTreeSet<&'static str>) -> String {
310    let mut output = String::new();
311    for (name, definition) in BUILTIN_RESOLVER_STRUCTS {
312        if used.contains(name) {
313            output.push_str(definition);
314            output.push_str("\n\n");
315        }
316    }
317    output
318}
319
320/// Map the element of a `Vec<T>` scalar array to its Rust primitive. Mirror of
321/// `typescript::typescript_scalar_array_element` and
322/// `python::py_scalar_array_element`; accepts both stored forms of the inner
323/// type (`"Vec < f64 >"` and the bare `"f64"`) and returns `None` for
324/// non-scalar elements.
325fn rust_scalar_array_element(inner_type: &str) -> Option<&'static str> {
326    let trimmed = inner_type.trim();
327    let element = trimmed
328        .strip_prefix("Vec <")
329        .and_then(|rest| rest.strip_suffix('>'))
330        .or_else(|| {
331            trimmed
332                .strip_prefix("Vec<")
333                .and_then(|rest| rest.strip_suffix('>'))
334        })
335        .map(str::trim)
336        .unwrap_or(trimmed);
337    match element {
338        "f32" | "f64" => Some("f64"),
339        "bool" => Some("bool"),
340        "String" | "&str" | "str" => Some("String"),
341        _ => None,
342    }
343}
344
345/// Rust type plus `serde_utils` requirement for a non-resolved (scalar /
346/// scalar-array) field. Type and `#[serde(...)]` attribute are derived from
347/// one place so an integer vector can never be typed `Vec<u64>` while its
348/// deserializer stays scalar (or vice versa).
349struct RustScalarShape {
350    /// The bare Rust type, before the patch `Option<..>` wrapping.
351    rust_type: String,
352    /// Normalized integer kind whose `serde_utils` deserializer this field
353    /// needs, or `None` for a plain `#[serde(default)]`.
354    integer_kind: Option<&'static str>,
355    /// Whether that deserializer must be the `_vec_` variant.
356    is_vec: bool,
357}
358
359/// Shape of a non-resolved field. Shared by the entity-section path and the
360/// IDL `ResolvedField` path so the same on-chain array is typed identically
361/// whichever way it is reached (mirror of `python::py_scalar_field_shape`).
362///
363/// `Vec<u64>`-shaped fields are stored as `BaseType::Array` with an explicit
364/// `integer_kind`, so the integer check has to consult `integer_kind` and not
365/// just `base_type`. The guard stays tighter than the TypeScript one
366/// (`BaseType::Array` only, never "any field carrying an `integer_kind`") so
367/// `BaseType::Binary` fields keep their `Vec<u8>`.
368fn rust_scalar_field_shape(
369    base_type: &BaseType,
370    integer_kind: Option<IntegerKind>,
371    is_array: bool,
372    inner_type: Option<&str>,
373    rust_type_name: &str,
374) -> RustScalarShape {
375    if is_array && matches!(base_type, BaseType::Array) {
376        if let Some(kind) = integer_kind {
377            let kind = normalized_integer_kind_of(kind);
378            return RustScalarShape {
379                rust_type: format!("Vec<{kind}>"),
380                integer_kind: Some(kind),
381                is_vec: true,
382            };
383        }
384        if let Some(element) = inner_type.and_then(rust_scalar_array_element) {
385            return RustScalarShape {
386                rust_type: format!("Vec<{element}>"),
387                integer_kind: None,
388                is_vec: false,
389            };
390        }
391    }
392
393    // Only integer and timestamp types need the string-or-number treatment.
394    let kind = match base_type {
395        BaseType::Integer => Some(normalized_integer_kind(rust_type_name)),
396        BaseType::Timestamp => Some("i64"),
397        _ => None,
398    };
399    let is_vec = is_array && !matches!(base_type, BaseType::Array);
400    let base = base_type_to_rust(base_type, rust_type_name);
401    RustScalarShape {
402        rust_type: if is_vec { format!("Vec<{base}>") } else { base },
403        integer_kind: kind,
404        is_vec,
405    }
406}
407
408/// The `serde_utils::deserialize_*` function a shape needs, or `None` when a
409/// plain `#[serde(default)]` suffices.
410fn deserialize_with_for_shape(shape: &RustScalarShape, is_optional: bool) -> Option<String> {
411    let kind = shape.integer_kind?;
412    Some(match (is_optional, shape.is_vec) {
413        (false, false) => format!("serde_utils::deserialize_option_{kind}"),
414        (true, false) => format!("serde_utils::deserialize_option_option_{kind}"),
415        (false, true) => format!("serde_utils::deserialize_option_vec_{kind}"),
416        (true, true) => format!("serde_utils::deserialize_option_option_vec_{kind}"),
417    })
418}
419
420fn base_type_to_rust(base_type: &BaseType, rust_type_name: &str) -> String {
421    match base_type {
422        BaseType::Integer => normalized_integer_kind(rust_type_name).to_string(),
423        BaseType::Float => "f64".to_string(),
424        BaseType::String => "String".to_string(),
425        BaseType::Boolean => "bool".to_string(),
426        BaseType::Timestamp => "i64".to_string(),
427        BaseType::Binary => "Vec<u8>".to_string(),
428        BaseType::Pubkey => "String".to_string(),
429        BaseType::Array => "Vec<serde_json::Value>".to_string(),
430        BaseType::Object => "serde_json::Value".to_string(),
431        BaseType::Any => "serde_json::Value".to_string(),
432    }
433}
434
435pub(crate) struct RustCompiler {
436    spec: SerializableStreamSpec,
437    entity_name: String,
438    config: RustConfig,
439    /// Field targets fed by an `AsCapture` mapping in this entity's handlers.
440    capture_fields: HashSet<String>,
441}
442
443impl RustCompiler {
444    pub(crate) fn new(
445        spec: SerializableStreamSpec,
446        entity_name: String,
447        config: RustConfig,
448    ) -> Self {
449        let capture_fields = capture_field_targets(&spec);
450        Self {
451            spec,
452            entity_name,
453            config,
454            capture_fields,
455        }
456    }
457
458    fn compile(&self) -> RustOutput {
459        RustOutput {
460            cargo_toml: self.generate_cargo_toml(),
461            lib_rs: self.generate_lib_rs(),
462            types_rs: self.generate_types_rs(),
463            entity_rs: self.generate_entity_rs(),
464            programs_rs: None,
465        }
466    }
467
468    fn generate_cargo_toml(&self) -> String {
469        format!(
470            r#"[package]
471name = "{}"
472version = "0.1.0"
473edition = "2021"
474
475[dependencies]
476arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
477serde = {{ version = "1", features = ["derive"] }}
478serde_json = "1"
479"#,
480            self.config.crate_name, self.config.sdk_version
481        )
482    }
483
484    fn generate_lib_rs(&self) -> String {
485        let stack_name = self.derive_stack_name();
486        let entity_name = &self.entity_name;
487
488        format!(
489            r#"mod entity;
490mod types;
491
492pub use entity::{{{stack_name}Stack, {stack_name}StackViews, {entity_name}EntityViews}};
493pub use types::*;
494
495pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
496"#,
497            stack_name = stack_name,
498            entity_name = entity_name
499        )
500    }
501
502    fn generate_types_rs(&self) -> String {
503        let mut output = String::new();
504        output.push_str("use serde::{Deserialize, Serialize};\n");
505        output.push_str("use arete_sdk::serde_utils;\n\n");
506
507        let resolved_name_map = self.build_resolved_type_name_map();
508        let mut generated = HashSet::new();
509
510        for section in &self.spec.sections {
511            if !Self::is_root_section(&section.name)
512                && section.fields.iter().any(|field| field.emit)
513                && generated.insert(section.name.clone())
514            {
515                output.push_str(&self.generate_struct_for_section(section, &resolved_name_map));
516                output.push_str("\n\n");
517            }
518        }
519
520        output.push_str(&self.generate_main_entity_struct(&resolved_name_map));
521        output.push_str(&self.generate_resolved_types(&resolved_name_map, &mut generated, None));
522
523        let builtins = render_builtin_resolver_structs(&self.used_builtin_resolver_types());
524        if !builtins.is_empty() {
525            output.push_str("\n\n");
526            output.push_str(builtins.trim_end());
527        }
528
529        output.push_str(&self.generate_wrapper_types());
530
531        output
532    }
533
534    pub(crate) fn generate_struct_for_section(
535        &self,
536        section: &EntitySection,
537        resolved_name_map: &HashMap<String, String>,
538    ) -> String {
539        let struct_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
540        let mut fields = Vec::new();
541
542        for field in &section.fields {
543            if !field.emit {
544                continue;
545            }
546            let field_name = to_snake_case(&field.field_name);
547            let rust_type = self.field_type_to_rust(field, &section.name, resolved_name_map);
548            let serde_attr = self.serde_attr_for_field(field, &section.name);
549
550            fields.push(format!(
551                "    {}\n    pub {}: {},",
552                serde_attr, field_name, rust_type
553            ));
554        }
555
556        format!(
557            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
558            struct_name,
559            fields.join("\n")
560        )
561    }
562
563    pub(crate) fn is_root_section(name: &str) -> bool {
564        name.eq_ignore_ascii_case("root")
565    }
566
567    pub(crate) fn generate_main_entity_struct(
568        &self,
569        resolved_name_map: &HashMap<String, String>,
570    ) -> String {
571        let mut fields = Vec::new();
572
573        for section in &self.spec.sections {
574            if !Self::is_root_section(&section.name)
575                && section.fields.iter().any(|field| field.emit)
576            {
577                let field_name = to_snake_case(&section.name);
578                let type_name = format!("{}{}", self.entity_name, to_pascal_case(&section.name));
579                fields.push(format!(
580                    "    #[serde(default)]\n    pub {}: {},",
581                    field_name, type_name
582                ));
583            }
584        }
585
586        for section in &self.spec.sections {
587            if Self::is_root_section(&section.name) {
588                for field in &section.fields {
589                    if !field.emit {
590                        continue;
591                    }
592                    let field_name = to_snake_case(&field.field_name);
593                    let rust_type =
594                        self.field_type_to_rust(field, &section.name, resolved_name_map);
595                    let serde_attr = self.serde_attr_for_field(field, &section.name);
596                    fields.push(format!(
597                        "    {}\n    pub {}: {},",
598                        serde_attr, field_name, rust_type
599                    ));
600                }
601            }
602        }
603
604        format!(
605            "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
606            self.entity_name,
607            fields.join("\n")
608        )
609    }
610
611    pub(crate) fn generate_resolved_types(
612        &self,
613        resolved_name_map: &HashMap<String, String>,
614        generated: &mut HashSet<String>,
615        mut account_structs: Option<&mut BTreeMap<String, String>>,
616    ) -> String {
617        let mut output = String::new();
618
619        for section in &self.spec.sections {
620            for field in &section.fields {
621                if !field.emit {
622                    continue;
623                }
624                if let Some(resolved) = &field.resolved_type {
625                    let emitted_name = self.resolved_type_to_rust_name(resolved, resolved_name_map);
626                    if generated.insert(emitted_name.clone()) {
627                        if resolved.is_account && !resolved.is_enum {
628                            if let Some(map) = account_structs.as_deref_mut() {
629                                map.entry(resolved.type_name.clone())
630                                    .or_insert_with(|| emitted_name.clone());
631                            }
632                        }
633                        output.push_str("\n\n");
634                        output.push_str(&self.generate_resolved_struct(resolved, &emitted_name));
635                    }
636                }
637            }
638        }
639
640        output
641    }
642
643    fn generate_resolved_struct(
644        &self,
645        resolved: &ResolvedStructType,
646        emitted_name: &str,
647    ) -> String {
648        if resolved.is_enum {
649            let variants: Vec<String> = resolved
650                .enum_variants
651                .iter()
652                .map(|v| format!("    {},", to_pascal_case(v)))
653                .collect();
654
655            format!(
656                "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
657                emitted_name,
658                variants.join("\n")
659            )
660        } else {
661            let fields: Vec<String> = resolved
662                .fields
663                .iter()
664                .map(|f| {
665                    let rust_type = self.resolved_field_to_rust(f);
666                    let serde_attr = self.serde_attr_for_resolved_field(f);
667                    format!(
668                        "    {}\n    pub {}: {},",
669                        serde_attr,
670                        to_snake_case(&f.field_name),
671                        rust_type
672                    )
673                })
674                .collect();
675
676            format!(
677                "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
678                emitted_name,
679                fields.join("\n")
680            )
681        }
682    }
683
684    fn generate_wrapper_types(&self) -> String {
685        format!("\n\n{WRAPPER_TYPES}")
686    }
687
688    fn generate_entity_rs(&self) -> String {
689        let entity_name = &self.entity_name;
690        let stack_name = self.derive_stack_name();
691        let stack_name_kebab = to_kebab_case(entity_name);
692        let entity_snake = to_snake_case(entity_name);
693
694        let types_import = if self.config.module_mode {
695            "super::types"
696        } else {
697            "crate::types"
698        };
699
700        // Generate URL line - either actual URL or placeholder comment
701        let url_impl = match &self.config.url {
702            Some(url) => format!(
703                r#"fn url() -> &'static str {{
704        "{}"
705    }}"#,
706                url
707            ),
708            None => r#"fn url() -> &'static str {
709        "" // TODO: Set URL after first deployment in arete.toml
710    }"#
711            .to_string(),
712        };
713
714        let entity_views = self.generate_entity_views_struct();
715
716        format!(
717            r#"use {types_import}::{entity_name};
718use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
719
720pub struct {stack_name}Stack;
721
722impl Stack for {stack_name}Stack {{
723    type Views = {stack_name}StackViews;
724    type Programs = ();
725
726    fn name() -> &'static str {{
727        "{stack_name_kebab}"
728    }}
729
730    {url_impl}
731}}
732
733pub struct {stack_name}StackViews {{
734    pub {entity_snake}: {entity_name}EntityViews,
735}}
736
737impl Views for {stack_name}StackViews {{
738    fn from_builder(builder: ViewBuilder) -> Self {{
739        Self {{
740            {entity_snake}: {entity_name}EntityViews {{ builder }},
741        }}
742    }}
743}}
744{entity_views}"#,
745            types_import = types_import,
746            entity_name = entity_name,
747            stack_name = stack_name,
748            stack_name_kebab = stack_name_kebab,
749            entity_snake = entity_snake,
750            url_impl = url_impl,
751            entity_views = entity_views
752        )
753    }
754
755    fn generate_entity_views_struct(&self) -> String {
756        let entity_name = &self.entity_name;
757
758        let derived: Vec<_> = self
759            .spec
760            .views
761            .iter()
762            .filter(|v| {
763                !v.id.ends_with("/state")
764                    && !v.id.ends_with("/list")
765                    && v.id.starts_with(entity_name)
766            })
767            .collect();
768
769        let mut derived_methods = String::new();
770        for view in &derived {
771            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
772            let method_name = to_snake_case(view_name);
773
774            derived_methods.push_str(&format!(
775                r#"
776    pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
777        self.builder.view("{view_id}")
778    }}
779"#,
780                method_name = method_name,
781                entity_name = entity_name,
782                view_id = view.id
783            ));
784        }
785
786        format!(
787            r#"
788pub struct {entity_name}EntityViews {{
789    builder: ViewBuilder,
790}}
791
792impl {entity_name}EntityViews {{
793    pub fn state(&self) -> StateView<{entity_name}> {{
794        StateView::new(
795            self.builder.connection().clone(),
796            self.builder.store().clone(),
797            "{entity_name}/state".to_string(),
798            self.builder.initial_data_timeout(),
799        )
800    }}
801
802    pub fn list(&self) -> ViewHandle<{entity_name}> {{
803        self.builder.view("{entity_name}/list")
804    }}
805{derived_methods}}}"#,
806            entity_name = entity_name,
807            derived_methods = derived_methods
808        )
809    }
810
811    /// Derive stack name from entity name.
812    /// E.g., "OreRound" -> "Ore", "PumpfunToken" -> "Pumpfun"
813    fn derive_stack_name(&self) -> String {
814        let entity_name = &self.entity_name;
815
816        // Common suffixes to strip
817        let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
818
819        for suffix in suffixes {
820            if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
821                return entity_name[..entity_name.len() - suffix.len()].to_string();
822            }
823        }
824
825        // If no suffix matched, use the full entity name
826        entity_name.clone()
827    }
828
829    /// Generate Rust type for a field.
830    ///
831    /// All fields are wrapped in Option<T> because we receive partial patches,
832    /// so any field may not yet be present.
833    ///
834    /// - Non-optional spec fields become `Option<T>`:
835    ///   - `None` = not yet received in any patch
836    ///   - `Some(value)` = has value
837    ///
838    /// - Optional spec fields become `Option<Option<T>>`:
839    ///   - `None` = not yet received in any patch
840    ///   - `Some(None)` = explicitly set to null
841    ///   - `Some(Some(value))` = has value
842    fn field_type_to_rust(
843        &self,
844        field: &FieldTypeInfo,
845        section_name: &str,
846        resolved_name_map: &HashMap<String, String>,
847    ) -> String {
848        // Fields backed by a resolved IDL struct are typed against the emitted
849        // struct, wrapped in the runtime envelope they actually arrive in.
850        // Mirror of `typescript::field_type_info_to_typescript`.
851        let typed = if let Some(resolved) = &field.resolved_type {
852            let name = self.resolved_type_to_rust_name(resolved, resolved_name_map);
853            let element = match wrapper_kind_for(field, resolved, &self.capture_fields) {
854                WrapperKind::None => name,
855                WrapperKind::Capture => format!("CaptureWrapper<{}>", name),
856                WrapperKind::Event => format!("EventWrapper<{}>", name),
857            };
858            if field.is_array {
859                format!("Vec<{}>", element)
860            } else {
861                element
862            }
863        } else if let Some(builtin) = self.builtin_type_for_field(section_name, field) {
864            // Builtin resolver outputs are typed against the generated struct.
865            // Mirror of `typescript::field_type_info_to_typescript`.
866            if field.is_array {
867                format!("Vec<{}>", builtin)
868            } else {
869                builtin.to_string()
870            }
871        } else {
872            self.scalar_shape_for_field(field).rust_type
873        };
874
875        // All fields wrapped in Option since we receive patches
876        // Optional spec fields get Option<Option<T>> to distinguish "not received" from "explicitly null"
877        if field.is_optional {
878            format!("Option<Option<{}>>", typed)
879        } else {
880            format!("Option<{}>", typed)
881        }
882    }
883
884    /// The builtin resolver struct a section field is typed against, if any.
885    ///
886    /// Mirror of the TypeScript generator's "effective field info" override in
887    /// `add_unmapped_fields`: a computed field keeps the *user's* declared Rust
888    /// type in the section (`ResolvedSlotHash`), and only the `field_mappings`
889    /// entry records the resolver output type (`SlotHashBytes`), so both have
890    /// to be consulted.
891    fn builtin_type_for_field(
892        &self,
893        section_name: &str,
894        field: &FieldTypeInfo,
895    ) -> Option<&'static str> {
896        if let Some(name) = builtin_resolver_struct(field.inner_type.as_deref()) {
897            return Some(name);
898        }
899        let field_path = format!("{}.{}", section_name, field.field_name);
900        self.spec
901            .field_mappings
902            .get(&field_path)
903            .and_then(|mapping| builtin_resolver_struct(mapping.inner_type.as_deref()))
904    }
905
906    /// Builtin resolver structs referenced by this entity's emitted fields.
907    pub(crate) fn used_builtin_resolver_types(&self) -> BTreeSet<&'static str> {
908        let mut used = BTreeSet::new();
909        for section in &self.spec.sections {
910            for field in &section.fields {
911                if !field.emit || field.resolved_type.is_some() {
912                    continue;
913                }
914                if let Some(name) = self.builtin_type_for_field(&section.name, field) {
915                    used.insert(name);
916                }
917            }
918        }
919        used
920    }
921
922    fn scalar_shape_for_field(&self, field: &FieldTypeInfo) -> RustScalarShape {
923        rust_scalar_field_shape(
924            &field.base_type,
925            field.effective_integer_kind(),
926            field.is_array,
927            field
928                .inner_type
929                .as_deref()
930                .or(Some(field.rust_type_name.as_str())),
931            &field.rust_type_name,
932        )
933    }
934
935    fn scalar_shape_for_resolved_field(&self, field: &ResolvedField) -> RustScalarShape {
936        rust_scalar_field_shape(
937            &field.base_type,
938            field.effective_integer_kind(),
939            field.is_array,
940            Some(field.field_type.as_str()),
941            &field.field_type,
942        )
943    }
944
945    /// Return the `#[serde(...)]` attribute for a field.
946    /// Integer fields get a `deserialize_with` pointing to the appropriate
947    /// `serde_utils` function so that string-encoded big integers are handled.
948    fn serde_attr_for_field(&self, field: &FieldTypeInfo, section_name: &str) -> String {
949        if field.resolved_type.is_some()
950            || self.builtin_type_for_field(section_name, field).is_some()
951        {
952            return "#[serde(default)]".to_string();
953        }
954        let shape = self.scalar_shape_for_field(field);
955        match deserialize_with_for_shape(&shape, field.is_optional) {
956            Some(deser_fn) => format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn),
957            None => "#[serde(default)]".to_string(),
958        }
959    }
960
961    /// Same as `serde_attr_for_field` but for resolved struct fields.
962    fn serde_attr_for_resolved_field(&self, field: &ResolvedField) -> String {
963        let shape = self.scalar_shape_for_resolved_field(field);
964        match deserialize_with_for_shape(&shape, field.is_optional) {
965            Some(deser_fn) => format!("#[serde(default, deserialize_with = \"{}\")]", deser_fn),
966            None => "#[serde(default)]".to_string(),
967        }
968    }
969
970    fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
971        let typed = self.scalar_shape_for_resolved_field(field).rust_type;
972
973        if field.is_optional {
974            format!("Option<Option<{}>>", typed)
975        } else {
976            format!("Option<{}>", typed)
977        }
978    }
979
980    fn build_resolved_type_name_map(&self) -> HashMap<String, String> {
981        let mut reserved_names = HashSet::from([
982            self.entity_name.clone(),
983            "EventWrapper".to_string(),
984            "CaptureWrapper".to_string(),
985        ]);
986
987        // Builtin resolver structs share the `types.rs` namespace, so a
988        // same-named IDL type has to be renamed around them. Mirror of the
989        // TypeScript generator reserving `TokenMetadata`.
990        for (name, _) in BUILTIN_RESOLVER_STRUCTS {
991            reserved_names.insert((*name).to_string());
992        }
993
994        for section in &self.spec.sections {
995            if !Self::is_root_section(&section.name)
996                && section.fields.iter().any(|field| field.emit)
997            {
998                reserved_names.insert(format!(
999                    "{}{}",
1000                    self.entity_name,
1001                    to_pascal_case(&section.name)
1002                ));
1003            }
1004        }
1005
1006        let mut resolved_name_map = HashMap::new();
1007
1008        for section in &self.spec.sections {
1009            for field in &section.fields {
1010                if !field.emit {
1011                    continue;
1012                }
1013
1014                let Some(resolved) = &field.resolved_type else {
1015                    continue;
1016                };
1017
1018                if resolved_name_map.contains_key(&resolved.type_name) {
1019                    continue;
1020                }
1021
1022                let emitted_name = unique_resolved_type_name(resolved, &mut reserved_names);
1023                resolved_name_map.insert(resolved.type_name.clone(), emitted_name);
1024            }
1025        }
1026
1027        resolved_name_map
1028    }
1029
1030    fn resolved_type_to_rust_name(
1031        &self,
1032        resolved: &ResolvedStructType,
1033        resolved_name_map: &HashMap<String, String>,
1034    ) -> String {
1035        resolved_name_map
1036            .get(&resolved.type_name)
1037            .cloned()
1038            .unwrap_or_else(|| to_pascal_case(&resolved.type_name))
1039    }
1040}
1041
1042fn unique_resolved_type_name(
1043    resolved: &ResolvedStructType,
1044    reserved_names: &mut HashSet<String>,
1045) -> String {
1046    let base_name = to_pascal_case(&resolved.type_name);
1047    if reserved_names.insert(base_name.clone()) {
1048        return base_name;
1049    }
1050
1051    let suffix = if resolved.is_account {
1052        "Account"
1053    } else if resolved.is_event {
1054        "Event"
1055    } else if resolved.is_instruction {
1056        "Instruction"
1057    } else {
1058        "Type"
1059    };
1060
1061    let preferred = format!("{}{}", base_name, suffix);
1062    if reserved_names.insert(preferred.clone()) {
1063        return preferred;
1064    }
1065
1066    let mut index = 2;
1067    loop {
1068        let candidate = format!("{}{}{}", base_name, suffix, index);
1069        if reserved_names.insert(candidate.clone()) {
1070            return candidate;
1071        }
1072        index += 1;
1073    }
1074}
1075
1076/// [`normalized_integer_kind`] for an already-classified [`IntegerKind`].
1077/// Kept byte-for-byte equivalent to the string-sniffing version: only
1078/// `u64`/`i64`/`u32`/`i32` have `serde_utils` deserializers, so unsigned small
1079/// ints widen to `u64` and everything else widens to `i64`.
1080fn normalized_integer_kind_of(kind: IntegerKind) -> &'static str {
1081    match kind {
1082        IntegerKind::U64 => "u64",
1083        IntegerKind::U32 => "u32",
1084        IntegerKind::I32 => "i32",
1085        IntegerKind::U8 | IntegerKind::U16 | IntegerKind::Usize => "u64",
1086        // Signed small ints (i16/i8/isize) and the 128-bit kinds widen to i64.
1087        _ => "i64",
1088    }
1089}
1090
1091fn normalized_integer_kind(rust_type_name: &str) -> &'static str {
1092    if rust_type_name.contains("u64") {
1093        "u64"
1094    } else if rust_type_name.contains("i64") {
1095        "i64"
1096    } else if rust_type_name.contains("u32") {
1097        "u32"
1098    } else if rust_type_name.contains("i32") {
1099        "i32"
1100    } else if rust_type_name.contains("u16")
1101        || rust_type_name.contains("u8")
1102        || rust_type_name.contains("usize")
1103    {
1104        "u64"
1105    } else {
1106        // Signed small ints (i16/i8/isize) and anything unknown widen to i64.
1107        "i64"
1108    }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113    use super::*;
1114    use std::collections::BTreeMap;
1115    use std::process::Command;
1116
1117    fn identity_spec() -> IdentitySpec {
1118        IdentitySpec {
1119            primary_keys: vec!["id.address".to_string()],
1120            lookup_indexes: vec![],
1121        }
1122    }
1123
1124    #[test]
1125    fn rust_generator_renames_account_types_on_collision() {
1126        let plan_field = FieldTypeInfo {
1127            field_name: "plan".to_string(),
1128            raw_name: Some("plan".to_string()),
1129            canonical_name: Some("plan".to_string()),
1130            rust_type_name: "Option<serde_json::Value>".to_string(),
1131            base_type: BaseType::Object,
1132            integer_kind: None,
1133            is_optional: false,
1134            is_array: false,
1135            inner_type: Some("Value".to_string()),
1136            source_path: None,
1137            resolved_type: Some(ResolvedStructType {
1138                type_name: "plan".to_string(),
1139                fields: vec![],
1140                is_instruction: false,
1141                is_account: true,
1142                is_event: false,
1143                is_enum: false,
1144                enum_variants: vec![],
1145            }),
1146            emit: true,
1147        };
1148
1149        let spec = SerializableStreamSpec {
1150            ast_version: CURRENT_AST_VERSION.to_string(),
1151            state_name: "Plan".to_string(),
1152            program_id: None,
1153            idl: None,
1154            identity: identity_spec(),
1155            handlers: vec![],
1156            sections: vec![
1157                EntitySection {
1158                    name: "id".to_string(),
1159                    fields: vec![FieldTypeInfo::new(
1160                        "address".to_string(),
1161                        "String".to_string(),
1162                    )],
1163                    is_nested_struct: false,
1164                    parent_field: None,
1165                },
1166                EntitySection {
1167                    name: "plan".to_string(),
1168                    fields: vec![plan_field],
1169                    is_nested_struct: false,
1170                    parent_field: None,
1171                },
1172            ],
1173            field_mappings: BTreeMap::new(),
1174            resolver_hooks: vec![],
1175            instruction_hooks: vec![],
1176            resolver_specs: vec![],
1177            computed_fields: vec![],
1178            computed_field_specs: vec![],
1179            content_hash: None,
1180            views: vec![],
1181        };
1182
1183        let output = compile_serializable_spec(spec, "Plan".to_string(), None)
1184            .expect("rust sdk generation should succeed");
1185
1186        // The resolved struct is renamed away from the entity struct, and the
1187        // field is typed against the renamed struct (no `AsCapture` mapping
1188        // feeds it, so it stays a bare struct โ€” see
1189        // `rust_generator_wraps_capture_and_event_fields`).
1190        assert!(output.types_rs.contains("pub struct PlanAccount"));
1191        assert!(output.types_rs.contains("pub plan: Option<PlanAccount>"));
1192        assert!(!output
1193            .types_rs
1194            .contains("pub plan: Option<serde_json::Value>"));
1195        assert!(
1196            !output.types_rs.contains("pub struct Plan {\n    #[serde(default, deserialize_with = \"serde_utils::deserialize_option_u64\")]\n    pub discriminator")
1197        );
1198    }
1199
1200    #[test]
1201    fn rust_generator_keeps_unsigned_numeric_fields_unsigned() {
1202        let spec = SerializableStreamSpec {
1203            ast_version: CURRENT_AST_VERSION.to_string(),
1204            state_name: "Plan".to_string(),
1205            program_id: None,
1206            idl: None,
1207            identity: identity_spec(),
1208            handlers: vec![],
1209            sections: vec![
1210                EntitySection {
1211                    name: "id".to_string(),
1212                    fields: vec![FieldTypeInfo::new(
1213                        "address".to_string(),
1214                        "String".to_string(),
1215                    )],
1216                    is_nested_struct: false,
1217                    parent_field: None,
1218                },
1219                EntitySection {
1220                    name: "state".to_string(),
1221                    fields: vec![FieldTypeInfo::new(
1222                        "status".to_string(),
1223                        "Option<u8>".to_string(),
1224                    )],
1225                    is_nested_struct: false,
1226                    parent_field: None,
1227                },
1228            ],
1229            field_mappings: BTreeMap::new(),
1230            resolver_hooks: vec![],
1231            instruction_hooks: vec![],
1232            resolver_specs: vec![],
1233            computed_fields: vec![],
1234            computed_field_specs: vec![],
1235            content_hash: None,
1236            views: vec![],
1237        };
1238
1239        let output = compile_serializable_spec(spec, "Plan".to_string(), None)
1240            .expect("rust sdk generation should succeed");
1241
1242        assert!(
1243            output.types_rs.contains("pub status: Option<Option<u64>>"),
1244            "expected unsigned optional field, got:\n{}",
1245            output.types_rs
1246        );
1247    }
1248
1249    /// Scalar arrays must land on a real Rust element type. `Vec<u64>`-shaped
1250    /// fields reach the generator as `BaseType::Array` + `integer_kind`, so the
1251    /// integer check has to consult `integer_kind` (TypeScript emits
1252    /// `bigint[]`, Python `List[int]`); non-integer scalar arrays keep their
1253    /// element type instead of degrading to `Vec<serde_json::Value>`.
1254    /// Rust twin of `python::tests::python_generator_converts_u64_arrays`.
1255    #[test]
1256    fn rust_generator_types_scalar_arrays() {
1257        let mut entity = minimal_entity("OreRound");
1258        entity.sections.push(EntitySection {
1259            name: "state".to_string(),
1260            fields: vec![
1261                FieldTypeInfo::new(
1262                    "deployed_per_square".to_string(),
1263                    "Option<Vec<u64>>".to_string(),
1264                ),
1265                FieldTypeInfo::new(
1266                    "deployed_per_square_ui".to_string(),
1267                    "Option<Vec<f64>>".to_string(),
1268                ),
1269                FieldTypeInfo::new("flags".to_string(), "Option<Vec<bool>>".to_string()),
1270                FieldTypeInfo::new("labels".to_string(), "Option<Vec<String>>".to_string()),
1271                FieldTypeInfo::new("resolved_seed".to_string(), "Option<Vec<u8>>".to_string()),
1272                // A `#[binary]` blob keeps `Vec<u8>`: the integer guard is
1273                // `BaseType::Array`-only, never "any field with an
1274                // `integer_kind`".
1275                FieldTypeInfo::new("payload".to_string(), "Option<Vec<u8>>".to_string()),
1276            ],
1277            is_nested_struct: false,
1278            parent_field: None,
1279        });
1280        // The interpreter records the element kind explicitly for `Vec<u64>`.
1281        for field in &mut entity.sections[1].fields {
1282            match field.field_name.as_str() {
1283                "deployed_per_square" => {
1284                    field.base_type = BaseType::Array;
1285                    field.integer_kind = Some(IntegerKind::U64);
1286                    field.is_array = true;
1287                    field.inner_type = Some("Vec < u64 >".to_string());
1288                }
1289                "resolved_seed" => {
1290                    field.base_type = BaseType::Array;
1291                    field.integer_kind = Some(IntegerKind::U8);
1292                    field.is_array = true;
1293                    field.inner_type = Some("Vec < u8 >".to_string());
1294                }
1295                "deployed_per_square_ui" => {
1296                    field.base_type = BaseType::Array;
1297                    field.is_array = true;
1298                    field.inner_type = Some("Vec < f64 >".to_string());
1299                }
1300                "flags" => {
1301                    field.base_type = BaseType::Array;
1302                    field.is_array = true;
1303                    field.inner_type = Some("Vec < bool >".to_string());
1304                }
1305                "labels" => {
1306                    field.base_type = BaseType::Array;
1307                    field.is_array = true;
1308                    field.inner_type = Some("Vec < String >".to_string());
1309                }
1310                "payload" => {
1311                    field.base_type = BaseType::Binary;
1312                    field.integer_kind = Some(IntegerKind::U8);
1313                    field.is_array = false;
1314                    field.inner_type = Some("Vec < u8 >".to_string());
1315                }
1316                _ => {}
1317            }
1318        }
1319
1320        let output = compile_stack_spec(stack_of("OreRound", entity), None)
1321            .expect("rust stack generation should succeed");
1322        let types = &output.types_rs;
1323
1324        assert!(
1325            !types.contains("Vec<serde_json::Value>"),
1326            "scalar arrays should not fall back to untyped values:\n{types}"
1327        );
1328
1329        // u64 arrives on the wire as decimal strings (canonical numeric rule),
1330        // so the typed vector needs the string-or-number vector deserializer.
1331        assert!(
1332            types.contains(
1333                "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_vec_u64\")]\n    pub deployed_per_square: Option<Option<Vec<u64>>>,"
1334            ),
1335            "expected a typed u64 vector with its deserializer:\n{types}"
1336        );
1337        // Small unsigned ints widen to u64, matching the scalar policy in
1338        // `normalized_integer_kind` (only u64/i64/u32/i32 have deserializers).
1339        assert!(
1340            types.contains(
1341                "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_vec_u64\")]\n    pub resolved_seed: Option<Option<Vec<u64>>>,"
1342            ),
1343            "expected u8 arrays to widen to Vec<u64>:\n{types}"
1344        );
1345
1346        // Non-integer scalar arrays keep their element type and need no
1347        // custom deserializer.
1348        assert!(types.contains(
1349            "#[serde(default)]\n    pub deployed_per_square_ui: Option<Option<Vec<f64>>>,"
1350        ));
1351        assert!(types.contains("#[serde(default)]\n    pub flags: Option<Option<Vec<bool>>>,"));
1352        assert!(types.contains("#[serde(default)]\n    pub labels: Option<Option<Vec<String>>>,"));
1353
1354        // `BaseType::Binary` is untouched by the integer-array branch.
1355        assert!(
1356            types.contains("#[serde(default)]\n    pub payload: Option<Option<Vec<u8>>>,"),
1357            "binary fields must keep Vec<u8>:\n{types}"
1358        );
1359    }
1360
1361    /// Builtin resolver outputs are typed against generated structs, matching
1362    /// TypeScript's `oreMetadata: TokenMetadata | null` /
1363    /// `expiresAtSlotHash: SlotHashBytes | null`. `expires_at_slot_hash` only
1364    /// names the resolver output type in `field_mappings` (the section keeps
1365    /// the user's declared `ResolvedSlotHash`), which is the TypeScript
1366    /// "effective field info" override.
1367    #[test]
1368    fn rust_generator_types_builtin_resolver_fields() {
1369        let mut entity = minimal_entity("OreRound");
1370        let mut ore_metadata = FieldTypeInfo::new(
1371            "ore_metadata".to_string(),
1372            "Option<TokenMetadata>".to_string(),
1373        );
1374        ore_metadata.base_type = BaseType::Object;
1375        ore_metadata.is_optional = true;
1376        ore_metadata.inner_type = Some("TokenMetadata".to_string());
1377
1378        let mut expires_at_slot_hash = FieldTypeInfo::new(
1379            "expires_at_slot_hash".to_string(),
1380            "Option<ResolvedSlotHash>".to_string(),
1381        );
1382        expires_at_slot_hash.base_type = BaseType::Object;
1383        expires_at_slot_hash.is_optional = true;
1384        expires_at_slot_hash.inner_type = Some("ResolvedSlotHash".to_string());
1385
1386        // `KeccakRngValue` is a registered resolver output type, but it is a
1387        // u64 that the wire spells as a decimal string; Rust decodes it.
1388        let mut rng = FieldTypeInfo::new("rng".to_string(), "Option<u64>".to_string());
1389        rng.is_optional = true;
1390        rng.inner_type = Some("KeccakRngValue".to_string());
1391        rng.integer_kind = Some(IntegerKind::U64);
1392
1393        entity.sections.push(EntitySection {
1394            name: "results".to_string(),
1395            fields: vec![expires_at_slot_hash.clone(), rng],
1396            is_nested_struct: false,
1397            parent_field: None,
1398        });
1399        entity.sections.push(EntitySection {
1400            name: "root".to_string(),
1401            fields: vec![ore_metadata],
1402            is_nested_struct: false,
1403            parent_field: None,
1404        });
1405
1406        let mut slot_hash_mapping = expires_at_slot_hash;
1407        slot_hash_mapping.base_type = BaseType::Any;
1408        slot_hash_mapping.inner_type = Some("SlotHashBytes".to_string());
1409        entity.field_mappings.insert(
1410            "results.expires_at_slot_hash".to_string(),
1411            slot_hash_mapping,
1412        );
1413
1414        let output = compile_stack_spec(stack_of("OreRound", entity), None)
1415            .expect("rust stack generation should succeed");
1416        let types = &output.types_rs;
1417
1418        assert!(
1419            types.contains("pub ore_metadata: Option<Option<TokenMetadata>>,"),
1420            "expected a typed TokenMetadata field:\n{types}"
1421        );
1422        assert!(
1423            types.contains("pub expires_at_slot_hash: Option<Option<SlotHashBytes>>,"),
1424            "expected the field_mappings override to type the slot hash:\n{types}"
1425        );
1426        assert!(!types.contains("pub ore_metadata: Option<Option<serde_json::Value>>,"));
1427        assert!(!types.contains("pub expires_at_slot_hash: Option<Option<serde_json::Value>>,"));
1428
1429        // The structs themselves are emitted once, with the snake_case wire keys.
1430        assert_eq!(types.matches("pub struct TokenMetadata {").count(), 1);
1431        assert_eq!(types.matches("pub struct SlotHashBytes {").count(), 1);
1432        assert!(types.contains("    pub logo_uri: Option<String>,"));
1433        assert!(types.contains("    pub bytes: Vec<u8>,"));
1434
1435        // `KeccakRngValue` stays a real u64 rather than degrading to String.
1436        assert!(
1437            types.contains(
1438                "#[serde(default, deserialize_with = \"serde_utils::deserialize_option_option_u64\")]\n    pub rng: Option<Option<u64>>,"
1439            ),
1440            "KeccakRngValue fields must stay integers:\n{types}"
1441        );
1442        assert!(!types.contains("pub struct KeccakRngValue"));
1443    }
1444
1445    /// Unused builtin resolver structs are not emitted.
1446    #[test]
1447    fn rust_generator_omits_unused_builtin_resolver_structs() {
1448        let output = compile_stack_spec(stack_of("OreTreasury", capture_entity()), None)
1449            .expect("rust stack generation should succeed");
1450
1451        assert!(!output.types_rs.contains("pub struct TokenMetadata"));
1452        assert!(!output.types_rs.contains("pub struct SlotHashBytes"));
1453    }
1454
1455    #[test]
1456    fn generated_manifest_uses_published_arete_sdk_package() {
1457        let manifest = generate_stack_cargo_toml(&RustStackConfig::default());
1458
1459        assert!(manifest.contains("arete-sdk = { package = \"arete-a4-sdk\", version = \"0.4\" }"));
1460    }
1461
1462    fn resolved_field_of(name: &str, field_type: &str, base_type: BaseType) -> ResolvedField {
1463        ResolvedField {
1464            field_name: name.to_string(),
1465            raw_name: Some(name.to_string()),
1466            canonical_name: None,
1467            field_type: field_type.to_string(),
1468            base_type,
1469            integer_kind: IntegerKind::from_rust_type(field_type),
1470            is_optional: false,
1471            is_array: false,
1472        }
1473    }
1474
1475    /// A root-section field backed by a resolved struct. Mirror of the Python
1476    /// generator's `snapshot_field` fixture.
1477    fn snapshot_field(
1478        field_name: &str,
1479        type_name: &str,
1480        is_account: bool,
1481        is_event: bool,
1482    ) -> FieldTypeInfo {
1483        FieldTypeInfo {
1484            field_name: field_name.to_string(),
1485            raw_name: Some(field_name.to_string()),
1486            canonical_name: None,
1487            rust_type_name: "Option<serde_json::Value>".to_string(),
1488            base_type: BaseType::Object,
1489            integer_kind: None,
1490            is_optional: true,
1491            is_array: false,
1492            inner_type: Some("Value".to_string()),
1493            source_path: None,
1494            resolved_type: Some(ResolvedStructType {
1495                type_name: type_name.to_string(),
1496                fields: vec![
1497                    resolved_field_of("motherlode", "u64", BaseType::Integer),
1498                    resolved_field_of("owner", "publicKey", BaseType::Pubkey),
1499                ],
1500                is_instruction: false,
1501                is_account,
1502                is_event,
1503                is_enum: false,
1504                enum_variants: vec![],
1505            }),
1506            emit: true,
1507        }
1508    }
1509
1510    /// The handler mapping that feeds a field via `#[capture]`.
1511    fn capture_handler(target_path: &str) -> SerializableHandlerSpec {
1512        SerializableHandlerSpec {
1513            source: SourceSpec::Source {
1514                program_id: None,
1515                discriminator: None,
1516                type_name: "Treasury".to_string(),
1517                serialization: None,
1518                is_account: true,
1519            },
1520            key_resolution: KeyResolutionStrategy::Embedded {
1521                primary_field: FieldPath::new(&["id", "address"]),
1522            },
1523            mappings: vec![SerializableFieldMapping {
1524                target_path: target_path.to_string(),
1525                source: MappingSource::AsCapture {
1526                    field_transforms: BTreeMap::new(),
1527                },
1528                transform: None,
1529                population: PopulationStrategy::LastWrite,
1530                condition: None,
1531                when: None,
1532                stop: None,
1533                emit: true,
1534            }],
1535            conditions: vec![],
1536            emit: true,
1537        }
1538    }
1539
1540    fn stack_of(name: &str, entity: SerializableStreamSpec) -> SerializableStackSpec {
1541        SerializableStackSpec {
1542            ast_version: CURRENT_AST_VERSION.to_string(),
1543            stack_name: name.to_string(),
1544            program_ids: vec![],
1545            idls: vec![],
1546            program_specs: vec![],
1547            entities: vec![entity],
1548            pdas: BTreeMap::new(),
1549            instructions: vec![],
1550            content_hash: None,
1551        }
1552    }
1553
1554    fn capture_entity() -> SerializableStreamSpec {
1555        let mut entity = minimal_entity("OreTreasury");
1556        entity.handlers.push(capture_handler("treasury_snapshot"));
1557        entity.sections.push(EntitySection {
1558            name: "root".to_string(),
1559            fields: vec![
1560                snapshot_field("treasury_snapshot", "Treasury", true, false),
1561                // Same struct kind, but no AsCapture mapping: stays unwrapped.
1562                snapshot_field("plain_account", "Vault", true, false),
1563                snapshot_field("deposit_event", "DepositEvent", false, true),
1564            ],
1565            is_nested_struct: false,
1566            parent_field: None,
1567        });
1568        entity
1569    }
1570
1571    /// `#[capture]`-fed account fields and event fields arrive wrapped on the
1572    /// wire (`{timestamp, account_address, data: {...}, slot?, signature?}`).
1573    /// Emitting them as untyped `serde_json::Value` loses the typing TS and
1574    /// Python give; the envelope itself stays exposed because the provenance
1575    /// is unrecoverable elsewhere.
1576    #[test]
1577    fn rust_generator_wraps_capture_and_event_fields() {
1578        let output = compile_stack_spec(stack_of("OreTreasury", capture_entity()), None)
1579            .expect("rust stack generation should succeed");
1580        let types = &output.types_rs;
1581
1582        // Both envelopes are emitted once, with the full provenance surface.
1583        assert!(types.contains("pub struct EventWrapper<T> {"));
1584        assert!(types.contains("pub struct CaptureWrapper<T> {"));
1585        assert!(types.contains("    pub account_address: String,"));
1586        assert!(types.contains("    pub data: T,"));
1587        assert!(types.contains("    pub slot: Option<u64>,"));
1588        assert!(types.contains("    pub signature: Option<String>,"));
1589        assert_eq!(types.matches("pub struct CaptureWrapper<T>").count(), 1);
1590
1591        // Capture-fed account field: typed envelope, not an untyped blob.
1592        assert!(
1593            types.contains("pub treasury_snapshot: Option<Option<CaptureWrapper<Treasury>>>,"),
1594            "expected a typed capture envelope, got:\n{types}"
1595        );
1596        assert!(!types.contains("pub treasury_snapshot: Option<Option<serde_json::Value>>,"));
1597
1598        // Event field: EventWrapper envelope.
1599        assert!(types.contains("pub deposit_event: Option<Option<EventWrapper<DepositEvent>>>,"));
1600
1601        // Unmapped account field keeps the bare-struct shape.
1602        assert!(types.contains("pub plain_account: Option<Option<Vault>>,"));
1603        assert!(!types.contains("CaptureWrapper<Vault>"));
1604
1605        // The inner structs are still emitted so the envelopes resolve.
1606        assert!(types.contains("pub struct Treasury {"));
1607        assert!(types.contains("pub struct Vault {"));
1608        assert!(types.contains("pub struct DepositEvent {"));
1609    }
1610
1611    /// The single-entity path (`compile_serializable_spec`) emits the same
1612    /// envelopes as the stack path.
1613    #[test]
1614    fn rust_generator_wraps_capture_fields_in_single_entity_mode() {
1615        let output = compile_serializable_spec(capture_entity(), "OreTreasury".to_string(), None)
1616            .expect("rust sdk generation should succeed");
1617        let types = &output.types_rs;
1618
1619        assert!(types.contains("pub struct CaptureWrapper<T> {"));
1620        assert!(types.contains("pub struct EventWrapper<T> {"));
1621        assert!(types.contains("pub treasury_snapshot: Option<Option<CaptureWrapper<Treasury>>>,"));
1622        assert!(types.contains("pub deposit_event: Option<Option<EventWrapper<DepositEvent>>>,"));
1623        assert!(types.contains("pub plain_account: Option<Option<Vault>>,"));
1624    }
1625
1626    /// `CaptureWrapper` is reserved in the resolved-type name map, so an IDL
1627    /// struct actually named `CaptureWrapper` is renamed instead of shadowing
1628    /// the envelope.
1629    #[test]
1630    fn rust_generator_reserves_wrapper_type_names() {
1631        let mut entity = minimal_entity("OreTreasury");
1632        entity.sections.push(EntitySection {
1633            name: "root".to_string(),
1634            fields: vec![
1635                snapshot_field("wrapped", "CaptureWrapper", true, false),
1636                snapshot_field("evented", "EventWrapper", true, false),
1637            ],
1638            is_nested_struct: false,
1639            parent_field: None,
1640        });
1641
1642        let output = compile_stack_spec(stack_of("OreTreasury", entity), None)
1643            .expect("rust stack generation should succeed");
1644        let types = &output.types_rs;
1645
1646        assert!(types.contains("pub struct CaptureWrapperAccount {"));
1647        assert!(types.contains("pub struct EventWrapperAccount {"));
1648        assert!(types.contains("pub wrapped: Option<Option<CaptureWrapperAccount>>,"));
1649        assert!(types.contains("pub evented: Option<Option<EventWrapperAccount>>,"));
1650        assert_eq!(types.matches("pub struct CaptureWrapper<T>").count(), 1);
1651    }
1652
1653    const TEST_PROGRAM_ID: &str = "Prog111111111111111111111111111111111111111";
1654
1655    fn minimal_entity(name: &str) -> SerializableStreamSpec {
1656        SerializableStreamSpec {
1657            ast_version: CURRENT_AST_VERSION.to_string(),
1658            state_name: name.to_string(),
1659            program_id: None,
1660            idl: None,
1661            identity: identity_spec(),
1662            handlers: vec![],
1663            sections: vec![EntitySection {
1664                name: "id".to_string(),
1665                fields: vec![FieldTypeInfo::new(
1666                    "address".to_string(),
1667                    "String".to_string(),
1668                )],
1669                is_nested_struct: false,
1670                parent_field: None,
1671            }],
1672            field_mappings: BTreeMap::new(),
1673            resolver_hooks: vec![],
1674            instruction_hooks: vec![],
1675            resolver_specs: vec![],
1676            computed_fields: vec![],
1677            computed_field_specs: vec![],
1678            content_hash: None,
1679            views: vec![],
1680        }
1681    }
1682
1683    fn test_idl() -> IdlSnapshot {
1684        IdlSnapshot {
1685            name: "demo".to_string(),
1686            program_id: Some(TEST_PROGRAM_ID.to_string()),
1687            version: "0.1.0".to_string(),
1688            accounts: vec![],
1689            instructions: vec![],
1690            types: vec![],
1691            events: vec![],
1692            errors: vec![IdlErrorSnapshot {
1693                code: 6000,
1694                name: "SlippageExceeded".to_string(),
1695                msg: Some("Slippage exceeded".to_string()),
1696            }],
1697            discriminant_size: 8,
1698        }
1699    }
1700
1701    fn instruction_account(name: &str, resolution: AccountResolution) -> InstructionAccountDef {
1702        InstructionAccountDef {
1703            name: name.to_string(),
1704            is_signer: matches!(resolution, AccountResolution::Signer),
1705            is_writable: true,
1706            resolution,
1707            is_optional: false,
1708            docs: vec![],
1709        }
1710    }
1711
1712    fn instruction_arg(name: &str, arg_type: &str) -> InstructionArgDef {
1713        InstructionArgDef {
1714            name: name.to_string(),
1715            arg_type: arg_type.to_string(),
1716            docs: vec![],
1717            amount_hint: None,
1718        }
1719    }
1720
1721    fn programs_stack_spec() -> SerializableStackSpec {
1722        let mut demo_pdas = BTreeMap::new();
1723        demo_pdas.insert(
1724            "counter".to_string(),
1725            PdaDefinition {
1726                name: "counter".to_string(),
1727                seeds: vec![
1728                    PdaSeedDef::Literal {
1729                        value: "counter".to_string(),
1730                    },
1731                    PdaSeedDef::AccountRef {
1732                        account_name: "authority".to_string(),
1733                    },
1734                ],
1735                program_id: None,
1736                program: None,
1737            },
1738        );
1739        let mut pdas = BTreeMap::new();
1740        pdas.insert("demo".to_string(), demo_pdas);
1741
1742        SerializableStackSpec {
1743            ast_version: CURRENT_AST_VERSION.to_string(),
1744            stack_name: "Demo".to_string(),
1745            program_ids: vec![TEST_PROGRAM_ID.to_string()],
1746            idls: vec![test_idl()],
1747            program_specs: vec![],
1748            entities: vec![minimal_entity("DemoThing")],
1749            pdas,
1750            instructions: vec![InstructionDef {
1751                name: "doThing".to_string(),
1752                discriminator: vec![12, 34],
1753                discriminator_size: 2,
1754                accounts: vec![
1755                    instruction_account("signer", AccountResolution::Signer),
1756                    instruction_account("authority", AccountResolution::UserProvided),
1757                    instruction_account(
1758                        "counter",
1759                        AccountResolution::PdaRef {
1760                            pda_name: "counter".to_string(),
1761                        },
1762                    ),
1763                    instruction_account(
1764                        "systemProgram",
1765                        AccountResolution::Known {
1766                            address: "11111111111111111111111111111111".to_string(),
1767                        },
1768                    ),
1769                ],
1770                args: vec![
1771                    instruction_arg("roundId", "u64"),
1772                    instruction_arg("admin", "solana_pubkey::Pubkey"),
1773                    instruction_arg("tip", "Option<u64>"),
1774                ],
1775                errors: vec![],
1776                program_id: Some(TEST_PROGRAM_ID.to_string()),
1777                docs: vec!["Does the thing.".to_string()],
1778            }],
1779            content_hash: None,
1780        }
1781    }
1782
1783    #[test]
1784    fn rust_generator_emits_program_sdk_module() {
1785        let output = compile_stack_spec(programs_stack_spec(), None)
1786            .expect("rust stack generation should succeed");
1787        let programs = output
1788            .programs_rs
1789            .expect("programs.rs should be generated for stacks with instructions");
1790
1791        assert!(programs.contains("pub mod demo {"));
1792        assert!(programs.contains(&format!(
1793            "pub const PROGRAM_ID: &str = \"{}\";",
1794            TEST_PROGRAM_ID
1795        )));
1796
1797        // Typed params: args (with serde renames) then account overrides.
1798        assert!(programs.contains("pub struct DoThingParams {"));
1799        assert!(programs.contains("#[serde(rename = \"roundId\")]"));
1800        assert!(programs.contains("pub round_id: u64,"));
1801        assert!(programs.contains("pub admin: String,"));
1802        assert!(programs.contains("pub tip: Option<u64>,"));
1803        assert!(programs.contains("pub signer: Option<String>,"));
1804        assert!(programs.contains("pub authority: String,"));
1805        assert!(programs.contains("#[serde(skip_serializing_if = \"Option::is_none\")]"));
1806
1807        // Handler literal fragments.
1808        assert!(programs.contains("discriminator: vec![12, 34]"));
1809        assert!(programs.contains("resolution: AccountResolution::Signer,"));
1810        assert!(programs.contains(
1811            "AccountResolution::Known(\"11111111111111111111111111111111\".to_string())"
1812        ));
1813        assert!(programs.contains(
1814            "AccountResolution::Pda(PdaConfig { program_id: None, seeds: vec![PdaSeed::Literal(\"counter\".to_string()), PdaSeed::AccountRef(\"authority\".to_string())] })"
1815        ));
1816        assert!(programs.contains("ArgSchema { name: \"roundId\".to_string(), ty: ArgType::U64 }"));
1817        assert!(programs.contains("ty: ArgType::Option(Box::new(ArgType::U64))"));
1818        assert!(programs.contains("ty: ArgType::Pubkey"));
1819        assert!(programs.contains(
1820            "ErrorMetadata { code: 6000, name: \"SlippageExceeded\".to_string(), msg: \"Slippage exceeded\".to_string() }"
1821        ));
1822
1823        // PDA helper fn.
1824        assert!(programs
1825            .contains("pub fn counter(authority: &str) -> Result<(Pubkey, u8), InstructionError>"));
1826
1827        // Program accessor struct carries the runtime + typed builder.
1828        assert!(programs.contains("pub struct DemoProgram {"));
1829        assert!(programs.contains("builder: arete_sdk::ProgramBuilder,"));
1830        assert!(
1831            programs.contains("pub fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self")
1832        );
1833        assert!(programs.contains(
1834            "pub fn do_thing(params: DoThingParams) -> Result<BuiltInstruction, InstructionError>"
1835        ));
1836        assert!(programs.contains("pub fn do_thing_handler() -> InstructionHandler"));
1837
1838        // No program spec recorded: the read layer is omitted with a doc note.
1839        assert!(programs.contains(
1840            "/// Program read layer omitted: no program specification was recorded for this program."
1841        ));
1842        assert!(!programs.contains("pub const PROGRAM_SPEC_HASH"));
1843        assert!(!programs.contains("pub fn read_descriptor"));
1844
1845        // Stack wiring.
1846        assert!(output
1847            .entity_rs
1848            .contains("type Programs = DemoStackPrograms;"));
1849        assert!(output
1850            .entity_rs
1851            .contains("pub demo: crate::programs::demo::DemoProgram,"));
1852        assert!(output
1853            .entity_rs
1854            .contains("demo: crate::programs::demo::DemoProgram::from_builder(builder),"));
1855        assert!(output
1856            .entity_rs
1857            .contains("impl arete_sdk::Programs for DemoStackPrograms"));
1858        assert!(output.lib_rs.contains("pub mod programs;"));
1859        assert!(output.lib_rs.contains("DemoStackPrograms"));
1860    }
1861
1862    #[test]
1863    fn rust_program_compiler_emits_no_view_or_stack_shell() {
1864        let output = compile_program_modules(
1865            programs_stack_spec(),
1866            Some(RustStackConfig {
1867                crate_name: "demo-program".to_string(),
1868                sdk_version: env!("CARGO_PKG_VERSION").to_string(),
1869                program_reads: vec![RustProgramReadConfig {
1870                    program_id: TEST_PROGRAM_ID.to_string(),
1871                    program_spec_hash: "arete:h1:program-spec:sha256:test".to_string(),
1872                    program_release_hash: "arete:h1:program-release:sha256:test".to_string(),
1873                    descriptor: Some(serde_json::json!({
1874                        "release": {
1875                            "programReleaseHash": "arete:h1:program-release:sha256:test",
1876                            "programSpecHash": "arete:h1:program-spec:sha256:test"
1877                        },
1878                        "transport": {
1879                            "kind": "hosted-binding",
1880                            "binding": {
1881                                "endpoint": "https://reads.example.test",
1882                                "programReadBindingId": "prb_00000000000000000000000000000001",
1883                                "auth": {
1884                                    "sessionEndpoint": "https://auth.example.test/session",
1885                                    "targetKind": "program-read-binding",
1886                                    "targetId": "prb_00000000000000000000000000000001"
1887                                }
1888                            }
1889                        }
1890                    })),
1891                }],
1892                ..Default::default()
1893            }),
1894        )
1895        .expect("standalone program generation should succeed");
1896
1897        assert!(!output.lib_rs.contains("mod entity"));
1898        assert!(!output.lib_rs.contains("StackViews"));
1899        assert!(!output.programs_rs.contains("EntityViews"));
1900        assert!(output.lib_rs.contains("pub use programs::DemoPrograms;"));
1901        assert!(output
1902            .programs_rs
1903            .contains("impl arete_sdk::ProgramSdk for DemoPrograms"));
1904        assert!(output
1905            .programs_rs
1906            .contains("impl arete_sdk::Programs for DemoPrograms"));
1907
1908        let base = std::env::temp_dir().join(format!(
1909            "arete-rust-program-codegen-{}-{:?}",
1910            std::process::id(),
1911            std::thread::current().id()
1912        ));
1913        let _ = std::fs::remove_dir_all(&base);
1914        write_rust_program_crate(&output, &base).expect("program crate should write");
1915        assert!(base.join("src/programs.rs").is_file());
1916        assert!(base.join("src/types.rs").is_file());
1917        assert!(!base.join("src/entity.rs").exists());
1918
1919        // Consumer smoke test: compile the generated crate against this
1920        // checkout's runtime. This catches invalid generated expressions,
1921        // module paths, re-exports, and dependency declarations.
1922        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1923            .parent()
1924            .expect("interpreter crate lives in the repo root");
1925        let runtime_path = repo_root.join("rust/arete-a4-sdk");
1926        let manifest_path = base.join("Cargo.toml");
1927        let mut manifest = std::fs::read_to_string(&manifest_path).expect("generated Cargo.toml");
1928        let generated_dependency = format!(
1929            "arete-sdk = {{ package = \"arete-a4-sdk\", version = {:?} }}",
1930            env!("CARGO_PKG_VERSION")
1931        );
1932        assert!(
1933            manifest.contains(&generated_dependency),
1934            "generated manifest must expose the expected runtime dependency"
1935        );
1936        manifest.push_str(&format!(
1937            "\n[patch.crates-io]\narete-a4-sdk = {{ path = {:?} }}\n",
1938            runtime_path
1939        ));
1940        std::fs::write(&manifest_path, manifest).expect("localize generated dependency");
1941
1942        let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
1943        let checked = Command::new(cargo)
1944            .args(["check", "--quiet", "--offline", "--manifest-path"])
1945            .arg(&manifest_path)
1946            .env("CARGO_TARGET_DIR", base.join("target"))
1947            .output()
1948            .expect("cargo must be available for generated consumer smoke tests");
1949        assert!(
1950            checked.status.success(),
1951            "generated standalone Rust crate failed cargo check:\nstdout:\n{}\nstderr:\n{}",
1952            String::from_utf8_lossy(&checked.stdout),
1953            String::from_utf8_lossy(&checked.stderr),
1954        );
1955        let _ = std::fs::remove_dir_all(&base);
1956    }
1957
1958    #[test]
1959    fn rust_program_compiler_keeps_idl_only_programs() {
1960        let mut spec = programs_stack_spec();
1961        spec.instructions.clear();
1962        let output = compile_program_modules(spec, None)
1963            .expect("an IDL-only standalone program should still be emitted");
1964        assert!(output.programs_rs.contains("pub mod demo"));
1965        assert!(output.programs_rs.contains("pub struct DemoProgram"));
1966        assert!(output.programs_rs.contains("pub struct DemoPrograms"));
1967    }
1968
1969    #[test]
1970    fn rust_generator_without_instructions_binds_unit_programs() {
1971        let mut spec = programs_stack_spec();
1972        spec.instructions.clear();
1973
1974        let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
1975
1976        assert!(output.programs_rs.is_none());
1977        assert!(output.entity_rs.contains("type Programs = ();"));
1978        assert!(!output.lib_rs.contains("pub mod programs;"));
1979        assert!(!output.entity_rs.contains("StackPrograms"));
1980    }
1981
1982    #[test]
1983    fn rust_generator_notes_skipped_instructions() {
1984        let mut spec = programs_stack_spec();
1985        spec.instructions.push(InstructionDef {
1986            name: "badThing".to_string(),
1987            discriminator: vec![9],
1988            discriminator_size: 1,
1989            accounts: vec![],
1990            args: vec![instruction_arg("payload", "MysteryType")],
1991            errors: vec![],
1992            program_id: Some(TEST_PROGRAM_ID.to_string()),
1993            docs: vec![],
1994        });
1995
1996        let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
1997        let programs = output.programs_rs.expect("programs.rs should be generated");
1998
1999        assert!(programs.contains("/// Skipped instructions (unsupported by instruction codegen):"));
2000        assert!(
2001            programs.contains("/// - `badThing`: arg 'payload' has unsupported type 'MysteryType'")
2002        );
2003        assert!(!programs.contains("BadThingParams"));
2004        // The supported instruction is still emitted.
2005        assert!(programs.contains("pub struct DoThingParams {"));
2006    }
2007
2008    #[test]
2009    fn rust_generator_emits_program_read_layer() {
2010        // Build a stack spec through the real program-spec pipeline so the
2011        // recorded ProgramSpecV1 hashes exactly like production specs.
2012        let idl_json = format!(
2013            r#"{{
2014              "address": "{TEST_PROGRAM_ID}",
2015              "version": "0.1.0",
2016              "name": "demo",
2017              "instructions": [
2018                {{
2019                  "name": "doThing",
2020                  "accounts": [{{ "name": "payer", "isMut": true, "isSigner": true }}],
2021                  "args": [{{ "name": "amount", "type": "u64" }}],
2022                  "discriminant": {{ "type": "u8", "value": 1 }}
2023                }}
2024              ],
2025              "accounts": [
2026                {{
2027                  "name": "Counter",
2028                  "type": {{
2029                    "kind": "struct",
2030                    "fields": [{{ "name": "count", "type": "u64" }}]
2031                  }}
2032                }}
2033              ],
2034              "types": [],
2035              "events": [],
2036              "errors": []
2037            }}"#
2038        );
2039        let mut spec = crate::program_sdk::build_program_only_stack_spec_from_idl_bytes(
2040            idl_json.as_bytes(),
2041            None,
2042            "Demo",
2043        )
2044        .expect("program-only stack spec should build");
2045        let expected_spec_hash = spec.program_specs[0].hash().unwrap().to_string();
2046        let expected_release_hash = spec.program_specs[0]
2047            .oss_release_hash()
2048            .unwrap()
2049            .to_string();
2050
2051        // One entity whose raw account struct (`Counter`) is emitted in types.rs.
2052        let mut entity = minimal_entity("DemoThing");
2053        entity.sections.push(EntitySection {
2054            name: "state".to_string(),
2055            fields: vec![FieldTypeInfo {
2056                field_name: "counter".to_string(),
2057                raw_name: Some("counter".to_string()),
2058                canonical_name: Some("counter".to_string()),
2059                rust_type_name: "Option<serde_json::Value>".to_string(),
2060                base_type: BaseType::Object,
2061                integer_kind: None,
2062                is_optional: false,
2063                is_array: false,
2064                inner_type: Some("Value".to_string()),
2065                source_path: None,
2066                resolved_type: Some(ResolvedStructType {
2067                    type_name: "Counter".to_string(),
2068                    fields: vec![],
2069                    is_instruction: false,
2070                    is_account: true,
2071                    is_event: false,
2072                    is_enum: false,
2073                    enum_variants: vec![],
2074                }),
2075                emit: true,
2076            }],
2077            is_nested_struct: false,
2078            parent_field: None,
2079        });
2080        spec.entities.push(entity);
2081
2082        let output = compile_stack_spec(spec, None).expect("rust stack generation should succeed");
2083        let programs = output.programs_rs.expect("programs.rs should be generated");
2084
2085        // Release identity consts + descriptor.
2086        assert!(programs.contains(&format!(
2087            "pub const PROGRAM_SPEC_HASH: &str = \"{expected_spec_hash}\";"
2088        )));
2089        assert!(programs.contains(&format!(
2090            "pub const PROGRAM_RELEASE_HASH: &str = \"{expected_release_hash}\";"
2091        )));
2092        assert!(programs.contains("pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor"));
2093        assert!(programs.contains("arete_sdk::ProgramReadDescriptor::LocalHttp"));
2094        assert!(!programs.contains("Program read layer omitted"));
2095
2096        // Typed account reader for the emitted `Counter` struct.
2097        assert!(output.types_rs.contains("pub struct Counter"));
2098        assert!(programs.contains(
2099            "pub fn counter_accounts(&self) -> Result<arete_sdk::AccountReader<crate::types::Counter>, arete_sdk::AreteError>"
2100        ));
2101        assert!(programs.contains("self.builder.account_transport(\"demo\", &read_descriptor())?"));
2102        assert!(programs.contains("arete_sdk::AccountReader::new(\n                \"Counter\","));
2103    }
2104
2105    #[test]
2106    fn rust_generator_emits_platform_release_override() {
2107        let idl_json = format!(
2108            r#"{{
2109              "address": "{TEST_PROGRAM_ID}",
2110              "version": "0.1.0",
2111              "name": "demo",
2112              "instructions": [
2113                {{
2114                  "name": "doThing",
2115                  "accounts": [{{ "name": "payer", "isMut": true, "isSigner": true }}],
2116                  "args": [{{ "name": "amount", "type": "u64" }}],
2117                  "discriminant": {{ "type": "u8", "value": 1 }}
2118                }}
2119              ],
2120              "accounts": [],
2121              "types": [],
2122              "events": [],
2123              "errors": []
2124            }}"#
2125        );
2126        let spec = crate::program_sdk::build_program_only_stack_spec_from_idl_bytes(
2127            idl_json.as_bytes(),
2128            None,
2129            "Demo",
2130        )
2131        .expect("program-only stack spec should build");
2132
2133        // A platform release override must win over the OSS-derived hash.
2134        let platform_spec = "arete:h1:program-spec:sha256:platformspec".to_string();
2135        let platform_release = "arete:h1:program-release:sha256:platformrelease".to_string();
2136        let config = RustStackConfig {
2137            program_reads: vec![RustProgramReadConfig {
2138                program_id: TEST_PROGRAM_ID.to_string(),
2139                program_spec_hash: platform_spec.clone(),
2140                program_release_hash: platform_release.clone(),
2141                descriptor: Some(serde_json::json!({
2142                    "release": {
2143                        "programReleaseHash": platform_release.clone(),
2144                        "programSpecHash": platform_spec.clone(),
2145                    },
2146                    "transport": {"kind": "hosted-binding", "binding": {
2147                        "endpoint": "https://reads.example.test",
2148                        "programReadBindingId": "prb_00000000000000000000000000000001",
2149                        "auth": {
2150                            "sessionEndpoint": "https://auth.example.test/session",
2151                            "targetKind": "program-read-binding",
2152                            "targetId": "prb_00000000000000000000000000000001"
2153                        }
2154                    }}
2155                })),
2156            }],
2157            ..Default::default()
2158        };
2159
2160        let output =
2161            compile_stack_spec(spec, Some(config)).expect("rust stack generation should succeed");
2162        let programs = output.programs_rs.expect("programs.rs should be generated");
2163
2164        assert!(programs.contains(&format!(
2165            "pub const PROGRAM_SPEC_HASH: &str = \"{platform_spec}\";"
2166        )));
2167        assert!(programs.contains(&format!(
2168            "pub const PROGRAM_RELEASE_HASH: &str = \"{platform_release}\";"
2169        )));
2170        assert!(programs.contains("pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor"));
2171        assert!(programs.contains("\\\"kind\\\":\\\"hosted-binding\\\""));
2172    }
2173
2174    #[test]
2175    fn rust_generator_emits_stack_http_url_override() {
2176        let output = compile_stack_spec(programs_stack_spec(), None)
2177            .expect("rust stack generation should succeed");
2178        assert!(!output.entity_rs.contains("fn http_url"));
2179
2180        let config = RustStackConfig {
2181            http_url: Some("https://demo.stack.example".to_string()),
2182            ..Default::default()
2183        };
2184        let output = compile_stack_spec(programs_stack_spec(), Some(config))
2185            .expect("rust stack generation should succeed");
2186        assert!(output.entity_rs.contains(
2187            "fn http_url() -> &'static str {\n        \"https://demo.stack.example\"\n    }"
2188        ));
2189    }
2190
2191    #[test]
2192    fn rust_generator_wires_extension_modules_after_generated_decls() {
2193        let config = RustStackConfig {
2194            module_mode: true,
2195            extension_modules: vec!["devex".to_string(), "extensions".to_string()],
2196            extension_entry: Some("extensions".to_string()),
2197            ..Default::default()
2198        };
2199        let output = compile_stack_spec(programs_stack_spec(), Some(config))
2200            .expect("rust stack generation should succeed");
2201        let mod_rs = output.mod_rs();
2202
2203        assert!(mod_rs.contains(
2204            "// Hand-authored devex extensions (staged from extensions.json; not generated)."
2205        ));
2206        let sdk_reexport = mod_rs.find("pub use arete_sdk::").expect("sdk re-export");
2207        let devex = mod_rs.find("pub mod devex;").expect("devex module decl");
2208        let entry = mod_rs
2209            .find("pub mod extensions;")
2210            .expect("entry module decl");
2211        let entry_reexport = mod_rs
2212            .find("pub use extensions::*;")
2213            .expect("entry glob re-export");
2214        assert!(sdk_reexport < devex);
2215        assert!(devex < entry);
2216        assert!(entry < entry_reexport);
2217        assert!(!mod_rs.contains("pub use devex::*;"));
2218    }
2219
2220    #[test]
2221    fn rust_generator_omits_extension_wiring_without_entry() {
2222        let output = compile_stack_spec(programs_stack_spec(), None)
2223            .expect("rust stack generation should succeed");
2224
2225        assert!(!output.mod_rs().contains("Hand-authored devex extensions"));
2226        assert!(!output.mod_rs().contains("pub mod extensions;"));
2227    }
2228
2229    #[test]
2230    fn rust_generator_rejects_extension_module_collisions() {
2231        for reserved in ["entity", "types", "programs"] {
2232            let config = RustStackConfig {
2233                extension_modules: vec![reserved.to_string(), "extensions".to_string()],
2234                extension_entry: Some("extensions".to_string()),
2235                ..Default::default()
2236            };
2237            let error = compile_stack_spec(programs_stack_spec(), Some(config))
2238                .expect_err("collision with a generated module must fail");
2239            assert!(
2240                error.contains(&format!("'{reserved}.rs'")),
2241                "collision error should name the file: {error}"
2242            );
2243        }
2244
2245        let duplicate = RustStackConfig {
2246            extension_modules: vec![
2247                "devex".to_string(),
2248                "devex".to_string(),
2249                "extensions".to_string(),
2250            ],
2251            extension_entry: Some("extensions".to_string()),
2252            ..Default::default()
2253        };
2254        assert!(compile_stack_spec(programs_stack_spec(), Some(duplicate)).is_err());
2255
2256        let entry_not_last = RustStackConfig {
2257            extension_modules: vec!["extensions".to_string(), "devex".to_string()],
2258            extension_entry: Some("extensions".to_string()),
2259            ..Default::default()
2260        };
2261        assert!(compile_stack_spec(programs_stack_spec(), Some(entry_not_last)).is_err());
2262    }
2263
2264    /// Regeneration helper for the checked-in ore example. Run with:
2265    /// `cargo test -p arete-interpreter regenerate_ore_example -- --ignored`
2266    ///
2267    /// Rewrites `examples/ore-rust/src/generated/ore/{mod,types,entity,programs}.rs`
2268    /// from `stacks/ore/.arete/OreStream.stack.json`.
2269    ///
2270    /// Extension wiring reuses the `extensions.json` staged in the output
2271    /// directory (files sorted, entry last, stems via [`rust_module_name`]) โ€”
2272    /// a faithful replica of the CLI's output-dir manifest resolution step,
2273    /// which lives in `a4-cli` and cannot be called from this crate. Staged
2274    /// extension files are preserved verbatim, so a second run is a byte-stable
2275    /// fixed point.
2276    #[test]
2277    #[ignore = "writes into examples/ore-rust; run explicitly to regenerate"]
2278    fn regenerate_ore_example() {
2279        let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2280            .parent()
2281            .expect("interpreter crate lives in the repo root")
2282            .to_path_buf();
2283        let spec_json =
2284            std::fs::read_to_string(repo_root.join("stacks/ore/.arete/OreStream.stack.json"))
2285                .expect("ore stack spec should exist");
2286        let spec = crate::versioned::load_stack_spec(&spec_json)
2287            .expect("ore stack spec should deserialize");
2288
2289        let out_dir = repo_root.join("examples/ore-rust/src/generated/ore");
2290        let (extension_modules, extension_entry) =
2291            match std::fs::read_to_string(out_dir.join("extensions.json")) {
2292                Ok(manifest_json) => {
2293                    let manifest: serde_json::Value = serde_json::from_str(&manifest_json)
2294                        .expect("staged extensions.json should parse");
2295                    let language = manifest["language"].as_str();
2296                    assert!(
2297                        language.is_none() || language == Some("rust"),
2298                        "staged ore extensions must be a Rust bundle"
2299                    );
2300                    let entry_stem = rust_module_name(
2301                        manifest["entry"]
2302                            .as_str()
2303                            .and_then(|entry| entry.strip_suffix(".rs"))
2304                            .expect("extensions entry should be a .rs file"),
2305                    );
2306                    let mut stems: Vec<String> = manifest["files"]
2307                        .as_array()
2308                        .expect("extensions files should be an array")
2309                        .iter()
2310                        .map(|file| {
2311                            rust_module_name(
2312                                file.as_str()
2313                                    .and_then(|file| file.strip_suffix(".rs"))
2314                                    .expect("extension files should be .rs files"),
2315                            )
2316                        })
2317                        .filter(|stem| stem != &entry_stem)
2318                        .collect();
2319                    stems.sort();
2320                    stems.dedup();
2321                    stems.push(entry_stem.clone());
2322                    (stems, Some(entry_stem))
2323                }
2324                Err(_) => (Vec::new(), None),
2325            };
2326
2327        let config = RustStackConfig {
2328            crate_name: "ore-stack".to_string(),
2329            sdk_version: "0.4".to_string(),
2330            module_mode: true,
2331            url: Some("wss://ore.stack.arete.run".to_string()),
2332            http_url: Some("https://ore.stack.arete.run".to_string()),
2333            extension_modules,
2334            extension_entry,
2335            program_reads: Vec::new(),
2336        };
2337        let output =
2338            compile_stack_spec(spec, Some(config)).expect("ore stack should compile to Rust");
2339
2340        std::fs::write(out_dir.join("mod.rs"), output.mod_rs()).unwrap();
2341        std::fs::write(out_dir.join("types.rs"), &output.types_rs).unwrap();
2342        std::fs::write(out_dir.join("entity.rs"), &output.entity_rs).unwrap();
2343        std::fs::write(
2344            out_dir.join("programs.rs"),
2345            output.programs_rs.as_deref().expect("ore has instructions"),
2346        )
2347        .unwrap();
2348    }
2349}
2350
2351// ============================================================================
2352// Stack-level compilation (multi-entity)
2353// ============================================================================
2354
2355#[derive(Debug, Clone)]
2356pub struct RustStackConfig {
2357    pub crate_name: String,
2358    pub sdk_version: String,
2359    pub module_mode: bool,
2360    pub url: Option<String>,
2361    /// HTTP base URL for the stack (account reads / queries / chain reads).
2362    /// When set and non-empty, the generated Stack impl overrides
2363    /// `Stack::http_url`; otherwise the runtime derives the HTTP endpoint
2364    /// from the WebSocket URL.
2365    pub http_url: Option<String>,
2366    /// Module stems of hand-authored devex extension files staged next to the
2367    /// generated output (one `pub mod <stem>;` each, in order, entry last).
2368    /// Stems are derived from the staged file names via [`rust_module_name`].
2369    pub extension_modules: Vec<String>,
2370    /// Module stem of the extension entry file. When set, the generated
2371    /// `mod.rs`/`lib.rs` re-exports the entry at the stack module root
2372    /// (`pub use <entry>::*;`) so extension traits come into scope with the
2373    /// stack's own glob import.
2374    pub extension_entry: Option<String>,
2375    /// Published-platform program read overrides keyed by program ID. When an
2376    /// entry matches a program, its exact `program_spec_hash` /
2377    /// `program_release_hash` (from a hosted platform release) are used for
2378    /// the program read layer instead of the OSS-derived release identity.
2379    /// Absent programs keep the default OSS/local-HTTP read layer. Published
2380    /// standalone programs may additionally carry their exact hosted-binding
2381    /// descriptor so they do not inherit a stack HTTP endpoint.
2382    pub program_reads: Vec<RustProgramReadConfig>,
2383}
2384
2385#[derive(Debug, Clone)]
2386pub struct RustProgramReadConfig {
2387    pub program_id: String,
2388    pub program_spec_hash: String,
2389    pub program_release_hash: String,
2390    /// Exact wire descriptor for a published hosted binding. `None` keeps
2391    /// the local-HTTP descriptor used by locally generated stack SDKs.
2392    pub descriptor: Option<serde_json::Value>,
2393}
2394
2395#[derive(Debug, Clone, Default)]
2396pub struct RustCompositionConfig {
2397    pub stack: RustStackConfig,
2398    pub live_urls: BTreeMap<String, String>,
2399}
2400
2401#[derive(Debug, Clone)]
2402pub struct RustAliasedStackOutput {
2403    pub alias: String,
2404    pub module_name: String,
2405    pub output: RustOutput,
2406}
2407
2408#[derive(Debug, Clone)]
2409pub struct RustCompositionOutput {
2410    pub name: String,
2411    pub cargo_toml: String,
2412    pub lib_rs: String,
2413    pub live_stacks: Vec<RustAliasedStackOutput>,
2414}
2415
2416impl Default for RustStackConfig {
2417    fn default() -> Self {
2418        Self {
2419            crate_name: "generated-stack".to_string(),
2420            sdk_version: "0.4".to_string(),
2421            module_mode: false,
2422            url: None,
2423            http_url: None,
2424            extension_modules: Vec::new(),
2425            extension_entry: None,
2426            program_reads: Vec::new(),
2427        }
2428    }
2429}
2430
2431/// Compile a full SerializableStackSpec (multi-entity) into unified Rust output.
2432///
2433/// Generates types.rs with ALL entity structs, entity.rs with a single Stack impl
2434/// and per-entity EntityViews, and mod.rs/lib.rs re-exporting everything.
2435pub fn compile_stack_spec(
2436    stack_spec: SerializableStackSpec,
2437    config: Option<RustStackConfig>,
2438) -> Result<RustOutput, String> {
2439    compile_stack_spec_with_view_selection(stack_spec, config, false)
2440}
2441
2442fn compile_stack_spec_with_view_selection(
2443    stack_spec: SerializableStackSpec,
2444    config: Option<RustStackConfig>,
2445    exact_views: bool,
2446) -> Result<RustOutput, String> {
2447    let config = config.unwrap_or_default();
2448    let stack_name = &stack_spec.stack_name;
2449    let stack_kebab = to_kebab_case(stack_name);
2450
2451    let mut entity_names: Vec<String> = Vec::new();
2452    let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
2453
2454    for mut spec in stack_spec.entities {
2455        if spec.idl.is_none() {
2456            spec.idl = stack_spec.idls.first().cloned();
2457        }
2458        entity_names.push(spec.state_name.clone());
2459        entity_specs.push(spec);
2460    }
2461
2462    let view_entity_names = entity_specs
2463        .iter()
2464        .zip(&entity_names)
2465        .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2466        .map(|(_, name)| name.clone())
2467        .collect::<Vec<_>>();
2468
2469    let (types_rs, account_structs) = generate_stack_types_rs(&entity_specs, &entity_names);
2470
2471    let programs = generate_stack_programs_rs(
2472        stack_name,
2473        &stack_spec.instructions,
2474        &stack_spec.idls,
2475        &stack_spec.pdas,
2476        &stack_spec.program_ids,
2477        &stack_spec.program_specs,
2478        &account_structs,
2479        config.module_mode,
2480        &config.program_reads,
2481        false,
2482    );
2483    let entity_rs = generate_stack_entity_rs(
2484        stack_name,
2485        &stack_kebab,
2486        &entity_specs,
2487        &entity_names,
2488        &config,
2489        exact_views,
2490        programs.as_ref(),
2491    );
2492    validate_extension_modules(&config, programs.is_some())?;
2493    let lib_rs = generate_stack_lib_rs(
2494        stack_name,
2495        &view_entity_names,
2496        config.module_mode,
2497        programs.is_some(),
2498        &config.extension_modules,
2499        config.extension_entry.as_deref(),
2500    );
2501    let cargo_toml = generate_stack_cargo_toml(&config);
2502
2503    Ok(RustOutput {
2504        cargo_toml,
2505        lib_rs,
2506        types_rs,
2507        entity_rs,
2508        programs_rs: programs.map(|codegen| codegen.code),
2509    })
2510}
2511
2512/// Compile a stack model whose `views` have already been projected by a
2513/// StackManifest selected-view allowlist.
2514pub fn compile_stack_spec_with_exact_views(
2515    stack_spec: SerializableStackSpec,
2516    config: Option<RustStackConfig>,
2517) -> Result<RustOutput, String> {
2518    compile_stack_spec_with_view_selection(stack_spec, config, true)
2519}
2520
2521/// Compile only the portable program surface: generated account/model types,
2522/// instruction builders, PDA helpers, read descriptors, and a `ProgramSdk`
2523/// aggregate. No entity, view, or stack binding is emitted.
2524pub fn compile_program_modules(
2525    stack_spec: SerializableStackSpec,
2526    config: Option<RustStackConfig>,
2527) -> Result<RustProgramOutput, String> {
2528    let config = config.unwrap_or_default();
2529    if stack_spec.idls.is_empty() {
2530        return Err(format!(
2531            "Stack '{}' carries no IDLs; a program-only SDK has nothing to emit",
2532            stack_spec.stack_name
2533        ));
2534    }
2535
2536    let entity_names = stack_spec
2537        .entities
2538        .iter()
2539        .map(|entity| entity.state_name.clone())
2540        .collect::<Vec<_>>();
2541    let (types_rs, account_structs) = generate_stack_types_rs(&stack_spec.entities, &entity_names);
2542    let mut programs = generate_stack_programs_rs(
2543        &stack_spec.stack_name,
2544        &stack_spec.instructions,
2545        &stack_spec.idls,
2546        &stack_spec.pdas,
2547        &stack_spec.program_ids,
2548        &stack_spec.program_specs,
2549        &account_structs,
2550        config.module_mode,
2551        &config.program_reads,
2552        true,
2553    )
2554    .ok_or_else(|| {
2555        format!(
2556            "Stack '{}' contains no programs to emit",
2557            stack_spec.stack_name
2558        )
2559    })?;
2560
2561    validate_extension_modules(&config, true)?;
2562    let aggregate_name = format!("{}Programs", to_pascal_case(&stack_spec.stack_name));
2563    programs.code.push('\n');
2564    programs.code.push_str(&generate_programs_accessor_struct(
2565        &aggregate_name,
2566        &programs,
2567        "self",
2568    ));
2569    programs.code.push_str(&format!(
2570        "\n\nimpl arete_sdk::ProgramSdk for {aggregate_name} {{\n    fn name() -> &'static str {{\n        {}\n    }}\n}}\n",
2571        rust_string_literal(&to_kebab_case(&stack_spec.stack_name)),
2572    ));
2573
2574    let mut lib_rs = format!(
2575        "mod types;\npub mod programs;\n\npub use programs::{aggregate_name};\npub use types::*;\n\npub use arete_sdk::{{ProgramSdk, Programs}};\n"
2576    );
2577    append_rust_extension_exports(
2578        &mut lib_rs,
2579        &config.extension_modules,
2580        config.extension_entry.as_deref(),
2581    );
2582
2583    Ok(RustProgramOutput {
2584        cargo_toml: generate_stack_cargo_toml(&config),
2585        lib_rs,
2586        types_rs,
2587        programs_rs: programs.code,
2588    })
2589}
2590
2591/// Compile Rust output from an explicit StackManifest and its public dependencies.
2592pub fn compile_public_artifacts(
2593    programs: &[arete_artifacts::ProgramSpecArtifact],
2594    live_spec: &arete_artifacts::LiveSpecArtifact,
2595    manifest: &arete_artifacts::StackManifestArtifact,
2596    config: Option<RustStackConfig>,
2597) -> Result<RustOutput, String> {
2598    let stack_spec =
2599        crate::public_artifacts::stack_spec_from_artifacts(programs, live_spec, manifest)?;
2600    compile_stack_spec(stack_spec, config)
2601}
2602
2603/// Compile typed V2 public artifacts through the current single-live generator.
2604pub fn compile_public_artifacts_v2(
2605    programs: &[arete_artifacts::ProgramSpecArtifact],
2606    live_spec: &arete_artifacts::LiveSpecArtifactV2,
2607    manifest: &arete_artifacts::StackManifestArtifactV2,
2608    config: Option<RustStackConfig>,
2609) -> Result<RustOutput, String> {
2610    let stack_spec =
2611        crate::public_artifacts::stack_spec_from_artifacts_v2(programs, live_spec, manifest)?;
2612    compile_stack_spec_with_view_selection(stack_spec, config, true)
2613}
2614
2615/// Generate one namespaced Rust stack module per live alias plus a manifest
2616/// module that preserves alias boundaries instead of flattening views/adapters.
2617pub fn compile_composed_public_artifacts_v2(
2618    programs: &[arete_artifacts::ProgramSpecArtifact],
2619    live_specs: &[(String, arete_artifacts::LiveSpecArtifactV2)],
2620    manifest: &arete_artifacts::StackManifestArtifactV2,
2621    config: Option<RustCompositionConfig>,
2622) -> Result<RustCompositionOutput, String> {
2623    let composed =
2624        crate::public_artifacts::stack_specs_from_artifacts_v2(programs, live_specs, manifest)?;
2625    if composed.live_specs.is_empty() {
2626        return Err(
2627            "Rust composition generation requires at least one aliased LiveSpec".to_string(),
2628        );
2629    }
2630    let config = config.unwrap_or_default();
2631    if !config.stack.extension_modules.is_empty() || config.stack.extension_entry.is_some() {
2632        return Err(
2633            "Rust composition SDKs do not support stack extensions; extensions attach to a single-live stack module".to_string(),
2634        );
2635    }
2636    let mut live_stacks = Vec::with_capacity(composed.live_specs.len());
2637    for live in composed.live_specs {
2638        let module_name = rust_module_name(&live.alias);
2639        let mut live_config = config.stack.clone();
2640        live_config.module_mode = true;
2641        live_config.url = config.live_urls.get(&live.alias).cloned();
2642        let output =
2643            compile_stack_spec_with_view_selection(live.stack_spec, Some(live_config), true)?;
2644        live_stacks.push(RustAliasedStackOutput {
2645            alias: live.alias,
2646            module_name,
2647            output,
2648        });
2649    }
2650    let lib_rs = live_stacks
2651        .iter()
2652        .map(|live| format!("pub mod {};", live.module_name))
2653        .collect::<Vec<_>>()
2654        .join("\n");
2655    Ok(RustCompositionOutput {
2656        name: composed.name,
2657        cargo_toml: generate_stack_cargo_toml(&config.stack),
2658        lib_rs: format!("{lib_rs}\n"),
2659        live_stacks,
2660    })
2661}
2662
2663pub fn write_rust_composition_crate(
2664    output: &RustCompositionOutput,
2665    crate_dir: &std::path::Path,
2666) -> Result<(), std::io::Error> {
2667    let source = crate_dir.join("src");
2668    std::fs::create_dir_all(&source)?;
2669    std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
2670    std::fs::write(source.join("lib.rs"), &output.lib_rs)?;
2671    for live in &output.live_stacks {
2672        write_rust_module(&live.output, &source.join(&live.module_name))?;
2673    }
2674    Ok(())
2675}
2676
2677pub fn write_rust_composition_module(
2678    output: &RustCompositionOutput,
2679    module_dir: &std::path::Path,
2680) -> Result<(), std::io::Error> {
2681    std::fs::create_dir_all(module_dir)?;
2682    std::fs::write(module_dir.join("mod.rs"), &output.lib_rs)?;
2683    for live in &output.live_stacks {
2684        write_rust_module(&live.output, &module_dir.join(&live.module_name))?;
2685    }
2686    Ok(())
2687}
2688
2689fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
2690    format!(
2691        r#"[package]
2692name = "{}"
2693version = "0.1.0"
2694edition = "2021"
2695
2696[dependencies]
2697arete-sdk = {{ package = "arete-a4-sdk", version = "{}" }}
2698serde = {{ version = "1", features = ["derive"] }}
2699serde_json = "1"
2700"#,
2701        config.crate_name, config.sdk_version
2702    )
2703}
2704
2705/// Validate hand-authored extension module stems against the generated
2706/// module names. Entry-stem collisions are a hard error because the staged
2707/// file would shadow (or be shadowed by) a generated file.
2708fn validate_extension_modules(config: &RustStackConfig, has_programs: bool) -> Result<(), String> {
2709    if config.extension_modules.is_empty() && config.extension_entry.is_none() {
2710        return Ok(());
2711    }
2712    if config.extension_entry.is_none() {
2713        return Err("extension modules were configured without an extension entry".to_string());
2714    }
2715    let mut seen = HashSet::new();
2716    for stem in &config.extension_modules {
2717        let reserved = matches!(stem.as_str(), "entity" | "types" | "mod" | "lib")
2718            || (stem == "programs" && has_programs);
2719        if reserved {
2720            return Err(format!(
2721                "extension file '{stem}.rs' collides with the generated '{stem}' module; rename the extension file"
2722            ));
2723        }
2724        if !seen.insert(stem.as_str()) {
2725            return Err(format!(
2726                "extension file '{stem}.rs' resolves to the same module name as another staged extension file"
2727            ));
2728        }
2729    }
2730    match &config.extension_entry {
2731        Some(entry) if config.extension_modules.last() == Some(entry) => Ok(()),
2732        Some(entry) => Err(format!(
2733            "extension entry module '{entry}' must be the last configured extension module"
2734        )),
2735        None => unreachable!("checked above"),
2736    }
2737}
2738
2739fn generate_stack_lib_rs(
2740    stack_name: &str,
2741    entity_names: &[String],
2742    _module_mode: bool,
2743    has_programs: bool,
2744    extension_modules: &[String],
2745    extension_entry: Option<&str>,
2746) -> String {
2747    let entity_views_exports: Vec<String> = entity_names
2748        .iter()
2749        .map(|name| format!("{}EntityViews", name))
2750        .collect();
2751
2752    let mut all_exports = format!(
2753        "{}Stack, {}StackViews, {}",
2754        stack_name,
2755        stack_name,
2756        entity_views_exports.join(", ")
2757    );
2758    if has_programs {
2759        all_exports.push_str(&format!(", {}StackPrograms", stack_name));
2760    }
2761
2762    let programs_mod = if has_programs {
2763        "\npub mod programs;"
2764    } else {
2765        ""
2766    };
2767
2768    let mut output = format!(
2769        r#"mod entity;
2770mod types;{programs_mod}
2771
2772pub use entity::{{{all_exports}}};
2773pub use types::*;
2774
2775pub use arete_sdk::{{ConnectionState, Arete, Stack, Update, Views}};
2776"#,
2777        programs_mod = programs_mod,
2778        all_exports = all_exports
2779    );
2780
2781    append_rust_extension_exports(&mut output, extension_modules, extension_entry);
2782
2783    output
2784}
2785
2786fn append_rust_extension_exports(
2787    output: &mut String,
2788    extension_modules: &[String],
2789    extension_entry: Option<&str>,
2790) {
2791    if let Some(entry) = extension_entry {
2792        output.push_str(
2793            "\n// Hand-authored devex extensions (staged from extensions.json; not generated).\n",
2794        );
2795        for stem in extension_modules {
2796            output.push_str(&format!("pub mod {stem};\n"));
2797        }
2798        output.push_str(&format!("pub use {entry}::*;\n"));
2799    }
2800}
2801
2802/// Generate types.rs containing structs for ALL entities in the stack.
2803///
2804/// Also returns the map of emitted raw account structs (IDL account type name
2805/// -> emitted Rust struct name) so the program SDK generator can attach typed
2806/// account readers for accounts that actually have a generated struct.
2807fn generate_stack_types_rs(
2808    entity_specs: &[SerializableStreamSpec],
2809    entity_names: &[String],
2810) -> (String, BTreeMap<String, String>) {
2811    let mut output = String::new();
2812    output.push_str("use serde::{Deserialize, Serialize};\n");
2813    output.push_str("use arete_sdk::serde_utils;\n\n");
2814
2815    let mut generated = HashSet::new();
2816    let mut account_structs: BTreeMap<String, String> = BTreeMap::new();
2817    let mut used_builtins: BTreeSet<&'static str> = BTreeSet::new();
2818
2819    for (i, spec) in entity_specs.iter().enumerate() {
2820        let entity_name = &entity_names[i];
2821        let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
2822        let resolved_name_map = compiler.build_resolved_type_name_map();
2823        used_builtins.extend(compiler.used_builtin_resolver_types());
2824
2825        // Generate section structs (e.g., OreRoundId, OreRoundState)
2826        for section in &spec.sections {
2827            if !RustCompiler::is_root_section(&section.name) {
2828                let struct_name = format!("{}{}", entity_name, to_pascal_case(&section.name));
2829                if generated.insert(struct_name) {
2830                    output.push_str(
2831                        &compiler.generate_struct_for_section(section, &resolved_name_map),
2832                    );
2833                    output.push_str("\n\n");
2834                }
2835            }
2836        }
2837
2838        // Generate main entity struct (e.g., OreRound, OreTreasury)
2839        output.push_str(&compiler.generate_main_entity_struct(&resolved_name_map));
2840        output.push_str("\n\n");
2841
2842        let resolved = compiler.generate_resolved_types(
2843            &resolved_name_map,
2844            &mut generated,
2845            Some(&mut account_structs),
2846        );
2847        output.push_str(&resolved);
2848        while !output.ends_with("\n\n") {
2849            output.push('\n');
2850        }
2851    }
2852
2853    // Generate the builtin resolver output structs (SlotHashBytes /
2854    // TokenMetadata) once, for the whole stack.
2855    output.push_str(&render_builtin_resolver_structs(&used_builtins));
2856
2857    // Generate the runtime envelopes (EventWrapper / CaptureWrapper) once.
2858    output.push('\n');
2859    output.push_str(WRAPPER_TYPES);
2860
2861    (output, account_structs)
2862}
2863
2864/// Generate entity.rs with a single Stack impl and per-entity EntityViews.
2865fn generate_stack_entity_rs(
2866    stack_name: &str,
2867    stack_kebab: &str,
2868    entity_specs: &[SerializableStreamSpec],
2869    entity_names: &[String],
2870    config: &RustStackConfig,
2871    exact_views: bool,
2872    programs: Option<&ProgramsCodegen>,
2873) -> String {
2874    let types_import = if config.module_mode {
2875        "super::types"
2876    } else {
2877        "crate::types"
2878    };
2879
2880    let selected_entities = entity_specs
2881        .iter()
2882        .zip(entity_names)
2883        .filter(|(spec, _)| !exact_views || !spec.views.is_empty())
2884        .collect::<Vec<_>>();
2885    let entity_type_imports = selected_entities
2886        .iter()
2887        .map(|(_, name)| (*name).to_string())
2888        .collect::<Vec<_>>();
2889
2890    let url_impl = match &config.url {
2891        Some(url) => format!(
2892            r#"fn url() -> &'static str {{
2893        "{}"
2894    }}"#,
2895            url
2896        ),
2897        None => r#"fn url() -> &'static str {
2898        "" // TODO: Set URL after first deployment in arete.toml
2899    }"#
2900        .to_string(),
2901    };
2902
2903    // Optional HTTP base URL override (account reads / queries / chain reads).
2904    let http_url_impl = match config.http_url.as_deref() {
2905        Some(http_url) if !http_url.is_empty() => format!(
2906            r#"
2907
2908    fn http_url() -> &'static str {{
2909        "{}"
2910    }}"#,
2911            http_url
2912        ),
2913        _ => String::new(),
2914    };
2915
2916    // StackViews struct fields
2917    let views_fields: Vec<String> = selected_entities
2918        .iter()
2919        .map(|(_, name)| {
2920            let snake = to_snake_case(name);
2921            format!("    pub {}: {}EntityViews,", snake, name)
2922        })
2923        .collect();
2924
2925    // Views::from_builder body โ€” clone builder for all but last entity
2926    let views_builder_fields: Vec<String> = selected_entities
2927        .iter()
2928        .enumerate()
2929        .map(|(i, (_, name))| {
2930            let snake = to_snake_case(name);
2931            if i < selected_entities.len() - 1 {
2932                format!(
2933                    "            {}: {}EntityViews {{ builder: builder.clone() }},",
2934                    snake, name
2935                )
2936            } else {
2937                format!("            {}: {}EntityViews {{ builder }},", snake, name)
2938            }
2939        })
2940        .collect();
2941
2942    // Per-entity EntityViews structs
2943    let mut entity_views_structs = Vec::new();
2944    for (i, entity_name) in entity_names.iter().enumerate() {
2945        let spec = &entity_specs[i];
2946        if exact_views && spec.views.is_empty() {
2947            continue;
2948        }
2949
2950        let derived: Vec<_> = spec
2951            .views
2952            .iter()
2953            .filter(|v| {
2954                !v.id.ends_with("/state")
2955                    && !v.id.ends_with("/list")
2956                    && v.id.starts_with(entity_name.as_str())
2957            })
2958            .collect();
2959
2960        let mut methods = Vec::new();
2961
2962        if !exact_views
2963            || spec
2964                .views
2965                .iter()
2966                .any(|view| view.id == format!("{entity_name}/state"))
2967        {
2968            methods.push(format!(
2969                r#"    pub fn state(&self) -> StateView<{entity}> {{
2970        StateView::new(
2971            self.builder.connection().clone(),
2972            self.builder.store().clone(),
2973            "{entity}/state".to_string(),
2974            self.builder.initial_data_timeout(),
2975        )
2976    }}"#,
2977                entity = entity_name
2978            ));
2979        }
2980
2981        if !exact_views
2982            || spec
2983                .views
2984                .iter()
2985                .any(|view| view.id == format!("{entity_name}/list"))
2986        {
2987            methods.push(format!(
2988                r#"
2989    pub fn list(&self) -> ViewHandle<{entity}> {{
2990        self.builder.view("{entity}/list")
2991    }}"#,
2992                entity = entity_name
2993            ));
2994        }
2995
2996        // Derived view methods
2997        for view in &derived {
2998            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
2999            let method_name = to_snake_case(view_name);
3000            methods.push(format!(
3001                r#"
3002    pub fn {method}(&self) -> ViewHandle<{entity}> {{
3003        self.builder.view("{view_id}")
3004    }}"#,
3005                method = method_name,
3006                entity = entity_name,
3007                view_id = view.id
3008            ));
3009        }
3010
3011        entity_views_structs.push(format!(
3012            r#"
3013pub struct {entity}EntityViews {{
3014    builder: ViewBuilder,
3015}}
3016
3017impl {entity}EntityViews {{
3018{methods}
3019}}"#,
3020            entity = entity_name,
3021            methods = methods.join("\n")
3022        ));
3023    }
3024
3025    let types_use = if entity_type_imports.is_empty() {
3026        String::new()
3027    } else {
3028        format!(
3029            "use {types_import}::{{{}}};\n",
3030            entity_type_imports.join(", ")
3031        )
3032    };
3033    let empty_builder = if selected_entities.is_empty() {
3034        "        let _ = builder;\n"
3035    } else {
3036        ""
3037    };
3038
3039    // Program SDK binding: stacks with generated programs bind a generated
3040    // accessor struct; program-less stacks bind `()`.
3041    let (programs_assoc, programs_struct) = match programs {
3042        Some(codegen) => {
3043            let programs_root = if config.module_mode { "super" } else { "crate" };
3044            let aggregate_name = format!("{}StackPrograms", stack_name);
3045            (
3046                format!("type Programs = {aggregate_name};"),
3047                format!(
3048                    "\n{}",
3049                    generate_programs_accessor_struct(
3050                        &aggregate_name,
3051                        codegen,
3052                        &format!("{programs_root}::programs"),
3053                    )
3054                ),
3055            )
3056        }
3057        None => ("type Programs = ();".to_string(), String::new()),
3058    };
3059
3060    format!(
3061        r#"{types_use}use arete_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
3062
3063pub struct {stack}Stack;
3064
3065impl Stack for {stack}Stack {{
3066    type Views = {stack}StackViews;
3067    {programs_assoc}
3068
3069    fn name() -> &'static str {{
3070        "{stack_kebab}"
3071    }}
3072
3073    {url_impl}{http_url_impl}
3074}}
3075
3076pub struct {stack}StackViews {{
3077{views_fields}
3078}}
3079
3080impl Views for {stack}StackViews {{
3081    fn from_builder(builder: ViewBuilder) -> Self {{
3082{empty_builder}        Self {{
3083{views_builder}
3084        }}
3085    }}
3086}}
3087{entity_views}{programs_struct}"#,
3088        types_use = types_use,
3089        stack = stack_name,
3090        stack_kebab = stack_kebab,
3091        programs_assoc = programs_assoc,
3092        url_impl = url_impl,
3093        http_url_impl = http_url_impl,
3094        views_fields = views_fields.join("\n"),
3095        views_builder = views_builder_fields.join("\n"),
3096        entity_views = entity_views_structs.join("\n"),
3097        empty_builder = empty_builder,
3098        programs_struct = programs_struct,
3099    )
3100}
3101
3102fn generate_programs_accessor_struct(
3103    aggregate_name: &str,
3104    codegen: &ProgramsCodegen,
3105    programs_root: &str,
3106) -> String {
3107    let fields = codegen
3108        .modules
3109        .iter()
3110        .map(|module| {
3111            format!(
3112                "    pub {}: {programs_root}::{}::{},",
3113                module.module_name, module.module_name, module.struct_name,
3114            )
3115        })
3116        .collect::<Vec<_>>();
3117    let inits = codegen
3118        .modules
3119        .iter()
3120        .enumerate()
3121        .map(|(index, module)| {
3122            let builder_expr = if index < codegen.modules.len() - 1 {
3123                "builder.clone()"
3124            } else {
3125                "builder"
3126            };
3127            format!(
3128                "            {}: {programs_root}::{}::{}::from_builder({builder_expr}),",
3129                module.module_name, module.module_name, module.struct_name,
3130            )
3131        })
3132        .collect::<Vec<_>>();
3133
3134    format!(
3135        r#"pub struct {aggregate_name} {{
3136{fields}
3137}}
3138
3139impl arete_sdk::Programs for {aggregate_name} {{
3140    fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {{
3141        Self {{
3142{inits}
3143        }}
3144    }}
3145}}"#,
3146        fields = fields.join("\n"),
3147        inits = inits.join("\n"),
3148    )
3149}
3150
3151// ============================================================================
3152// Program SDK generation (programs.rs)
3153// ============================================================================
3154
3155/// One generated program module and the accessor struct it exports.
3156#[derive(Debug, Clone)]
3157pub(crate) struct ProgramModule {
3158    module_name: String,
3159    struct_name: String,
3160}
3161
3162/// Result of generating the `programs` module for a stack.
3163#[derive(Debug, Clone)]
3164pub(crate) struct ProgramsCodegen {
3165    code: String,
3166    modules: Vec<ProgramModule>,
3167}
3168
3169/// Which `arete_sdk::instruction` items a generated program module references.
3170#[derive(Debug, Default)]
3171struct ProgramImports {
3172    account_meta: bool,
3173    arg_schema: bool,
3174    pda: bool,
3175    error_metadata: bool,
3176}
3177
3178/// A parsed instruction argument type.
3179#[derive(Debug, Clone)]
3180struct RustParsedArg {
3181    /// `ArgType::โ€ฆ` constructor expression for the handler schema.
3182    schema: String,
3183    /// Rust type for the typed params struct field.
3184    param_type: String,
3185    /// Whether the type is representable by the core serializer.
3186    supported: bool,
3187}
3188
3189fn rust_unsupported() -> RustParsedArg {
3190    RustParsedArg {
3191        schema: "ArgType::U8".to_string(),
3192        param_type: "()".to_string(),
3193        supported: false,
3194    }
3195}
3196
3197fn rust_prim(schema: &str, param_type: &str) -> RustParsedArg {
3198    RustParsedArg {
3199        schema: schema.to_string(),
3200        param_type: param_type.to_string(),
3201        supported: true,
3202    }
3203}
3204
3205/// Render a Rust string literal (quoted and escaped).
3206fn rust_string_literal(value: &str) -> String {
3207    format!("{:?}", value)
3208}
3209
3210/// Resolver for IDL-defined types (structs/enums) referenced by instruction
3211/// args. Resolved types are inlined into arg schemas as `ArgType::Struct` /
3212/// `ArgType::Enum` expressions; the typed params field for such args is
3213/// `serde_json::Value`. Mirrors the TypeScript `DefinedTypes` parsing rules.
3214struct RustDefinedTypes<'a> {
3215    /// IDL type definitions by name, first-wins across programs.
3216    defs: BTreeMap<String, &'a IdlTypeDefSnapshot>,
3217    /// lowercase name -> canonical key, for case-insensitive fallback lookup.
3218    lower: BTreeMap<String, String>,
3219    /// Memoized resolutions by original IDL name (`None` = unsupported).
3220    resolved: BTreeMap<String, Option<RustParsedArg>>,
3221    /// Names currently being resolved (cycle guard).
3222    visiting: HashSet<String>,
3223}
3224
3225impl<'a> RustDefinedTypes<'a> {
3226    fn new(idls: &'a [IdlSnapshot]) -> Self {
3227        let mut defs: BTreeMap<String, &'a IdlTypeDefSnapshot> = BTreeMap::new();
3228        let mut lower: BTreeMap<String, String> = BTreeMap::new();
3229        for idl in idls {
3230            for def in &idl.types {
3231                if !defs.contains_key(def.name.as_str()) {
3232                    defs.insert(def.name.clone(), def);
3233                    lower.insert(def.name.to_lowercase(), def.name.clone());
3234                }
3235            }
3236        }
3237        RustDefinedTypes {
3238            defs,
3239            lower,
3240            resolved: BTreeMap::new(),
3241            visiting: HashSet::new(),
3242        }
3243    }
3244
3245    /// Parse a stringified Rust-ish arg type (what `to_rust_type_string`
3246    /// produces), resolving bare names against the IDL type definitions.
3247    fn parse_arg_type(&mut self, raw: &str) -> RustParsedArg {
3248        let t = raw.trim().trim_start_matches('&').trim();
3249
3250        // Generic wrappers: Option<T>, Vec<T>.
3251        if let Some((name, inner)) = split_generic(t) {
3252            match name {
3253                "Option" => {
3254                    let inner = self.parse_arg_type(inner);
3255                    return RustParsedArg {
3256                        schema: format!("ArgType::Option(Box::new({}))", inner.schema),
3257                        param_type: format!("Option<{}>", inner.param_type),
3258                        supported: inner.supported,
3259                    };
3260                }
3261                "Vec" => {
3262                    let inner = self.parse_arg_type(inner);
3263                    return RustParsedArg {
3264                        schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
3265                        param_type: format!("Vec<{}>", inner.param_type),
3266                        supported: inner.supported,
3267                    };
3268                }
3269                _ => return rust_unsupported(),
3270            }
3271        }
3272
3273        // Fixed-size array: [T; N].
3274        if let Some(stripped) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
3275            if let Some((ty, n)) = stripped.rsplit_once(';') {
3276                let inner = self.parse_arg_type(ty.trim());
3277                let n = n.trim();
3278                if n.parse::<usize>().is_ok() {
3279                    return RustParsedArg {
3280                        schema: format!("ArgType::Array(Box::new({}), {})", inner.schema, n),
3281                        param_type: format!("Vec<{}>", inner.param_type),
3282                        supported: inner.supported,
3283                    };
3284                }
3285            }
3286        }
3287
3288        // Primitive (possibly path-qualified, e.g. solana_pubkey::Pubkey).
3289        let last = t.rsplit("::").next().unwrap_or(t);
3290        match last {
3291            "u8" => rust_prim("ArgType::U8", "u8"),
3292            "u16" => rust_prim("ArgType::U16", "u16"),
3293            "u32" => rust_prim("ArgType::U32", "u32"),
3294            "u64" => rust_prim("ArgType::U64", "u64"),
3295            // serde_json cannot carry 128-bit integers losslessly; the core
3296            // serializer accepts decimal strings for them.
3297            "u128" => rust_prim("ArgType::U128", "String"),
3298            "i8" => rust_prim("ArgType::I8", "i8"),
3299            "i16" => rust_prim("ArgType::I16", "i16"),
3300            "i32" => rust_prim("ArgType::I32", "i32"),
3301            "i64" => rust_prim("ArgType::I64", "i64"),
3302            "i128" => rust_prim("ArgType::I128", "String"),
3303            "f32" => rust_prim("ArgType::F32", "f32"),
3304            "f64" => rust_prim("ArgType::F64", "f64"),
3305            "bool" => rust_prim("ArgType::Bool", "bool"),
3306            "String" | "string" | "str" => rust_prim("ArgType::String", "String"),
3307            "Pubkey" | "pubkey" | "PublicKey" | "publicKey" => {
3308                rust_prim("ArgType::Pubkey", "String")
3309            }
3310            "bytes" => rust_prim("ArgType::Bytes", "Vec<u8>"),
3311            _ => self.resolve_defined(last).unwrap_or_else(rust_unsupported),
3312        }
3313    }
3314
3315    /// Parse an IDL snapshot type (used inside struct fields / enum variants).
3316    fn parse_snapshot_type(&mut self, t: &IdlTypeSnapshot) -> RustParsedArg {
3317        match t {
3318            IdlTypeSnapshot::Simple(s) => self.parse_arg_type(s),
3319            IdlTypeSnapshot::Option(o) => {
3320                let inner = self.parse_snapshot_type(&o.option);
3321                RustParsedArg {
3322                    schema: format!("ArgType::Option(Box::new({}))", inner.schema),
3323                    param_type: format!("Option<{}>", inner.param_type),
3324                    supported: inner.supported,
3325                }
3326            }
3327            IdlTypeSnapshot::Vec(v) => {
3328                let inner = self.parse_snapshot_type(&v.vec);
3329                RustParsedArg {
3330                    schema: format!("ArgType::Vec(Box::new({}))", inner.schema),
3331                    param_type: format!("Vec<{}>", inner.param_type),
3332                    supported: inner.supported,
3333                }
3334            }
3335            IdlTypeSnapshot::Array(arr) => {
3336                let mut element: Option<RustParsedArg> = None;
3337                let mut size: Option<u32> = None;
3338                for part in &arr.array {
3339                    match part {
3340                        IdlArrayElementSnapshot::Type(inner) => {
3341                            element = Some(self.parse_snapshot_type(inner))
3342                        }
3343                        IdlArrayElementSnapshot::TypeName(name) => {
3344                            element = Some(self.parse_arg_type(name))
3345                        }
3346                        IdlArrayElementSnapshot::Size(n) => size = Some(*n),
3347                    }
3348                }
3349                match (element, size) {
3350                    (Some(inner), Some(n)) => RustParsedArg {
3351                        schema: format!("ArgType::Array(Box::new({}), {})", inner.schema, n),
3352                        param_type: format!("Vec<{}>", inner.param_type),
3353                        supported: inner.supported,
3354                    },
3355                    _ => rust_unsupported(),
3356                }
3357            }
3358            IdlTypeSnapshot::HashMap(map) => {
3359                let key = self.parse_snapshot_type(&map.hash_map.0);
3360                let value = self.parse_snapshot_type(&map.hash_map.1);
3361                if !key.supported || key.schema != "ArgType::String" || !value.supported {
3362                    rust_unsupported()
3363                } else {
3364                    RustParsedArg {
3365                        schema: format!(
3366                            "ArgType::HashMap(Box::new({}), Box::new({}))",
3367                            key.schema, value.schema
3368                        ),
3369                        param_type: "serde_json::Value".to_string(),
3370                        supported: true,
3371                    }
3372                }
3373            }
3374            IdlTypeSnapshot::Defined(d) => {
3375                let name = match &d.defined {
3376                    IdlDefinedInnerSnapshot::Named { name } => name.as_str(),
3377                    IdlDefinedInnerSnapshot::Simple(s) => s.as_str(),
3378                };
3379                self.resolve_defined(name).unwrap_or_else(rust_unsupported)
3380            }
3381        }
3382    }
3383
3384    /// Resolve a bare type name against the IDL type definitions. Returns
3385    /// `None` when unsupported (unknown, recursive, tuple struct, โ€ฆ).
3386    fn resolve_defined(&mut self, name: &str) -> Option<RustParsedArg> {
3387        if let Some(cached) = self.resolved.get(name) {
3388            return cached.clone();
3389        }
3390        if self.visiting.contains(name) {
3391            // Recursive types are not supported by instruction codegen.
3392            return None;
3393        }
3394
3395        let key = if self.defs.contains_key(name) {
3396            name.to_string()
3397        } else {
3398            match self.lower.get(&name.to_lowercase()) {
3399                Some(canonical) => canonical.clone(),
3400                None => {
3401                    self.resolved.insert(name.to_string(), None);
3402                    return None;
3403                }
3404            }
3405        };
3406
3407        self.visiting.insert(key.clone());
3408        let def = self.defs[&key];
3409        let result = match &def.type_def {
3410            IdlTypeDefKindSnapshot::Struct { fields, .. } => {
3411                let fields = fields.clone();
3412                self.resolve_struct(&fields)
3413            }
3414            IdlTypeDefKindSnapshot::TupleStruct { .. } => None,
3415            IdlTypeDefKindSnapshot::Enum { variants, .. } => {
3416                let variants = variants.clone();
3417                self.resolve_enum(&variants)
3418            }
3419        };
3420        self.visiting.remove(&key);
3421        self.resolved.insert(name.to_string(), result.clone());
3422        if name != key {
3423            self.resolved.insert(key, result.clone());
3424        }
3425        result
3426    }
3427
3428    fn resolve_struct(&mut self, fields: &[IdlFieldSnapshot]) -> Option<RustParsedArg> {
3429        let mut field_exprs: Vec<String> = Vec::new();
3430        for field in fields {
3431            let parsed = self.parse_snapshot_type(&field.type_);
3432            if !parsed.supported {
3433                return None;
3434            }
3435            field_exprs.push(format!(
3436                "ArgField {{ name: {}.to_string(), ty: {} }}",
3437                rust_string_literal(&field.name),
3438                parsed.schema
3439            ));
3440        }
3441        Some(RustParsedArg {
3442            schema: format!("ArgType::Struct(vec![{}])", field_exprs.join(", ")),
3443            param_type: "serde_json::Value".to_string(),
3444            supported: true,
3445        })
3446    }
3447
3448    fn resolve_enum(&mut self, variants: &[IdlEnumVariantSnapshot]) -> Option<RustParsedArg> {
3449        let mut variant_exprs: Vec<String> = Vec::new();
3450        for variant in variants {
3451            let name_literal = rust_string_literal(&variant.name);
3452            if variant.fields.is_empty() {
3453                variant_exprs.push(format!(
3454                    "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Unit }}",
3455                    name_literal
3456                ));
3457                continue;
3458            }
3459
3460            let named: Vec<_> = variant
3461                .fields
3462                .iter()
3463                .filter_map(|field| match field {
3464                    IdlEnumVariantFieldSnapshot::Named(field) => Some(field),
3465                    IdlEnumVariantFieldSnapshot::Tuple(_) => None,
3466                })
3467                .collect();
3468
3469            if named.len() == variant.fields.len() {
3470                let mut field_exprs: Vec<String> = Vec::new();
3471                for field in named {
3472                    let parsed = self.parse_snapshot_type(&field.type_);
3473                    if !parsed.supported {
3474                        return None;
3475                    }
3476                    field_exprs.push(format!(
3477                        "ArgField {{ name: {}.to_string(), ty: {} }}",
3478                        rust_string_literal(&field.name),
3479                        parsed.schema
3480                    ));
3481                }
3482                variant_exprs.push(format!(
3483                    "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Struct(vec![{}]) }}",
3484                    name_literal,
3485                    field_exprs.join(", ")
3486                ));
3487            } else if named.is_empty() {
3488                let mut element_exprs: Vec<String> = Vec::new();
3489                for field in &variant.fields {
3490                    let IdlEnumVariantFieldSnapshot::Tuple(ty) = field else {
3491                        unreachable!("named.is_empty() guarantees tuple fields");
3492                    };
3493                    let parsed = self.parse_snapshot_type(ty);
3494                    if !parsed.supported {
3495                        return None;
3496                    }
3497                    element_exprs.push(parsed.schema);
3498                }
3499                variant_exprs.push(format!(
3500                    "EnumVariantDef {{ name: {}.to_string(), kind: EnumVariantKind::Tuple(vec![{}]) }}",
3501                    name_literal,
3502                    element_exprs.join(", ")
3503                ));
3504            } else {
3505                // Mixed named and tuple fields are not supported.
3506                return None;
3507            }
3508        }
3509        Some(RustParsedArg {
3510            schema: format!("ArgType::Enum(vec![{}])", variant_exprs.join(", ")),
3511            param_type: "serde_json::Value".to_string(),
3512            supported: true,
3513        })
3514    }
3515}
3516
3517/// Whether any emitted schema expression references `ArgField` /
3518/// `EnumVariantDef` (defined struct/enum types were inlined).
3519fn schema_uses_defined_types(schema: &str) -> bool {
3520    schema.contains("ArgField") || schema.contains("EnumVariantDef")
3521}
3522
3523/// How a mapped account surfaces in the typed params struct.
3524#[derive(Debug, Clone, Copy, PartialEq)]
3525enum RustAccountFieldKind {
3526    /// Signer slot: optional address override (payer fallback applies).
3527    Signer,
3528    /// Required user-provided account address.
3529    Required,
3530    /// Optional user-provided account address.
3531    Optional,
3532}
3533
3534/// Result of mapping a single instruction account.
3535struct MappedRustAccount {
3536    /// `AccountMeta { โ€ฆ },` literal, indented for the handler's accounts vec.
3537    literal: String,
3538    /// Params field for caller-supplied addresses.
3539    field: Option<(String, RustAccountFieldKind)>,
3540    /// Human-readable notes surfaced in the typed builder's doc comment.
3541    notes: Vec<String>,
3542    /// Whether the emitted resolution references `PdaConfig` / `PdaSeed`.
3543    uses_pda: bool,
3544}
3545
3546fn rust_account_meta_literal(
3547    acc: &InstructionAccountDef,
3548    resolution: &str,
3549    comment: Option<&str>,
3550) -> String {
3551    let mut out = String::new();
3552    if let Some(comment) = comment {
3553        out.push_str(&format!("                // [arete codegen] {}\n", comment));
3554    }
3555    out.push_str(&format!(
3556        "                AccountMeta {{\n                    name: {name}.to_string(),\n                    is_signer: {is_signer},\n                    is_writable: {is_writable},\n                    resolution: {resolution},\n                    is_optional: {is_optional},\n                }},",
3557        name = rust_string_literal(&acc.name),
3558        is_signer = acc.is_signer,
3559        is_writable = acc.is_writable,
3560        resolution = resolution,
3561        is_optional = acc.is_optional,
3562    ));
3563    out
3564}
3565
3566fn map_rust_account(
3567    acc: &InstructionAccountDef,
3568    pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3569    account_names: &HashSet<&str>,
3570    arg_types: &BTreeMap<&str, &str>,
3571) -> MappedRustAccount {
3572    let user_field_kind = if acc.is_optional {
3573        RustAccountFieldKind::Optional
3574    } else {
3575        RustAccountFieldKind::Required
3576    };
3577    let degraded = |reason: String| -> MappedRustAccount {
3578        let note = format!(
3579            "account `{}` degraded to user-provided ({})",
3580            acc.name, reason
3581        );
3582        MappedRustAccount {
3583            literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", Some(&note)),
3584            field: Some((acc.name.clone(), user_field_kind)),
3585            notes: vec![note],
3586            uses_pda: false,
3587        }
3588    };
3589
3590    match &acc.resolution {
3591        AccountResolution::Signer => MappedRustAccount {
3592            literal: rust_account_meta_literal(acc, "AccountResolution::Signer", None),
3593            field: Some((acc.name.clone(), RustAccountFieldKind::Signer)),
3594            notes: Vec::new(),
3595            uses_pda: false,
3596        },
3597        AccountResolution::Known { address } => MappedRustAccount {
3598            literal: rust_account_meta_literal(
3599                acc,
3600                &format!(
3601                    "AccountResolution::Known({}.to_string())",
3602                    rust_string_literal(address)
3603                ),
3604                None,
3605            ),
3606            field: None,
3607            notes: Vec::new(),
3608            uses_pda: false,
3609        },
3610        AccountResolution::UserProvided => MappedRustAccount {
3611            literal: rust_account_meta_literal(acc, "AccountResolution::UserProvided", None),
3612            field: Some((acc.name.clone(), user_field_kind)),
3613            notes: Vec::new(),
3614            uses_pda: false,
3615        },
3616        AccountResolution::PdaInline {
3617            seeds,
3618            program_id,
3619            program,
3620        } => {
3621            if program.is_some() {
3622                return degraded(
3623                    "uses a dynamic PDA program selector not supported by the Rust low-level resolver"
3624                        .to_string(),
3625                );
3626            }
3627            match build_rust_pda_config(seeds, program_id.as_deref(), account_names, arg_types) {
3628                Ok((resolution, notes)) => MappedRustAccount {
3629                    literal: rust_account_meta_literal(acc, &resolution, None),
3630                    field: None,
3631                    notes,
3632                    uses_pda: true,
3633                },
3634                Err(reason) => degraded(reason),
3635            }
3636        }
3637        AccountResolution::PdaRef { pda_name } => match pda_lookup.get(pda_name.as_str()) {
3638            Some(def) => {
3639                if def.program.is_some() {
3640                    return degraded(format!(
3641                        "PDA '{}' uses a dynamic program selector not supported by the Rust low-level resolver",
3642                        pda_name
3643                    ));
3644                }
3645                match build_rust_pda_config(
3646                    &def.seeds,
3647                    def.program_id.as_deref(),
3648                    account_names,
3649                    arg_types,
3650                ) {
3651                    Ok((resolution, notes)) => MappedRustAccount {
3652                        literal: rust_account_meta_literal(acc, &resolution, None),
3653                        field: None,
3654                        notes,
3655                        uses_pda: true,
3656                    },
3657                    Err(reason) => degraded(format!("PDA '{}': {}", pda_name, reason)),
3658                }
3659            }
3660            None => degraded(format!("references unknown PDA '{}'", pda_name)),
3661        },
3662    }
3663}
3664
3665/// Build an `AccountResolution::Pda(PdaConfig { โ€ฆ })` expression from seed
3666/// definitions. Returns `Err(reason)` when the PDA cannot be represented by
3667/// the core resolver, so the caller can degrade to user-provided.
3668fn build_rust_pda_config(
3669    seeds: &[PdaSeedDef],
3670    program_id: Option<&str>,
3671    account_names: &HashSet<&str>,
3672    arg_types: &BTreeMap<&str, &str>,
3673) -> Result<(String, Vec<String>), String> {
3674    let mut seed_exprs: Vec<String> = Vec::new();
3675    let mut notes: Vec<String> = Vec::new();
3676    for seed in seeds {
3677        match seed {
3678            PdaSeedDef::Literal { value } => {
3679                seed_exprs.push(format!(
3680                    "PdaSeed::Literal({}.to_string())",
3681                    rust_string_literal(value)
3682                ));
3683            }
3684            PdaSeedDef::Bytes { value } => {
3685                let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
3686                seed_exprs.push(format!("PdaSeed::Bytes(vec![{}])", bytes.join(", ")));
3687            }
3688            PdaSeedDef::AccountRef { account_name } => {
3689                if account_name.contains('.') {
3690                    return Err(format!(
3691                        "seed references account field '{}' which is not supported for auto-resolution",
3692                        account_name
3693                    ));
3694                }
3695                if !account_names.contains(account_name.as_str()) {
3696                    return Err(format!(
3697                        "seed references account '{}' not present in this instruction",
3698                        account_name
3699                    ));
3700                }
3701                seed_exprs.push(format!(
3702                    "PdaSeed::AccountRef({}.to_string())",
3703                    rust_string_literal(account_name)
3704                ));
3705            }
3706            PdaSeedDef::ArgRef { arg_name, arg_type } => {
3707                let arg_root = arg_name.split('.').next().unwrap_or(arg_name.as_str());
3708                let present =
3709                    arg_types.contains_key(arg_name.as_str()) || arg_types.contains_key(arg_root);
3710                // Prefer the seed's declared type; fall back to the
3711                // instruction arg's type (Anchor seeds carry no type info).
3712                let raw_type = arg_type
3713                    .as_deref()
3714                    .or_else(|| arg_types.get(arg_name.as_str()).copied())
3715                    .or_else(|| arg_types.get(arg_root).copied());
3716                let canonical = raw_type.and_then(normalize_seed_arg_type);
3717                if !present {
3718                    if canonical.is_none() {
3719                        return Err(format!(
3720                            "seed helper arg '{}' is not present in this instruction and has no primitive type information",
3721                            arg_name
3722                        ));
3723                    }
3724                    notes.push(format!(
3725                        "seed input `{}` must be supplied via the `resolve` key when building through the raw handler",
3726                        arg_name
3727                    ));
3728                }
3729                match canonical {
3730                    Some(canonical) => seed_exprs.push(format!(
3731                        "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: Some({}.to_string()) }}",
3732                        rust_string_literal(arg_name),
3733                        rust_string_literal(&canonical)
3734                    )),
3735                    None => {
3736                        notes.push(format!(
3737                            "seed arg `{}` has non-primitive type '{}'; the runtime will use heuristic encoding",
3738                            arg_name,
3739                            raw_type.unwrap_or("<unknown>")
3740                        ));
3741                        seed_exprs.push(format!(
3742                            "PdaSeed::ArgRef {{ arg: {}.to_string(), arg_type: None }}",
3743                            rust_string_literal(arg_name)
3744                        ));
3745                    }
3746                }
3747            }
3748        }
3749    }
3750
3751    let program_expr = match program_id {
3752        Some(pid) => format!("Some({}.to_string())", rust_string_literal(pid)),
3753        None => "None".to_string(),
3754    };
3755    Ok((
3756        format!(
3757            "AccountResolution::Pda(PdaConfig {{ program_id: {}, seeds: vec![{}] }})",
3758            program_expr,
3759            seed_exprs.join(", ")
3760        ),
3761        notes,
3762    ))
3763}
3764
3765/// Generated code for one instruction: module items plus the accessor method.
3766struct RustInstructionBlock {
3767    code: String,
3768    method: String,
3769    uses_defined_types: bool,
3770}
3771
3772fn generate_rust_instruction_block(
3773    instr: &InstructionDef,
3774    errors: &[IdlErrorSnapshot],
3775    pda_lookup: &BTreeMap<&str, &PdaDefinition>,
3776    parser: &mut RustDefinedTypes<'_>,
3777    needs: &mut ProgramImports,
3778) -> Result<RustInstructionBlock, String> {
3779    // --- Parse args; skip the whole instruction on unsupported types. ---
3780    let mut parsed_args: Vec<(&InstructionArgDef, RustParsedArg)> = Vec::new();
3781    for arg in &instr.args {
3782        let parsed = parser.parse_arg_type(&arg.arg_type);
3783        if !parsed.supported {
3784            return Err(format!(
3785                "arg '{}' has unsupported type '{}'",
3786                arg.name, arg.arg_type
3787            ));
3788        }
3789        parsed_args.push((arg, parsed));
3790    }
3791
3792    // --- Map accounts. ---
3793    let account_names: HashSet<&str> = instr.accounts.iter().map(|a| a.name.as_str()).collect();
3794    let arg_types: BTreeMap<&str, &str> = instr
3795        .args
3796        .iter()
3797        .map(|a| (a.name.as_str(), a.arg_type.as_str()))
3798        .collect();
3799
3800    let mut account_literals: Vec<String> = Vec::new();
3801    let mut account_fields: Vec<(String, RustAccountFieldKind)> = Vec::new();
3802    let mut notes: Vec<String> = Vec::new();
3803    for acc in &instr.accounts {
3804        let mapped = map_rust_account(acc, pda_lookup, &account_names, &arg_types);
3805        account_literals.push(mapped.literal);
3806        if let Some(field) = mapped.field {
3807            account_fields.push(field);
3808        }
3809        notes.extend(mapped.notes);
3810        if mapped.uses_pda {
3811            needs.pda = true;
3812        }
3813    }
3814    if !instr.accounts.is_empty() {
3815        needs.account_meta = true;
3816    }
3817    if !instr.args.is_empty() {
3818        needs.arg_schema = true;
3819    }
3820    if !errors.is_empty() {
3821        needs.error_metadata = true;
3822    }
3823
3824    let fn_name = to_snake_case(&instr.name);
3825    let pascal = to_pascal_case(&instr.name);
3826    let params_name = format!("{}Params", pascal);
3827
3828    // --- Typed params struct: args first, then caller-supplied accounts.
3829    // Instruction args win name collisions (mirrors the TS SDK's params
3830    // precedence in `splitParams`). ---
3831    let arg_name_set: HashSet<&str> = instr.args.iter().map(|a| a.name.as_str()).collect();
3832    let mut used_field_names: HashSet<String> = HashSet::new();
3833    let mut param_fields: Vec<String> = Vec::new();
3834    let mut uses_defined_types = false;
3835    for (arg, parsed) in &parsed_args {
3836        let field_name = to_snake_case(&arg.name);
3837        used_field_names.insert(field_name.clone());
3838        uses_defined_types |= schema_uses_defined_types(&parsed.schema);
3839        let mut lines = Vec::new();
3840        if field_name != arg.name {
3841            lines.push(format!(
3842                "        #[serde(rename = {})]",
3843                rust_string_literal(&arg.name)
3844            ));
3845        }
3846        lines.push(format!(
3847            "        pub {}: {},",
3848            field_name, parsed.param_type
3849        ));
3850        param_fields.push(lines.join("\n"));
3851    }
3852    for (name, kind) in &account_fields {
3853        if arg_name_set.contains(name.as_str()) {
3854            notes.push(format!(
3855                "account `{}` shares its name with an instruction arg and has no typed override field",
3856                name
3857            ));
3858            continue;
3859        }
3860        let field_name = to_snake_case(name);
3861        if !used_field_names.insert(field_name.clone()) {
3862            notes.push(format!(
3863                "account `{}` collides with another params field and has no typed override field",
3864                name
3865            ));
3866            continue;
3867        }
3868        let mut lines = Vec::new();
3869        match kind {
3870            RustAccountFieldKind::Signer => lines.push(format!(
3871                "        /// Optional address override for the `{}` signer (defaults to the payer).",
3872                name
3873            )),
3874            RustAccountFieldKind::Required => {
3875                lines.push(format!("        /// Address of the `{}` account.", name))
3876            }
3877            RustAccountFieldKind::Optional => lines.push(format!(
3878                "        /// Optional address of the `{}` account.",
3879                name
3880            )),
3881        }
3882        if field_name != *name {
3883            lines.push(format!(
3884                "        #[serde(rename = {})]",
3885                rust_string_literal(name)
3886            ));
3887        }
3888        match kind {
3889            RustAccountFieldKind::Required => {
3890                lines.push(format!("        pub {}: String,", field_name))
3891            }
3892            _ => {
3893                lines.push(
3894                    "        #[serde(skip_serializing_if = \"Option::is_none\")]".to_string(),
3895                );
3896                lines.push(format!("        pub {}: Option<String>,", field_name));
3897            }
3898        }
3899        param_fields.push(lines.join("\n"));
3900    }
3901
3902    let params_struct = if param_fields.is_empty() {
3903        format!(
3904            "    /// Typed params for `{name}` (no args or caller-supplied accounts).\n    #[derive(Debug, Clone, Serialize, Default)]\n    pub struct {params_name} {{}}",
3905            name = instr.name,
3906            params_name = params_name
3907        )
3908    } else {
3909        format!(
3910            "    /// Typed params for `{name}`: instruction args plus overridable accounts.\n    #[derive(Debug, Clone, Serialize, Default)]\n    pub struct {params_name} {{\n{fields}\n    }}",
3911            name = instr.name,
3912            params_name = params_name,
3913            fields = param_fields.join("\n")
3914        )
3915    };
3916
3917    // --- Typed builder fn. ---
3918    let mut doc_lines: Vec<String> = instr
3919        .docs
3920        .iter()
3921        .map(|line| line.trim().to_string())
3922        .collect();
3923    if doc_lines.is_empty() {
3924        doc_lines.push(format!("Builds the `{}` instruction.", instr.name));
3925    }
3926    if !notes.is_empty() {
3927        doc_lines.push(String::new());
3928        doc_lines.push("Codegen notes:".to_string());
3929        for note in &notes {
3930            doc_lines.push(format!("- {}", note));
3931        }
3932    }
3933    let docs = doc_lines
3934        .iter()
3935        .map(|line| {
3936            if line.is_empty() {
3937                "    ///".to_string()
3938            } else {
3939                format!("    /// {}", line)
3940            }
3941        })
3942        .collect::<Vec<_>>()
3943        .join("\n");
3944
3945    let typed_fn = format!(
3946        "{docs}\n    pub fn {fn_name}(params: {params_name}) -> Result<BuiltInstruction, InstructionError> {{\n        let params = serde_json::to_value(params).map_err(|error| InstructionError::InvalidValue {{\n            context: \"params\".to_string(),\n            message: error.to_string(),\n        }})?;\n        {fn_name}_handler().build(params)\n    }}",
3947        docs = docs,
3948        fn_name = fn_name,
3949        params_name = params_name
3950    );
3951
3952    // --- Handler literal. ---
3953    let discriminator = instr
3954        .discriminator
3955        .iter()
3956        .map(|b| b.to_string())
3957        .collect::<Vec<_>>()
3958        .join(", ");
3959    let accounts_literal = if account_literals.is_empty() {
3960        "vec![]".to_string()
3961    } else {
3962        format!("vec![\n{}\n            ]", account_literals.join("\n"))
3963    };
3964    let args_literal = if parsed_args.is_empty() {
3965        "vec![]".to_string()
3966    } else {
3967        let entries: Vec<String> = parsed_args
3968            .iter()
3969            .map(|(arg, parsed)| {
3970                format!(
3971                    "                ArgSchema {{ name: {}.to_string(), ty: {} }},",
3972                    rust_string_literal(&arg.name),
3973                    parsed.schema
3974                )
3975            })
3976            .collect();
3977        format!("vec![\n{}\n            ]", entries.join("\n"))
3978    };
3979    let errors_literal = if errors.is_empty() {
3980        "vec![]".to_string()
3981    } else {
3982        let entries: Vec<String> = errors
3983            .iter()
3984            .map(|error| {
3985                format!(
3986                    "                ErrorMetadata {{ code: {}, name: {}.to_string(), msg: {}.to_string() }},",
3987                    error.code,
3988                    rust_string_literal(&error.name),
3989                    rust_string_literal(error.msg.as_deref().unwrap_or(""))
3990                )
3991            })
3992            .collect();
3993        format!("vec![\n{}\n            ]", entries.join("\n"))
3994    };
3995
3996    let handler_fn = format!(
3997        "    /// Raw instruction handler for `{name}`.\n    pub fn {fn_name}_handler() -> InstructionHandler {{\n        InstructionHandler {{\n            program_id: PROGRAM_ID.to_string(),\n            discriminator: vec![{discriminator}],\n            accounts: {accounts},\n            args: {args},\n            errors: {errors},\n        }}\n    }}",
3998        name = instr.name,
3999        fn_name = fn_name,
4000        discriminator = discriminator,
4001        accounts = accounts_literal,
4002        args = args_literal,
4003        errors = errors_literal
4004    );
4005
4006    let method = format!(
4007        "        pub fn {fn_name}(&self, params: {params_name}) -> Result<BuiltInstruction, InstructionError> {{\n            {fn_name}(params)\n        }}",
4008        fn_name = fn_name,
4009        params_name = params_name
4010    );
4011
4012    Ok(RustInstructionBlock {
4013        code: format!("{}\n\n{}\n\n{}", params_struct, typed_fn, handler_fn),
4014        method,
4015        uses_defined_types,
4016    })
4017}
4018
4019/// Generate the `pdas` helper module for one program. Returns `None` when the
4020/// program declares no PDAs.
4021fn generate_rust_pdas_module(pdas: &BTreeMap<String, PdaDefinition>) -> Option<String> {
4022    if pdas.is_empty() {
4023        return None;
4024    }
4025
4026    let mut fns: Vec<String> = Vec::new();
4027    let mut needs_serialize = false;
4028    let mut needs_program_id = false;
4029    for def in pdas.values() {
4030        let fn_name = to_snake_case(&def.name);
4031        let mut params: Vec<(String, String)> = Vec::new();
4032        let mut seed_exprs: Vec<String> = Vec::new();
4033        for seed in &def.seeds {
4034            match seed {
4035                PdaSeedDef::Literal { value } => {
4036                    seed_exprs.push(format!(
4037                        "{}.as_bytes().to_vec()",
4038                        rust_string_literal(value)
4039                    ));
4040                }
4041                PdaSeedDef::Bytes { value } => {
4042                    let bytes: Vec<String> = value.iter().map(|b| b.to_string()).collect();
4043                    seed_exprs.push(format!("vec![{}]", bytes.join(", ")));
4044                }
4045                PdaSeedDef::AccountRef { account_name } => {
4046                    let param = to_snake_case(account_name);
4047                    if !params.iter().any(|(name, _)| *name == param) {
4048                        params.push((param.clone(), "&str".to_string()));
4049                    }
4050                    needs_serialize = true;
4051                    seed_exprs.push(format!(
4052                        "serialize_seed_value(&serde_json::json!({}), Some(\"pubkey\"))?",
4053                        param
4054                    ));
4055                }
4056                PdaSeedDef::ArgRef { arg_name, arg_type } => {
4057                    let param = to_snake_case(arg_name);
4058                    let canonical = arg_type.as_deref().and_then(normalize_seed_arg_type);
4059                    let (param_type, hint) = match canonical.as_deref() {
4060                        Some("pubkey") => ("&str", "Some(\"pubkey\")".to_string()),
4061                        Some("string") => ("&str", "Some(\"string\")".to_string()),
4062                        Some(int) if int.starts_with('i') => {
4063                            ("i64", format!("Some({})", rust_string_literal(int)))
4064                        }
4065                        Some(int) => ("u64", format!("Some({})", rust_string_literal(int))),
4066                        None => ("&str", "None".to_string()),
4067                    };
4068                    if !params.iter().any(|(name, _)| *name == param) {
4069                        params.push((param.clone(), param_type.to_string()));
4070                    }
4071                    needs_serialize = true;
4072                    seed_exprs.push(format!(
4073                        "serialize_seed_value(&serde_json::json!({}), {})?",
4074                        param, hint
4075                    ));
4076                }
4077            }
4078        }
4079
4080        let program_expr = match (&def.program_id, &def.program) {
4081            (Some(pid), _) => rust_string_literal(pid),
4082            (None, Some(PdaProgramDef::AccountRef { account_name })) => {
4083                let param = to_snake_case(account_name);
4084                if !params.iter().any(|(name, _)| *name == param) {
4085                    params.push((param.clone(), "&str".to_string()));
4086                }
4087                param
4088            }
4089            (None, Some(PdaProgramDef::ArgRef { arg_name })) => {
4090                let param = to_snake_case(arg_name);
4091                if !params.iter().any(|(name, _)| *name == param) {
4092                    params.push((param.clone(), "&str".to_string()));
4093                }
4094                param
4095            }
4096            (None, None) => {
4097                needs_program_id = true;
4098                "PROGRAM_ID".to_string()
4099            }
4100        };
4101        let param_list = params
4102            .iter()
4103            .map(|(name, ty)| format!("{}: {}", name, ty))
4104            .collect::<Vec<_>>()
4105            .join(", ");
4106        let seeds_body = if seed_exprs.is_empty() {
4107            "            let seeds: Vec<Vec<u8>> = vec![];".to_string()
4108        } else {
4109            format!(
4110                "            let seeds: Vec<Vec<u8>> = vec![\n{}\n            ];",
4111                seed_exprs
4112                    .iter()
4113                    .map(|expr| format!("                {},", expr))
4114                    .collect::<Vec<_>>()
4115                    .join("\n")
4116            )
4117        };
4118        fns.push(format!(
4119            "        /// Derive the `{name}` PDA (returns the address and bump).\n        pub fn {fn_name}({params}) -> Result<(Pubkey, u8), InstructionError> {{\n{seeds}\n            derive_program_address(&seeds, {program})\n        }}",
4120            name = def.name,
4121            fn_name = fn_name,
4122            params = param_list,
4123            seeds = seeds_body,
4124            program = program_expr
4125        ));
4126    }
4127
4128    let mut imports = vec!["derive_program_address"];
4129    if needs_serialize {
4130        imports.push("serialize_seed_value");
4131    }
4132    imports.extend(["InstructionError", "Pubkey"]);
4133    imports.sort_unstable();
4134    let mut use_lines = format!(
4135        "        use arete_sdk::instruction::{{{}}};",
4136        imports.join(", ")
4137    );
4138    if needs_program_id {
4139        use_lines.push_str("\n\n        use super::PROGRAM_ID;");
4140    }
4141
4142    Some(format!(
4143        "    /// PDA derivation helpers for this program.\n    pub mod pdas {{\n{use_lines}\n\n{fns}\n    }}",
4144        use_lines = use_lines,
4145        fns = fns.join("\n\n")
4146    ))
4147}
4148
4149/// Release identity computed at generation time for one program, or the
4150/// reason the program's read layer is omitted.
4151type ProgramReadLayer = Result<(String, String, Option<serde_json::Value>), String>;
4152
4153/// Resolve the release identity (`PROGRAM_SPEC_HASH`, `PROGRAM_RELEASE_HASH`)
4154/// for one program from the stack's recorded program specs.
4155fn resolve_program_read_layer(
4156    program_specs: &[arete_hash::ProgramSpecV1],
4157    program_id: &str,
4158) -> ProgramReadLayer {
4159    let Some(spec) = program_specs
4160        .iter()
4161        .find(|spec| spec.program_id == program_id)
4162    else {
4163        return Err("no program specification was recorded for this program".to_string());
4164    };
4165    let spec_hash = spec
4166        .hash()
4167        .map_err(|error| format!("failed to compute the program spec hash ({error})"))?;
4168    let release_hash = spec
4169        .oss_release_hash()
4170        .map_err(|error| format!("failed to compute the release hash ({error})"))?;
4171    Ok((spec_hash.to_string(), release_hash.to_string(), None))
4172}
4173
4174/// Generate `programs.rs`: one module per program with typed instruction
4175/// builders, raw handlers, PDA helpers, and (when the stack records a program
4176/// spec for the program) release identity consts plus typed account readers.
4177/// Returns `None` when the stack declares no instructions.
4178#[allow(clippy::too_many_arguments)]
4179fn generate_stack_programs_rs(
4180    stack_name: &str,
4181    instructions: &[InstructionDef],
4182    idls: &[IdlSnapshot],
4183    pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
4184    program_ids: &[String],
4185    program_specs: &[arete_hash::ProgramSpecV1],
4186    account_structs: &BTreeMap<String, String>,
4187    module_mode: bool,
4188    reads: &[RustProgramReadConfig],
4189    include_idl_only_programs: bool,
4190) -> Option<ProgramsCodegen> {
4191    if instructions.is_empty() && !include_idl_only_programs {
4192        return None;
4193    }
4194
4195    // Path to the generated types module from inside a `pub mod <program>`
4196    // block within programs.rs.
4197    let types_path = if module_mode {
4198        "super::super::types"
4199    } else {
4200        "crate::types"
4201    };
4202
4203    let default_program_id = program_ids.first().cloned().unwrap_or_default();
4204
4205    // Group instructions by resolved program id, preserving first-seen order.
4206    let mut groups: Vec<(String, Vec<&InstructionDef>)> = Vec::new();
4207    if include_idl_only_programs {
4208        for (index, idl) in idls.iter().enumerate() {
4209            let program_id = idl
4210                .program_id
4211                .clone()
4212                .or_else(|| program_ids.get(index).cloned())
4213                .unwrap_or_default();
4214            if !groups.iter().any(|(existing, _)| *existing == program_id) {
4215                groups.push((program_id, Vec::new()));
4216            }
4217        }
4218    }
4219    for instr in instructions {
4220        let pid = instr
4221            .program_id
4222            .clone()
4223            .unwrap_or_else(|| default_program_id.clone());
4224        match groups.iter_mut().find(|(existing, _)| *existing == pid) {
4225            Some((_, list)) => list.push(instr),
4226            None => groups.push((pid, vec![instr])),
4227        }
4228    }
4229
4230    let mut parser = RustDefinedTypes::new(idls);
4231    let mut used_module_names: HashSet<String> = HashSet::new();
4232    let mut module_blocks: Vec<String> = Vec::new();
4233    let mut modules: Vec<ProgramModule> = Vec::new();
4234
4235    for (index, (program_id, group)) in groups.iter().enumerate() {
4236        let idl = idls
4237            .iter()
4238            .find(|idl| idl.program_id.as_deref() == Some(program_id.as_str()));
4239        let raw_name = match idl {
4240            Some(idl) => idl.name.clone(),
4241            None if index == 0 => stack_name.to_string(),
4242            None => format!("program{}", index),
4243        };
4244        let mut module_name = rust_module_name(&raw_name);
4245        if module_name.is_empty() {
4246            module_name = format!("program{}", index);
4247        }
4248        while !used_module_names.insert(module_name.clone()) {
4249            module_name.push('_');
4250        }
4251        let struct_name = format!("{}Program", to_pascal_case(&module_name));
4252
4253        // PDA registry lookup: this program's group first, then any group.
4254        let own_pdas = idl.and_then(|idl| pdas.get(idl.name.as_str()));
4255        let mut pda_lookup: BTreeMap<&str, &PdaDefinition> = BTreeMap::new();
4256        if let Some(own) = own_pdas {
4257            for (name, def) in own {
4258                pda_lookup.insert(name.as_str(), def);
4259            }
4260        }
4261        for group_pdas in pdas.values() {
4262            for (name, def) in group_pdas {
4263                pda_lookup.entry(name.as_str()).or_insert(def);
4264            }
4265        }
4266
4267        let program_errors = idl
4268            .map(|idl| dedupe_errors_by_code(&idl.errors))
4269            .unwrap_or_default();
4270
4271        let mut needs = ProgramImports::default();
4272        let mut blocks: Vec<String> = Vec::new();
4273        let mut methods: Vec<String> = Vec::new();
4274        let mut skipped: Vec<(String, String)> = Vec::new();
4275        let mut uses_defined_types = false;
4276        for instr in group {
4277            let errors = if instr.errors.is_empty() {
4278                program_errors.clone()
4279            } else {
4280                dedupe_errors_by_code(&instr.errors)
4281            };
4282            match generate_rust_instruction_block(
4283                instr,
4284                &errors,
4285                &pda_lookup,
4286                &mut parser,
4287                &mut needs,
4288            ) {
4289                Ok(block) => {
4290                    blocks.push(block.code);
4291                    methods.push(block.method);
4292                    uses_defined_types |= block.uses_defined_types;
4293                }
4294                Err(reason) => skipped.push((instr.name.clone(), reason)),
4295            }
4296        }
4297
4298        // --- Program read layer: release identity + typed account readers. ---
4299        let read_layer = match reads.iter().find(|r| r.program_id == *program_id) {
4300            Some(r) => Ok((
4301                r.program_spec_hash.clone(),
4302                r.program_release_hash.clone(),
4303                r.descriptor.clone(),
4304            )),
4305            None => resolve_program_read_layer(program_specs, program_id),
4306        };
4307        let mut reader_methods: Vec<String> = Vec::new();
4308        let mut reader_notes: Vec<String> = Vec::new();
4309        if read_layer.is_ok() {
4310            let mut used_method_names: HashSet<String> = group
4311                .iter()
4312                .map(|instr| to_snake_case(&instr.name))
4313                .collect();
4314            used_method_names.insert("from_builder".to_string());
4315            let accounts = idl.map(|idl| idl.accounts.as_slice()).unwrap_or_default();
4316            for account in accounts {
4317                let Some(struct_name) = account_structs.get(&account.name).or_else(|| {
4318                    account_structs
4319                        .iter()
4320                        .find(|(name, _)| name.eq_ignore_ascii_case(&account.name))
4321                        .map(|(_, emitted)| emitted)
4322                }) else {
4323                    // No generated struct for this account type; no reader.
4324                    continue;
4325                };
4326                let method_name = format!("{}_accounts", to_snake_case(&account.name));
4327                if !used_method_names.insert(method_name.clone()) {
4328                    reader_notes.push(format!(
4329                        "account reader for `{}` skipped: method name `{}` collides with an instruction builder",
4330                        account.name, method_name
4331                    ));
4332                    continue;
4333                }
4334                reader_methods.push(format!(
4335                    "        /// Typed reader for `{account}` accounts (release-addressed HTTP reads).\n        pub fn {method_name}(&self) -> Result<arete_sdk::AccountReader<{types_path}::{struct_name}>, arete_sdk::AreteError> {{\n            Ok(arete_sdk::AccountReader::new(\n                {account_literal},\n                std::sync::Arc::new(self.builder.account_transport({program_literal}, &read_descriptor())?),\n            ))\n        }}",
4336                    account = account.name,
4337                    method_name = method_name,
4338                    types_path = types_path,
4339                    struct_name = struct_name,
4340                    account_literal = rust_string_literal(&account.name),
4341                    program_literal = rust_string_literal(&raw_name),
4342                ));
4343            }
4344        }
4345
4346        let mut sections: Vec<String> = Vec::new();
4347        if !blocks.is_empty() {
4348            let mut imports = vec!["BuiltInstruction", "InstructionError", "InstructionHandler"];
4349            if needs.account_meta {
4350                imports.extend(["AccountMeta", "AccountResolution"]);
4351            }
4352            if needs.arg_schema {
4353                imports.extend(["ArgSchema", "ArgType"]);
4354            }
4355            if uses_defined_types {
4356                imports.extend(["ArgField", "EnumVariantDef", "EnumVariantKind"]);
4357            }
4358            if needs.error_metadata {
4359                imports.push("ErrorMetadata");
4360            }
4361            if needs.pda {
4362                imports.extend(["PdaConfig", "PdaSeed"]);
4363            }
4364            imports.sort_unstable();
4365            sections.push(format!(
4366                "    use arete_sdk::instruction::{{{}}};\n    use serde::Serialize;",
4367                imports.join(", ")
4368            ));
4369        }
4370        sections.push(format!(
4371            "    pub const PROGRAM_ID: &str = {};",
4372            rust_string_literal(program_id)
4373        ));
4374        if let Ok((spec_hash, release_hash, descriptor)) = &read_layer {
4375            let descriptor_body = match descriptor {
4376                Some(descriptor) => {
4377                    let json = serde_json::to_string(descriptor)
4378                        .expect("program read descriptor must serialize");
4379                    format!(
4380                        "        serde_json::from_str({}).expect(\"generated hosted program read descriptor must be valid\")",
4381                        rust_string_literal(&json),
4382                    )
4383                }
4384                None => "        arete_sdk::ProgramReadDescriptor::LocalHttp {\n            release: arete_sdk::ProgramReleaseReference {\n                program_release_hash: PROGRAM_RELEASE_HASH.to_string(),\n                program_spec_hash: PROGRAM_SPEC_HASH.to_string(),\n            },\n        }".to_string(),
4385            };
4386            sections.push(format!(
4387                "    /// Content hash of the exact program specification captured at generation time.\n    pub const PROGRAM_SPEC_HASH: &str = {spec};\n\n    /// Release identity addressing hosted account reads for this program.\n    pub const PROGRAM_RELEASE_HASH: &str = {release};\n\n    /// Exact release-addressed read descriptor for this program.\n    pub fn read_descriptor() -> arete_sdk::ProgramReadDescriptor {{\n{descriptor_body}\n    }}",
4388                spec = rust_string_literal(spec_hash),
4389                release = rust_string_literal(release_hash),
4390            ));
4391        }
4392        sections.extend(blocks);
4393        if let Some(pdas_module) = own_pdas.and_then(generate_rust_pdas_module) {
4394            sections.push(pdas_module);
4395        }
4396
4397        // Program accessor: carries the client's program runtime so account
4398        // readers can build release-addressed transports. Instruction
4399        // builders stay pure and are also available as free functions.
4400        let builder_field = if reader_methods.is_empty() {
4401            // No generated reader uses the runtime (yet); silence dead_code.
4402            "        #[allow(dead_code)]\n        builder: arete_sdk::ProgramBuilder,"
4403        } else {
4404            "        builder: arete_sdk::ProgramBuilder,"
4405        };
4406        let mut impl_methods: Vec<String> = vec![
4407            "        /// Construct from the connected client's program runtime.\n        pub fn from_builder(builder: arete_sdk::ProgramBuilder) -> Self {\n            Self { builder }\n        }"
4408                .to_string(),
4409        ];
4410        impl_methods.extend(methods);
4411        impl_methods.extend(reader_methods);
4412        let program_struct = format!(
4413            "    /// Program accessor exposed on the stack client's `programs` namespace.\n    #[derive(Clone)]\n    pub struct {struct_name} {{\n{builder_field}\n    }}\n\n    impl {struct_name} {{\n{impl_methods}\n    }}",
4414            struct_name = struct_name,
4415            builder_field = builder_field,
4416            impl_methods = impl_methods.join("\n\n")
4417        );
4418        sections.push(program_struct);
4419
4420        let mut doc = format!(
4421            "/// Program SDK for `{}` (program ID `{}`).\n",
4422            raw_name, program_id
4423        );
4424        if let Err(reason) = &read_layer {
4425            doc.push_str(&format!(
4426                "///\n/// Program read layer omitted: {}.\n",
4427                reason
4428            ));
4429        }
4430        if !reader_notes.is_empty() {
4431            doc.push_str("///\n");
4432            for note in &reader_notes {
4433                doc.push_str(&format!("/// {}\n", note));
4434            }
4435        }
4436        if !skipped.is_empty() {
4437            doc.push_str("///\n/// Skipped instructions (unsupported by instruction codegen):\n");
4438            for (name, reason) in &skipped {
4439                doc.push_str(&format!("/// - `{}`: {}\n", name, reason));
4440            }
4441        }
4442        module_blocks.push(format!(
4443            "{doc}pub mod {module_name} {{\n{body}\n}}",
4444            doc = doc,
4445            module_name = module_name,
4446            body = sections.join("\n\n")
4447        ));
4448        modules.push(ProgramModule {
4449            module_name,
4450            struct_name,
4451        });
4452    }
4453
4454    let code = format!(
4455        "//! Generated program SDK: typed instruction builders grouped per program.\n//!\n//! Instruction building is pure (no network access). Each program module\n//! exposes `PROGRAM_ID`, typed `*Params` structs, `fn <instruction>(params)`\n//! builders returning `BuiltInstruction`, raw `*_handler()` accessors, and a\n//! `pdas` module with PDA derivation helpers. Programs with a recorded\n//! program spec additionally expose `PROGRAM_SPEC_HASH` /\n//! `PROGRAM_RELEASE_HASH`, a `read_descriptor()` for release-addressed HTTP\n//! reads, and typed `*_accounts()` readers on the program accessor. Standalone\n//! output also exports a `ProgramSdk` aggregate for direct/session composition.\n\n{}\n",
4456        module_blocks.join("\n\n")
4457    );
4458
4459    Some(ProgramsCodegen { code, modules })
4460}
4461
4462fn to_kebab_case(s: &str) -> String {
4463    let mut result = String::new();
4464    for (i, c) in s.chars().enumerate() {
4465        if c.is_uppercase() {
4466            if i > 0 {
4467                result.push('-');
4468            }
4469            result.push(c.to_lowercase().next().unwrap());
4470        } else {
4471            result.push(c);
4472        }
4473    }
4474    result
4475}
4476
4477fn to_pascal_case(s: &str) -> String {
4478    s.split(['_', '-', '.'])
4479        .map(|word| {
4480            let mut chars = word.chars();
4481            match chars.next() {
4482                None => String::new(),
4483                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
4484            }
4485        })
4486        .collect()
4487}
4488
4489fn to_snake_case(s: &str) -> String {
4490    let mut result = String::new();
4491    let mut separator = false;
4492    for ch in s.chars() {
4493        if ch.is_ascii_alphanumeric() {
4494            if separator && !result.is_empty() {
4495                result.push('_');
4496            }
4497            separator = false;
4498            if ch.is_ascii_uppercase() {
4499                if !result.is_empty() && !result.ends_with('_') {
4500                    result.push('_');
4501                }
4502                result.push(ch.to_ascii_lowercase());
4503            } else {
4504                result.push(ch.to_ascii_lowercase());
4505            }
4506        } else {
4507            separator = true;
4508        }
4509    }
4510    if result
4511        .chars()
4512        .next()
4513        .is_some_and(|character| character.is_ascii_digit())
4514    {
4515        result.insert_str(0, "value_");
4516    }
4517    if is_rust_keyword(&result) {
4518        result.push('_');
4519    }
4520    result
4521}
4522
4523/// Derive a valid Rust module name from an arbitrary alias or file stem
4524/// (lowercased, non-alphanumerics collapsed to `_`, keywords and leading
4525/// digits escaped). Shared with the CLI so staged devex extension files wire
4526/// up under the same stems the composition generator would use.
4527pub fn rust_module_name(value: &str) -> String {
4528    let mut output = String::new();
4529    let mut separator = false;
4530    for character in value.chars() {
4531        if character.is_ascii_alphanumeric() {
4532            if separator && !output.is_empty() {
4533                output.push('_');
4534            }
4535            separator = false;
4536            output.push(character.to_ascii_lowercase());
4537        } else {
4538            separator = true;
4539        }
4540    }
4541    if output
4542        .chars()
4543        .next()
4544        .is_some_and(|character| character.is_ascii_digit())
4545    {
4546        output.insert_str(0, "live_");
4547    }
4548    if is_rust_keyword(&output) {
4549        output.push_str("_live");
4550    }
4551    output
4552}
4553
4554fn is_rust_keyword(value: &str) -> bool {
4555    matches!(
4556        value,
4557        "as" | "async"
4558            | "await"
4559            | "break"
4560            | "const"
4561            | "continue"
4562            | "crate"
4563            | "dyn"
4564            | "else"
4565            | "enum"
4566            | "extern"
4567            | "false"
4568            | "fn"
4569            | "for"
4570            | "if"
4571            | "impl"
4572            | "in"
4573            | "let"
4574            | "loop"
4575            | "match"
4576            | "mod"
4577            | "move"
4578            | "mut"
4579            | "pub"
4580            | "ref"
4581            | "return"
4582            | "self"
4583            | "Self"
4584            | "static"
4585            | "struct"
4586            | "super"
4587            | "trait"
4588            | "true"
4589            | "type"
4590            | "union"
4591            | "unsafe"
4592            | "use"
4593            | "where"
4594            | "while"
4595            | "abstract"
4596            | "become"
4597            | "box"
4598            | "do"
4599            | "final"
4600            | "macro"
4601            | "override"
4602            | "priv"
4603            | "typeof"
4604            | "unsized"
4605            | "virtual"
4606            | "yield"
4607            | "try"
4608    )
4609}