Skip to main content

runar_compiler_rust/
artifact.rs

1//! Rúnar Artifact -- the final compiled output of a Rúnar compiler.
2//!
3//! This is what gets consumed by wallets, SDKs, and deployment tooling.
4
5use serde::{Deserialize, Serialize};
6
7use crate::codegen::emit::{ConstructorSlot, SourceMapping};
8use crate::ir::ANFProgram;
9
10// ---------------------------------------------------------------------------
11// ABI types
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ABIParam {
16    pub name: String,
17    #[serde(rename = "type")]
18    pub param_type: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ABIConstructor {
23    pub params: Vec<ABIParam>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ABIMethod {
28    pub name: String,
29    pub params: Vec<ABIParam>,
30    #[serde(rename = "isPublic")]
31    pub is_public: bool,
32    #[serde(rename = "isTerminal", skip_serializing_if = "Option::is_none")]
33    pub is_terminal: Option<bool>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ABI {
38    pub constructor: ABIConstructor,
39    pub methods: Vec<ABIMethod>,
40}
41
42// ---------------------------------------------------------------------------
43// State fields
44// ---------------------------------------------------------------------------
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct StateField {
48    pub name: String,
49    #[serde(rename = "type")]
50    pub field_type: String,
51    pub index: usize,
52    #[serde(rename = "initialValue", skip_serializing_if = "Option::is_none")]
53    pub initial_value: Option<serde_json::Value>,
54}
55
56// ---------------------------------------------------------------------------
57// Source map
58// ---------------------------------------------------------------------------
59
60/// Source-level debug mappings (opcode index to source location).
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SourceMapData {
63    pub mappings: Vec<SourceMapping>,
64}
65
66/// Optional IR snapshots for debugging / conformance checking.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct IRDebug {
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub anf: Option<ANFProgram>,
71}
72
73// ---------------------------------------------------------------------------
74// Top-level artifact
75// ---------------------------------------------------------------------------
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct RunarArtifact {
79    pub version: String,
80    #[serde(rename = "compilerVersion")]
81    pub compiler_version: String,
82    #[serde(rename = "contractName")]
83    pub contract_name: String,
84    pub abi: ABI,
85    pub script: String,
86    pub asm: String,
87    #[serde(rename = "sourceMap", skip_serializing_if = "Option::is_none")]
88    pub source_map: Option<SourceMapData>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ir: Option<IRDebug>,
91    #[serde(rename = "stateFields", skip_serializing_if = "Vec::is_empty")]
92    pub state_fields: Vec<StateField>,
93    #[serde(rename = "constructorSlots", skip_serializing_if = "Vec::is_empty", default)]
94    pub constructor_slots: Vec<ConstructorSlot>,
95    #[serde(rename = "codeSeparatorIndex", skip_serializing_if = "Option::is_none")]
96    pub code_separator_index: Option<usize>,
97    #[serde(rename = "codeSeparatorIndices", skip_serializing_if = "Option::is_none")]
98    pub code_separator_indices: Option<Vec<usize>>,
99    #[serde(rename = "buildTimestamp")]
100    pub build_timestamp: String,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub anf: Option<ANFProgram>,
103}
104
105// ---------------------------------------------------------------------------
106// Assembly
107// ---------------------------------------------------------------------------
108
109const SCHEMA_VERSION: &str = "runar-v0.4.4";
110const COMPILER_VERSION: &str = "0.4.4-rust";
111
112/// Build a RunarArtifact from the compilation products.
113pub fn assemble_artifact(
114    program: &ANFProgram,
115    script_hex: &str,
116    script_asm: &str,
117    constructor_slots: Vec<ConstructorSlot>,
118    code_separator_index: i64,
119    code_separator_indices: Vec<usize>,
120    include_anf: bool,
121    source_mappings: Vec<SourceMapping>,
122) -> RunarArtifact {
123    // Build constructor params from properties, excluding those with initializers
124    // (properties with default values are not constructor parameters).
125    let constructor_params: Vec<ABIParam> = program
126        .properties
127        .iter()
128        .filter(|p| p.initial_value.is_none())
129        .map(|p| ABIParam {
130            name: p.name.clone(),
131            param_type: p.prop_type.clone(),
132        })
133        .collect();
134
135    // Build state fields for stateful contracts.
136    // Index = property position (matching constructor arg order), not sequential mutable index.
137    let mut state_fields = Vec::new();
138    for (i, prop) in program.properties.iter().enumerate() {
139        if !prop.readonly {
140            state_fields.push(StateField {
141                name: prop.name.clone(),
142                field_type: prop.prop_type.clone(),
143                index: i,
144                initial_value: prop.initial_value.clone(),
145            });
146        }
147    }
148    let is_stateful = !state_fields.is_empty();
149
150    // Build method ABIs (exclude constructor — it's in abi.constructor, not methods)
151    let methods: Vec<ABIMethod> = program
152        .methods
153        .iter()
154        .filter(|m| m.name != "constructor")
155        .map(|m| {
156            // For stateful contracts, mark public methods without _changePKH as terminal
157            let is_terminal = if is_stateful && m.is_public {
158                let has_change = m.params.iter().any(|p| p.name == "_changePKH");
159                if !has_change { Some(true) } else { None }
160            } else {
161                None
162            };
163            ABIMethod {
164                name: m.name.clone(),
165                params: m
166                    .params
167                    .iter()
168                    .map(|p| ABIParam {
169                        name: p.name.clone(),
170                        param_type: p.param_type.clone(),
171                    })
172                    .collect(),
173                is_public: m.is_public,
174                is_terminal,
175            }
176        })
177        .collect();
178
179    // Timestamp
180    let now = chrono_lite_utc_now();
181
182    let cs_index = if code_separator_index >= 0 {
183        Some(code_separator_index as usize)
184    } else {
185        None
186    };
187    let cs_indices = if code_separator_indices.is_empty() {
188        None
189    } else {
190        Some(code_separator_indices)
191    };
192
193    let anf = if include_anf {
194        Some(program.clone())
195    } else {
196        None
197    };
198
199    let source_map = if source_mappings.is_empty() {
200        None
201    } else {
202        Some(SourceMapData {
203            mappings: source_mappings,
204        })
205    };
206
207    let ir = if include_anf {
208        Some(IRDebug {
209            anf: Some(program.clone()),
210        })
211    } else {
212        None
213    };
214
215    RunarArtifact {
216        version: SCHEMA_VERSION.to_string(),
217        compiler_version: COMPILER_VERSION.to_string(),
218        contract_name: program.contract_name.clone(),
219        abi: ABI {
220            constructor: ABIConstructor {
221                params: constructor_params,
222            },
223            methods,
224        },
225        script: script_hex.to_string(),
226        asm: script_asm.to_string(),
227        source_map,
228        ir,
229        state_fields,
230        constructor_slots,
231        code_separator_index: cs_index,
232        code_separator_indices: cs_indices,
233        build_timestamp: now,
234        anf,
235    }
236}
237
238/// Simple UTC timestamp without pulling in the full chrono crate.
239fn chrono_lite_utc_now() -> String {
240    use std::time::{SystemTime, UNIX_EPOCH};
241
242    let duration = SystemTime::now()
243        .duration_since(UNIX_EPOCH)
244        .unwrap_or_default();
245    let secs = duration.as_secs();
246
247    // Convert epoch seconds to a rough ISO-8601 string.
248    // This is a simplified implementation; for production use chrono.
249    let days = secs / 86400;
250    let time_of_day = secs % 86400;
251    let hours = time_of_day / 3600;
252    let minutes = (time_of_day % 3600) / 60;
253    let seconds = time_of_day % 60;
254
255    // Days since epoch to Y-M-D (simplified leap-year-aware calculation)
256    let (year, month, day) = epoch_days_to_ymd(days);
257
258    format!(
259        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
260        year, month, day, hours, minutes, seconds
261    )
262}
263
264fn epoch_days_to_ymd(days: u64) -> (u64, u64, u64) {
265    // Civil date algorithm from Howard Hinnant
266    let z = days + 719468;
267    let era = z / 146097;
268    let doe = z - era * 146097;
269    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
270    let y = yoe + era * 400;
271    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
272    let mp = (5 * doy + 2) / 153;
273    let d = doy - (153 * mp + 2) / 5 + 1;
274    let m = if mp < 10 { mp + 3 } else { mp - 9 };
275    let year = if m <= 2 { y + 1 } else { y };
276    (year, m, d)
277}