1#![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
25pub 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 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}