1use std::fmt;
15use std::fmt::Write as _;
16use std::sync::LazyLock;
17
18use regex::Regex;
19use sha2::{Digest, Sha256};
20
21use crate::decorators::DecoratorSet;
22use crate::grammar::decorate_emitted;
23use crate::translation::check::check_program;
24use crate::translation::diagnostics::TranslationError;
25use crate::translation::emit_common::Emitted;
26use crate::translation::{
27 emit_javascript::emit_javascript, emit_rust::emit_rust, javascript::parse_javascript,
28 rust::parse_rust,
29};
30use crate::{LinkNetwork, ParseConfiguration};
31
32pub const SELF_TRANSLATION_LANGUAGES: [&str; 3] = ["JavaScript", "TypeScript", "Rust"];
34
35const HEADER: &str = "// meta-language:self-translation:v1 ";
36const CARRIED: &str = "// meta-language:carried ";
37const TRANSLATED: &str = "// meta-language:translated ";
38const SOURCE_LINE: &str = "// |";
39const PRELUDE_BEGIN: &str = "// meta-language:prelude begin";
40const PRELUDE_END: &str = "// meta-language:prelude end";
41const RUST_ALLOW: &str = "#![allow(unused, unreachable_patterns, non_snake_case, non_camel_case_types, invalid_nan_comparisons)]";
43const COMMENTS: [&str; 3] = ["comment", "line_comment", "block_comment"];
44
45static RUST_EXPORT: LazyLock<Regex> =
46 LazyLock::new(|| Regex::new(r"(?m)^pub(\([^)]*\))?\s").expect("a valid pattern"));
47static JAVASCRIPT_EXPORT: LazyLock<Regex> =
48 LazyLock::new(|| Regex::new(r"(?m)^export\s").expect("a valid pattern"));
49static RUST_MAIN: LazyLock<Regex> =
50 LazyLock::new(|| Regex::new(r"\bfn\s+main\s*\(").expect("a valid pattern"));
51static LINE_BREAK: LazyLock<Regex> =
52 LazyLock::new(|| Regex::new(r"\r?\n").expect("a valid pattern"));
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct SelfTranslationError {
57 pub message: String,
59}
60
61impl fmt::Display for SelfTranslationError {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 formatter.write_str(&self.message)
64 }
65}
66
67impl std::error::Error for SelfTranslationError {}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct SelfTranslationItem {
72 pub term: String,
74 pub start: usize,
76 pub end: usize,
78 pub status: &'static str,
82 pub reason: Option<String>,
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct SelfTranslation {
89 pub source_language: &'static str,
91 pub target_language: &'static str,
93 pub code: String,
95 pub items: Vec<SelfTranslationItem>,
97}
98
99#[must_use]
101pub fn self_translation_language(language: &str) -> Option<&'static str> {
102 match language.to_lowercase().as_str() {
103 "javascript" | "js" | "mjs" => Some("JavaScript"),
104 "typescript" | "ts" => Some("TypeScript"),
105 "rust" | "rs" => Some("Rust"),
106 _ => None,
107 }
108}
109
110fn required(language: &str) -> Result<&'static str, SelfTranslationError> {
111 self_translation_language(language).ok_or_else(|| SelfTranslationError {
112 message: format!(
113 "self-translation reads and writes {}, not {language}",
114 SELF_TRANSLATION_LANGUAGES.join(", ")
115 ),
116 })
117}
118
119fn family(language: &str) -> &'static str {
122 if language == "Rust" {
123 "Rust"
124 } else {
125 "JavaScript"
126 }
127}
128
129fn sha256(text: &str) -> String {
130 Sha256::digest(text.as_bytes())
131 .iter()
132 .fold(String::new(), |mut hex, byte| {
133 let _ = write!(hex, "{byte:02x}");
134 hex
135 })
136}
137
138#[derive(Clone, Debug)]
139struct Item<'a> {
140 term: String,
141 start: usize,
142 end: usize,
143 comment: bool,
144 text: &'a str,
145 after: &'a str,
146}
147
148pub fn self_translate(
156 source: &str,
157 source_language: &str,
158 target_language: &str,
159) -> Result<SelfTranslation, SelfTranslationError> {
160 self_translate_decorated(
161 source,
162 source_language,
163 target_language,
164 &DecoratorSet::default(),
165 )
166}
167
168pub fn self_translate_decorated(
178 source: &str,
179 source_language: &str,
180 target_language: &str,
181 decorators: &DecoratorSet,
182) -> Result<SelfTranslation, SelfTranslationError> {
183 let from = required(source_language)?;
184 let to = required(target_language)?;
185 let reconstructed =
186 LinkNetwork::parse(source, from, ParseConfiguration::default()).reconstruct_text();
187 if reconstructed != source {
188 return Err(SelfTranslationError {
189 message: format!("the {from} links of the source do not reproduce it"),
190 });
191 }
192 let items = top_level_items(source, from);
193 if family(from) == family(to) {
194 let items = items
195 .iter()
196 .map(|item| SelfTranslationItem {
197 term: item.term.clone(),
198 start: item.start,
199 end: item.end,
200 status: "kept",
201 reason: None,
202 })
203 .collect();
204 return Ok(SelfTranslation {
205 source_language: from,
206 target_language: to,
207 code: reconstructed,
208 items,
209 });
210 }
211 let mut blocks: Vec<String> = Vec::new();
212 let mut preludes: Vec<String> = Vec::new();
213 let mut recorded = Vec::new();
214 let mut gap = String::new();
215 for group in group_items(&items, source) {
216 let out = translate_group(&group, from, to, decorators);
217 for prelude in out.preludes {
218 if !preludes.contains(&prelude) {
219 preludes.push(prelude);
220 }
221 }
222 for item in &group.items {
223 recorded.push(SelfTranslationItem {
224 term: item.term.clone(),
225 start: item.start,
226 end: item.end,
227 status: out.status,
228 reason: out.reason.clone(),
229 });
230 }
231 if let Some(code) = out.code {
232 let separator = if blocks.is_empty() {
233 String::new()
234 } else {
235 breaks(&gap)
236 };
237 blocks.push(format!("{separator}{code}"));
238 group.after.clone_into(&mut gap);
239 } else if gap.is_empty() {
240 group.after.clone_into(&mut gap);
242 }
243 }
244 let body = format!("{}\n", blocks.concat());
245 let header = items
247 .iter()
248 .find(|item| item.comment && item.text.starts_with(HEADER));
249 if let Some(header) = header
250 && header.text.contains(&format!(" source={to} "))
251 && header.text.contains(&format!(" sha256={} ", sha256(&body)))
252 {
253 return Ok(SelfTranslation {
254 source_language: from,
255 target_language: to,
256 code: body,
257 items: recorded,
258 });
259 }
260 if family(to) == "Rust" && recorded.iter().any(|item| item.status == "translated") {
261 preludes.insert(0, RUST_ALLOW.to_owned());
262 }
263 let mut lines = vec![
264 format!(
265 "{HEADER}source={from} target={to} sha256={} bytes={}",
266 sha256(source),
267 source.len()
268 ),
269 String::new(),
270 ];
271 if !preludes.is_empty() {
272 lines.push(PRELUDE_BEGIN.to_owned());
273 for (index, prelude) in preludes.into_iter().enumerate() {
274 if index > 0 {
275 lines.push(String::new());
276 }
277 lines.push(prelude);
278 }
279 lines.push(PRELUDE_END.to_owned());
280 lines.push(String::new());
281 }
282 Ok(SelfTranslation {
283 source_language: from,
284 target_language: to,
285 code: format!("{}\n{body}", lines.join("\n")),
286 items: recorded,
287 })
288}
289
290fn breaks(gap: &str) -> String {
292 "\n".repeat(gap.matches('\n').count().max(1))
293}
294
295fn top_level_items<'a>(text: &'a str, language: &str) -> Vec<Item<'a>> {
297 let bytes = text.as_bytes();
298 let nodes: Vec<(String, usize, usize)> =
299 crate::tree_sitter_adapter::top_level_nodes(text, language)
300 .unwrap_or_default()
301 .into_iter()
302 .filter(|(term, _, _)| term != "whitespace")
303 .map(|(term, start, mut end)| {
304 if COMMENTS.contains(&term.as_str()) && end > 0 && bytes[end - 1] == b'\n' {
306 end -= if end > 1 && bytes[end - 2] == b'\r' {
307 2
308 } else {
309 1
310 };
311 }
312 (term, start, end)
313 })
314 .collect();
315 nodes
316 .iter()
317 .enumerate()
318 .map(|(index, (term, start, end))| Item {
319 term: term.clone(),
320 start: *start,
321 end: *end,
322 comment: COMMENTS.contains(&term.as_str()),
323 text: &text[*start..*end],
324 after: &text[*end..nodes.get(index + 1).map_or(text.len(), |next| next.1)],
325 })
326 .collect()
327}
328
329fn line_break(layout: &str) -> bool {
330 layout == "\n" || layout == "\r\n"
331}
332
333fn source_lines(items: &[Item<'_>]) -> Vec<String> {
334 items
335 .iter()
336 .map(|item| {
337 let rest = &item.text[SOURCE_LINE.len()..];
338 rest.strip_prefix(' ').unwrap_or(rest).to_owned()
339 })
340 .collect()
341}
342
343fn is_marker(item: &Item<'_>) -> bool {
344 item.comment
345 && [HEADER, CARRIED, TRANSLATED, PRELUDE_BEGIN]
346 .iter()
347 .any(|marker| item.text.starts_with(marker))
348}
349
350#[derive(Debug)]
351enum GroupKind {
352 Provenance,
353 Carried {
354 language: String,
355 lines: Vec<String>,
356 },
357 Comment {
358 text: String,
359 term: String,
360 },
361 Item {
362 text: String,
363 term: String,
364 },
365}
366
367struct Group<'a> {
368 kind: GroupKind,
369 items: Vec<Item<'a>>,
370 after: &'a str,
371}
372
373fn group_items<'a>(items: &[Item<'a>], text: &'a str) -> Vec<Group<'a>> {
377 let mut groups = Vec::new();
378 let between = |first: &Item<'_>, last: &Item<'_>| text[first.start..last.end].to_owned();
379 let lines_after = |at: usize| {
381 let mut end = at;
382 while end + 1 < items.len()
383 && items[end + 1].comment
384 && items[end + 1].text.starts_with(SOURCE_LINE)
385 && line_break(items[end].after)
386 {
387 end += 1;
388 }
389 end
390 };
391 let mut index = 0;
392 while index < items.len() {
393 let item = &items[index];
394 let (end, kind) = if item.comment && item.text.starts_with(HEADER) {
395 (index, GroupKind::Provenance)
396 } else if item.comment && item.text == PRELUDE_BEGIN {
397 let mut end = index;
398 while end + 1 < items.len() && items[end].text != PRELUDE_END {
399 end += 1;
400 }
401 (end, GroupKind::Provenance)
402 } else if item.comment && item.text.starts_with(CARRIED) {
403 let end = lines_after(index);
404 let language = first_word(&item.text[CARRIED.len()..]);
405 let lines = source_lines(&items[index + 1..=end]);
406 (end, GroupKind::Carried { language, lines })
407 } else if item.comment && item.text.starts_with(TRANSLATED) {
408 let rest = &item.text[TRANSLATED.len()..];
409 let field = |name: &str| {
410 rest.split(' ').skip(2).find_map(|field| {
411 let (key, value) = field.split_once('=').unwrap_or((field, ""));
412 (key == name).then_some(value)
413 })
414 };
415 let lines_end = lines_after(index);
416 let count = field("items").and_then(|count| count.parse::<usize>().ok());
417 match count {
418 Some(count) if count > 0 && lines_end + count < items.len() => {
419 let last = lines_end + count;
420 let code = between(&items[lines_end + 1], &items[last]);
421 if field("sha256") == Some(sha256(&code).as_str()) {
422 let lines = source_lines(&items[index + 1..=lines_end]);
423 (
424 last,
425 GroupKind::Carried {
426 language: first_word(rest),
427 lines,
428 },
429 )
430 } else {
431 (lines_end, GroupKind::Provenance)
433 }
434 }
435 _ => (
436 index,
437 GroupKind::Comment {
438 text: item.text.to_owned(),
439 term: item.term.clone(),
440 },
441 ),
442 }
443 } else {
444 let mut end = index;
446 while items[end].comment
447 && end + 1 < items.len()
448 && line_break(items[end].after)
449 && !is_marker(&items[end + 1])
450 {
451 end += 1;
452 }
453 let text = between(item, &items[end]);
454 let term = items[end].term.clone();
455 if items[end].comment {
457 (end, GroupKind::Comment { text, term })
458 } else {
459 (end, GroupKind::Item { text, term })
460 }
461 };
462 groups.push(Group {
463 kind,
464 items: items[index..=end].to_vec(),
465 after: items[end].after,
466 });
467 index = end + 1;
468 }
469 groups
470}
471
472fn first_word(text: &str) -> String {
473 text.split(' ').next().unwrap_or_default().to_owned()
474}
475
476struct Translated {
477 code: Option<String>,
478 preludes: Vec<String>,
479 status: &'static str,
480 reason: Option<String>,
481}
482
483fn translate_group(
484 group: &Group<'_>,
485 from: &str,
486 to: &str,
487 decorators: &DecoratorSet,
488) -> Translated {
489 let done = |code: String, status: &'static str| Translated {
490 code: Some(code),
491 preludes: Vec::new(),
492 status,
493 reason: None,
494 };
495 let (text, term) = match &group.kind {
496 GroupKind::Provenance => {
497 return Translated {
498 code: None,
499 preludes: Vec::new(),
500 status: "provenance",
501 reason: None,
502 };
503 }
504 GroupKind::Carried { language, lines } => {
505 if family(language) == family(to) {
506 return done(lines.join("\n"), "restored");
507 }
508 let mut code = vec![group.items[0].text.to_owned()];
509 code.extend(lines.iter().map(|line| source_line(line)));
510 return Translated {
511 reason: Some("carried from another language".to_owned()),
512 ..done(code.join("\n"), "carried")
513 };
514 }
515 GroupKind::Comment { text, term } => {
516 if group
518 .items
519 .iter()
520 .all(|item| comment_fits(item.text, from) && comment_fits(item.text, to))
521 {
522 return done(text.clone(), "comment");
523 }
524 return carry(text, term, from, "comment the target cannot hold");
525 }
526 GroupKind::Item { text, term } => (text, term),
527 };
528 let emitted = match emit_item(text, from, to) {
529 Ok(Some(emitted)) => emitted,
530 Ok(None) => return carry(text, term, from, "top-level statement"),
531 Err(error) => return carry(text, term, from, error.kind.as_str()),
532 };
533 if emitted.definitions.is_empty() {
534 return carry(text, term, from, "no definition");
535 }
536 let exported = family(to) == "JavaScript"
537 && if family(from) == "Rust" {
538 RUST_EXPORT.is_match(text)
539 } else {
540 JAVASCRIPT_EXPORT.is_match(text)
541 };
542 let generic = emitted
543 .definitions
544 .iter()
545 .map(|definition| {
546 if exported {
547 format!("export {definition}")
548 } else {
549 definition.clone()
550 }
551 })
552 .collect::<Vec<_>>()
553 .join("\n\n");
554 let code = decorate_emitted(to, &generic, decorators);
555 if code.trim().is_empty() {
556 return carry(text, term, from, "dropped by a decorator");
557 }
558 let marker = format!(
559 "{TRANSLATED}{from} {term} items={} sha256={}",
560 emitted.definitions.len(),
561 sha256(&code)
562 );
563 let mut lines = vec![marker];
564 lines.extend(LINE_BREAK.split(text).map(source_line));
565 lines.push(code);
566 Translated {
567 code: Some(lines.join("\n")),
568 preludes: emitted.preludes,
569 status: "translated",
570 reason: None,
571 }
572}
573
574fn emit_item(text: &str, from: &str, to: &str) -> Result<Option<Emitted>, TranslationError> {
576 let surface = if family(from) == "Rust" {
577 if RUST_MAIN.is_match(text) {
579 parse_rust(text)?
580 } else {
581 parse_rust(&format!("{text}\nfn main() {{}}\n"))?
582 }
583 } else {
584 parse_javascript(text)?
585 };
586 let program = check_program(&surface)?;
587 if program
588 .main
589 .as_ref()
590 .is_some_and(|main| !main.effects.is_empty())
591 {
592 return Ok(None);
593 }
594 let emitted = if family(to) == "Rust" {
595 emit_rust(&program)?
596 } else {
597 emit_javascript(&program)?
598 };
599 Ok(Some(emitted))
600}
601
602fn carry(text: &str, term: &str, from: &str, reason: &str) -> Translated {
603 let mut lines = vec![format!("{CARRIED}{from} {term} ({reason})")];
604 lines.extend(LINE_BREAK.split(text).map(source_line));
605 Translated {
606 code: Some(lines.join("\n")),
607 preludes: Vec::new(),
608 status: "carried",
609 reason: Some(reason.to_owned()),
610 }
611}
612
613fn source_line(line: &str) -> String {
614 if line.is_empty() {
615 SOURCE_LINE.to_owned()
616 } else {
617 format!("{SOURCE_LINE} {line}")
618 }
619}
620
621fn comment_fits(text: &str, to: &str) -> bool {
624 if text.starts_with("/*") {
625 let inner = text
626 .get(2..text.len().saturating_sub(2))
627 .unwrap_or_default();
628 if inner.contains("/*") || inner.contains("*/") {
629 return false;
630 }
631 }
632 family(to) != "Rust"
633 || !["///", "//!", "/**", "/*!"]
634 .iter()
635 .any(|prefix| text.starts_with(prefix))
636}