parallax-cli 0.1.0

Parallax CLI — universal polyglot execution runtime
Documentation
//! Emit target-language source previews from PIR bindings.

use parallax_core::{ErrorCode, ParallaxError};
use parallax_ir::{PirDocument, PirValue};

/// Emit a JavaScript module assigning captured bindings.
pub fn emit_javascript(doc: &PirDocument) -> Result<String, ParallaxError> {
    let mut out = String::from("// Generated by Parallax migrate - preview of restored bindings\n");
    for (name, value) in &doc.bindings {
        out.push_str(&format!(
            "const {} = {};\n",
            ident(name)?,
            js_expr(value, 0)?
        ));
    }
    if let Some(state) = doc.binding("state") {
        out.push_str("\nconsole.log(JSON.stringify(state, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));\n");
        let _ = state;
    }
    Ok(out)
}

/// Emit a Python module assigning captured bindings.
pub fn emit_python(doc: &PirDocument) -> Result<String, ParallaxError> {
    let mut out = String::from("# Generated by Parallax migrate - preview of restored bindings\n");
    for (name, value) in &doc.bindings {
        out.push_str(&format!("{} = {}\n", ident(name)?, py_expr(value, 0)?));
    }
    if doc.binding("state").is_some() {
        out.push_str("\nprint(state)\n");
    }
    Ok(out)
}

fn ident(name: &str) -> Result<&str, ParallaxError> {
    if name
        .chars()
        .next()
        .map(|c| c.is_ascii_alphabetic() || c == '_')
        .unwrap_or(false)
        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
    {
        Ok(name)
    } else {
        Err(ParallaxError::new(
            ErrorCode::UnsupportedValue,
            format!("cannot emit binding name as identifier: {name}"),
        ))
    }
}

fn js_float_literal(v: f64) -> String {
    if v.is_nan() {
        "NaN".into()
    } else if v == f64::INFINITY {
        "Infinity".into()
    } else if v == f64::NEG_INFINITY {
        "-Infinity".into()
    } else {
        format!("{v}")
    }
}

fn py_float_literal(v: f64) -> String {
    if v.is_nan() {
        "float('nan')".into()
    } else if v == f64::INFINITY {
        "float('inf')".into()
    } else if v == f64::NEG_INFINITY {
        "float('-inf')".into()
    } else {
        format!("{v}")
    }
}

fn js_expr(v: &PirValue, depth: usize) -> Result<String, ParallaxError> {
    if depth > 64 {
        return Err(ParallaxError::new(
            ErrorCode::UnsupportedValue,
            "value nesting too deep",
        ));
    }
    Ok(match v {
        PirValue::Null => "null".into(),
        PirValue::Bool { v } => if *v { "true" } else { "false" }.into(),
        PirValue::Int { v } => v.decimal.clone(),
        PirValue::Float { v } => js_float_literal(*v),
        PirValue::String { v } => serde_json::to_string(v).unwrap_or_else(|_| "\"\"".into()),
        PirValue::Bytes { v } => format!(
            "new Uint8Array({})",
            serde_json::to_string(v).unwrap_or_else(|_| "[]".into())
        ),
        PirValue::List { v } | PirValue::Tuple { v } | PirValue::Set { v } => {
            let parts: Result<Vec<_>, _> = v.iter().map(|x| js_expr(x, depth + 1)).collect();
            format!("[{}]", parts?.join(", "))
        }
        PirValue::Map { entries } => {
            let mut parts = Vec::new();
            for e in entries {
                match &e.key {
                    PirValue::String { v: k } => {
                        parts.push(format!(
                            "{}: {}",
                            serde_json::to_string(k).unwrap_or_else(|_| "\"\"".into()),
                            js_expr(&e.value, depth + 1)?
                        ));
                    }
                    _ => {
                        return Err(ParallaxError::new(
                            ErrorCode::UnsupportedValue,
                            "JS object emit requires string keys",
                        ));
                    }
                }
            }
            format!("{{ {} }}", parts.join(", "))
        }
        PirValue::BigInt { v } => format!("{v}n"),
        PirValue::Unsupported { reason, .. } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit unsupported value: {reason}"),
            ));
        }
        PirValue::Function { name, .. } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit function {:?}", name),
            ));
        }
        PirValue::Ref { id } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit ref {id}"),
            ));
        }
    })
}

fn py_expr(v: &PirValue, depth: usize) -> Result<String, ParallaxError> {
    if depth > 64 {
        return Err(ParallaxError::new(
            ErrorCode::UnsupportedValue,
            "value nesting too deep",
        ));
    }
    Ok(match v {
        PirValue::Null => "None".into(),
        PirValue::Bool { v } => if *v { "True" } else { "False" }.into(),
        PirValue::Int { v } => v.decimal.clone(),
        PirValue::BigInt { v } => v.clone(),
        PirValue::Float { v } => py_float_literal(*v),
        PirValue::String { v } => serde_json::to_string(v).unwrap_or_else(|_| "\"\"".into()),
        PirValue::Bytes { v } => format!("bytes({v:?})"),
        PirValue::List { v } => {
            let parts: Result<Vec<_>, _> = v.iter().map(|x| py_expr(x, depth + 1)).collect();
            format!("[{}]", parts?.join(", "))
        }
        PirValue::Tuple { v } => {
            let parts: Result<Vec<_>, _> = v.iter().map(|x| py_expr(x, depth + 1)).collect();
            let p = parts?;
            if p.len() == 1 {
                format!("({},)", p[0])
            } else {
                format!("({})", p.join(", "))
            }
        }
        PirValue::Set { v } => {
            let parts: Result<Vec<_>, _> = v.iter().map(|x| py_expr(x, depth + 1)).collect();
            format!("{{{}}}", parts?.join(", "))
        }
        PirValue::Map { entries } => {
            let mut parts = Vec::new();
            for e in entries {
                parts.push(format!(
                    "{}: {}",
                    py_expr(&e.key, depth + 1)?,
                    py_expr(&e.value, depth + 1)?
                ));
            }
            format!("{{{}}}", parts.join(", "))
        }
        PirValue::Unsupported { reason, .. } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit unsupported value: {reason}"),
            ));
        }
        PirValue::Function { name, .. } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit function {:?}", name),
            ));
        }
        PirValue::Ref { id } => {
            return Err(ParallaxError::new(
                ErrorCode::UnsupportedValue,
                format!("cannot emit ref {id}"),
            ));
        }
    })
}