Skip to main content

parallax_migrate/
lib.rs

1//! Cross-runtime migration engine and semantic-loss analysis.
2
3#![deny(unsafe_code)]
4#![warn(missing_docs)]
5
6mod analyze;
7mod contract;
8mod convert;
9mod report;
10
11pub use analyze::{analyze_document, analyze_value, LossFinding};
12pub use contract::{
13    analyze_contract, analyze_ues_contract, require_contract, ContractAnalysis, ContractFinding,
14    MigrationContract, RequiredSemantic,
15};
16pub use convert::{convert_document, convert_value};
17pub use report::{MigrationReport, MigrationTimings};
18
19use parallax_core::{
20    ConversionPolicy, ErrorCode, MigrationId, ParallaxError, Remediation, RuntimeKind, SemanticLoss,
21};
22use parallax_ir::PirDocument;
23use std::time::Instant;
24
25/// Perform an offline PIR migration analysis + conversion.
26pub fn migrate_document(
27    source_runtime: RuntimeKind,
28    target_runtime: RuntimeKind,
29    doc: &PirDocument,
30    policy: &ConversionPolicy,
31) -> parallax_core::Result<(PirDocument, MigrationReport)> {
32    let id = MigrationId::new();
33    let t0 = Instant::now();
34    let findings = analyze_document(doc, &source_runtime, &target_runtime, policy);
35    let analyze_us = t0.elapsed().as_micros() as u64;
36
37    let worst = findings
38        .iter()
39        .map(|f| f.loss)
40        .fold(SemanticLoss::None, SemanticLoss::worsen);
41
42    if worst.blocks_default_migration() && !policy.allows(worst) {
43        let detail = findings
44            .iter()
45            .filter(|f| f.loss.blocks_default_migration())
46            .map(|f| format!("{} @ {}: {}", f.loss, f.path, f.message))
47            .collect::<Vec<_>>()
48            .join("\n");
49        return Err(ParallaxError::new(
50            ErrorCode::MigrationRejected,
51            format!(
52                "migration {} → {} rejected due to semantic loss ({worst})",
53                source_runtime, target_runtime
54            ),
55        )
56        .with_source("parallax-migrate")
57        .with_operation("migrate_document")
58        .context("findings", detail)
59        .remediate(Remediation::with_detail(
60            "Re-run with --allow-lossy if the loss is acceptable",
61            "Or adjust values / use BigInt-friendly targets",
62        )));
63    }
64
65    // Unsupported values: either reject or keep as Unsupported nodes.
66    if policy.reject_unsupported && findings.iter().any(|f| f.loss == SemanticLoss::Unsupported) {
67        return Err(ParallaxError::new(
68            ErrorCode::UnsupportedValue,
69            "migration contains unsupported values",
70        )
71        .with_source("parallax-migrate")
72        .with_operation("migrate_document"));
73    }
74
75    let t1 = Instant::now();
76    let converted = convert_document(doc, &source_runtime, &target_runtime, policy)?;
77    let convert_us = t1.elapsed().as_micros() as u64;
78
79    let report = MigrationReport {
80        id,
81        source_runtime,
82        target_runtime,
83        policy: policy.clone(),
84        findings,
85        worst_loss: worst,
86        timings: MigrationTimings {
87            analyze_us,
88            convert_us,
89            capture_us: None,
90            restore_us: None,
91            total_us: analyze_us + convert_us,
92        },
93        success: true,
94        notes: Vec::new(),
95    };
96    Ok((converted, report))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use parallax_ir::{PirInteger, PirValue};
103
104    #[test]
105    fn rejects_unsafe_int_without_bigint_or_lossy() {
106        let mut doc = PirDocument::new();
107        doc.set_binding(
108            "n",
109            PirValue::Int {
110                v: PirInteger::from_decimal("9007199254740993"),
111            },
112        );
113        let policy = ConversionPolicy {
114            prefer_bigint: false,
115            allow_lossy: false,
116            ..ConversionPolicy::default()
117        };
118        let err = migrate_document(RuntimeKind::Python, RuntimeKind::JavaScript, &doc, &policy)
119            .unwrap_err();
120        assert_eq!(err.code, ErrorCode::MigrationRejected);
121    }
122
123    #[test]
124    fn prefer_bigint_converts_unsafe_int() {
125        let mut doc = PirDocument::new();
126        doc.set_binding(
127            "n",
128            PirValue::Int {
129                v: PirInteger::from_decimal("9007199254740993"),
130            },
131        );
132        let (out, report) = migrate_document(
133            RuntimeKind::Python,
134            RuntimeKind::JavaScript,
135            &doc,
136            &ConversionPolicy::default(),
137        )
138        .unwrap();
139        assert!(report.success);
140        assert!(matches!(out.binding("n"), Some(PirValue::BigInt { .. })));
141    }
142
143    #[test]
144    fn reject_unsupported_policy_fails_on_function() {
145        let mut doc = PirDocument::new();
146        doc.set_binding(
147            "fn",
148            PirValue::Function {
149                name: Some("f".into()),
150                descriptor: "<fn>".into(),
151            },
152        );
153        let err = migrate_document(
154            RuntimeKind::Python,
155            RuntimeKind::JavaScript,
156            &doc,
157            &ConversionPolicy::strict(),
158        )
159        .unwrap_err();
160        assert!(matches!(
161            err.code,
162            ErrorCode::MigrationRejected | ErrorCode::UnsupportedValue
163        ));
164    }
165
166    #[test]
167    fn keeps_unsupported_under_default_policy() {
168        let mut doc = PirDocument::new();
169        doc.set_binding(
170            "fn",
171            PirValue::Function {
172                name: Some("f".into()),
173                descriptor: "<fn>".into(),
174            },
175        );
176        let (out, report) = migrate_document(
177            RuntimeKind::Python,
178            RuntimeKind::JavaScript,
179            &doc,
180            &ConversionPolicy::default(),
181        )
182        .unwrap();
183        assert!(report.success);
184        assert!(matches!(
185            out.binding("fn"),
186            Some(PirValue::Unsupported { .. })
187        ));
188    }
189
190    #[test]
191    fn migrates_demo_state() {
192        use indexmap::IndexMap;
193        use parallax_ir::PirMapEntry;
194        let mut fields: IndexMap<String, PirValue> = IndexMap::new();
195        fields.insert("username".into(), PirValue::string("Ada"));
196        fields.insert("score".into(), PirValue::int_i64(42));
197        fields.insert(
198            "projects".into(),
199            PirValue::list(vec![
200                PirValue::string("compiler"),
201                PirValue::string("runtime"),
202                PirValue::string("vm"),
203            ]),
204        );
205        let state = PirValue::Map {
206            entries: fields
207                .into_iter()
208                .map(|(k, v)| PirMapEntry {
209                    key: PirValue::string(k),
210                    value: v,
211                })
212                .collect(),
213        };
214        let mut doc = PirDocument::new();
215        doc.set_binding("state", state);
216        let (out, report) = migrate_document(
217            RuntimeKind::Python,
218            RuntimeKind::JavaScript,
219            &doc,
220            &ConversionPolicy::default(),
221        )
222        .unwrap();
223        assert!(report.success);
224        assert!(out.binding("state").is_some());
225    }
226}