nb2pb/
lib.rs

1#![forbid(unsafe_code)]
2
3#[cfg(feature = "pyo3")]
4mod python;
5
6use std::fmt::Write;
7use std::rc::Rc;
8use std::iter;
9use std::sync::LazyLock;
10
11use compact_str::{CompactString, ToCompactString, format_compact};
12use base64::engine::Engine as Base64Engine;
13use regex::Regex;
14
15#[macro_use] extern crate serde_json;
16
17pub use netsblox_ast::Error as ParseError;
18use netsblox_ast::{*, util::*};
19
20#[cfg(test)]
21mod test;
22
23static PY_IDENT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[_a-zA-Z][_a-zA-Z0-9]*$").unwrap());
24fn is_py_ident(sym: &str) -> bool {
25    PY_IDENT_REGEX.is_match(sym)
26}
27#[test]
28fn test_py_ident() {
29    assert!(is_py_ident("fooBar_23"));
30    assert!(!is_py_ident("34hello"));
31    assert!(!is_py_ident("hello world"));
32}
33
34#[derive(Debug)]
35pub enum TranslateError {
36    Parse(Box<Error>),
37    NoRoles,
38
39    UnsupportedExpr(Box<Expr>),
40    UnsupportedStmt(Box<Stmt>),
41    UnsupportedHat(Box<Hat>),
42
43    UnknownImageFormat,
44
45    Upvars,
46    AnyMessage,
47    RingTypeQuery,
48    CommandRing,
49    TellAskClosure,
50}
51impl From<Box<Error>> for TranslateError { fn from(e: Box<Error>) -> Self { Self::Parse(e) } }
52
53fn fmt_comment(comment: Option<&str>) -> CompactString {
54    match comment {
55        Some(v) => format_compact!(" # {}", v.replace('\n', " -- ")),
56        None => "".into(),
57    }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum Type {
62    Unknown, Wrapped,
63}
64
65fn wrap(val: (CompactString, Type)) -> CompactString {
66    match &val.1 {
67        Type::Wrapped => val.0,
68        Type::Unknown => format_compact!("snap.wrap({})", val.0),
69    }
70}
71fn wrap_number(val: (CompactString, Type), coerce: bool) -> CompactString {
72    val.0.chars().next().filter(|&ch| (ch == '"' || ch == '\'') && val.0.len() > 1 && val.0.ends_with(ch))
73        .and_then(|_| val.0[1..val.0.len()-1].parse::<f64>().ok().map(|f| f.to_string().into())).unwrap_or_else(|| format_compact!("{}{}", coerce.then(|| "+").unwrap_or_default(), wrap(val)))
74}
75
76fn translate_var(var: &VariableRef) -> CompactString {
77    match &var.location {
78        VarLocation::Local => var.trans_name.clone(),
79        VarLocation::Field => format_compact!("self.{}", var.trans_name),
80        VarLocation::Global => format_compact!("globals.{}", var.trans_name),
81    }
82}
83
84struct ScriptInfo<'a> {
85    stage_name: &'a str,
86}
87impl<'a> ScriptInfo<'a> {
88    fn new(stage_name: &'a str) -> Self {
89        Self { stage_name }
90    }
91    fn translate_value(&mut self, value: &Value) -> Result<(CompactString, Type), TranslateError> {
92        Ok(match value {
93            Value::String(v) => (format_compact!("'{}'", escape(v)), Type::Unknown),
94            Value::Number(v) => (format_compact!("{}", v), Type::Unknown),
95            Value::Bool(v) => ((if *v { "True" } else { "False" }).into(), Type::Wrapped), // bool is considered wrapped since we can't extend it
96            Value::Constant(c) => match c {
97                Constant::Pi => ("math.pi".into(), Type::Unknown),
98                Constant::E => ("math.e".into(), Type::Unknown),
99            }
100            Value::List(vals, _) => {
101                let mut items = Vec::with_capacity(vals.len());
102                for val in vals {
103                    items.push(self.translate_value(val)?.0);
104                }
105                (format_compact!("[{}]", Punctuated(items.iter(), ", ")), Type::Unknown)
106            }
107            Value::Image(_) => unreachable!(),
108            Value::Audio(_) => unreachable!(),
109            Value::Ref(_) => unreachable!(),
110        })
111    }
112    fn translate_kwargs(&mut self, kwargs: &[(CompactString, Expr)], prefix: &str, wrap_vals: bool) -> Result<CompactString, TranslateError> {
113        let mut ident_args = vec![];
114        let mut non_ident_args = vec![];
115        for arg in kwargs {
116            let val_raw = self.translate_expr(&arg.1)?;
117            let val = if wrap_vals { wrap(val_raw) } else { val_raw.0 };
118            match is_py_ident(&arg.0) {
119                true => ident_args.push(format_compact!("{} = {}", arg.0, val)),
120                false => non_ident_args.push(format_compact!("'{}': {}", escape(&arg.0), val)),
121            }
122        }
123
124        Ok(match (ident_args.is_empty(), non_ident_args.is_empty()) {
125            (false, false) => format_compact!("{}{}, **{{ {} }}", prefix, Punctuated(ident_args.iter(), ", "), Punctuated(non_ident_args.iter(), ", ")),
126            (false, true) => format_compact!("{}{}", prefix, Punctuated(ident_args.iter(), ", ")),
127            (true, false) => format_compact!("{}**{{ {} }}", prefix, Punctuated(non_ident_args.iter(), ", ")),
128            (true, true) => CompactString::default(),
129        })
130    }
131    fn translate_rpc(&mut self, service: &str, rpc: &str, args: &[(CompactString, Expr)]) -> Result<CompactString, TranslateError> {
132        let args_str = self.translate_kwargs(args, ", ", false)?;
133        Ok(format_compact!("nothrow(nb.call)('{}', '{}'{})", escape(service), escape(rpc), args_str))
134    }
135    fn translate_fn_call(&mut self, function: &FnRef, args: &[Expr], upvars: &[VariableRef]) -> Result<CompactString, TranslateError> {
136        if !upvars.is_empty() {
137            return Err(TranslateError::Upvars);
138        }
139
140        let mut trans_args = Vec::with_capacity(args.len());
141        for arg in args.iter() {
142            trans_args.push(wrap(self.translate_expr(arg)?));
143        }
144
145        Ok(match function.location {
146            FnLocation::Global => format_compact!("{}({})", function.trans_name, Punctuated(trans_args.iter(), ", ")),
147            FnLocation::Method => format_compact!("self.{}({})", function.trans_name, Punctuated(trans_args.iter(), ", ")),
148        })
149    }
150    fn translate_closure_call(&mut self, new_entity: Option<&Expr>, closure: &Expr, args: &[Expr]) -> Result<CompactString, TranslateError> {
151        if new_entity.is_some() {
152            return Err(TranslateError::TellAskClosure);
153        }
154
155        let args = args.iter().map(|x| Ok(wrap(self.translate_expr(x)?))).collect::<Result<Vec<_>,TranslateError>>()?;
156        Ok(format_compact!("{}({})", self.translate_expr(closure)?.0, args.join(", "))) // return values are always considered wrapped
157    }
158    fn translate_expr(&mut self, expr: &Expr) -> Result<(CompactString, Type), TranslateError> {
159        Ok(match &expr.kind {
160            ExprKind::Value(v) => self.translate_value(v)?,
161            ExprKind::Variable { var, .. } => (translate_var(var), Type::Wrapped), // all assignments are wrapped, so we can assume vars are wrapped
162
163            ExprKind::Closure { kind: _, params, captures: _, stmts } => match stmts.as_slice() {
164                [Stmt { kind: StmtKind::Return { value }, info: _ }] => {
165                    let mut params_string = CompactString::default();
166                    for param in params {
167                        if params_string.is_empty() {
168                            params_string.push(' ');
169                        } else {
170                            params_string.push_str(", ");
171                        }
172                        params_string.push_str(&param.trans_name);
173                    }
174                    (format_compact!("(lambda{}: {})",params_string, wrap(self.translate_expr(value)?)), Type::Wrapped) // functions are always considered wrapped
175                },
176                _ => return Err(TranslateError::CommandRing),
177            }
178
179            ExprKind::This => ("self".into(), Type::Wrapped), // non-primitives are considered wrapped
180            ExprKind::Entity { trans_name, .. } => (trans_name.clone(), Type::Wrapped), // non-primitives are considered wrapped
181
182            ExprKind::ImageOfEntity { entity } => (format_compact!("{}.get_image()", self.translate_expr(entity)?.0), Type::Wrapped), // non-primitives are considered wrapped
183            ExprKind::ImageOfDrawings => (format_compact!("{}.get_drawings()", self.stage_name), Type::Wrapped), // non-primitives are considered wrapped
184
185            ExprKind::IsTouchingEntity { entity } => (format_compact!("self.is_touching({})", self.translate_expr(entity)?.0), Type::Wrapped), // bool is considered wrapped
186
187            ExprKind::MakeList { values } => {
188                let trans = values.iter().map(|x| Ok(self.translate_expr(x)?.0)).collect::<Result<Vec<_>,TranslateError>>()?;
189                (format_compact!("[{}]", trans.join(", ")), Type::Unknown)
190            }
191            ExprKind::CopyList { list } => (format_compact!("[*{}]", wrap(self.translate_expr(list)?)), Type::Unknown),
192            ExprKind::ListCons { item, list } => (format_compact!("[{}, *{}]", self.translate_expr(item)?.0, wrap(self.translate_expr(list)?)), Type::Unknown),
193            ExprKind::ListCdr { value } => (format_compact!("{}[1:]", wrap(self.translate_expr(value)?)), Type::Wrapped),
194
195            ExprKind::ListGet { list, index } => (format_compact!("{}[{} - snap.wrap(1)]", wrap(self.translate_expr(list)?), wrap(self.translate_expr(index)?)), Type::Wrapped),
196            ExprKind::ListGetRandom { list } => (format_compact!("{}.rand", wrap(self.translate_expr(list)?)), Type::Wrapped),
197            ExprKind::ListGetLast { list } => (format_compact!("{}.last", wrap(self.translate_expr(list)?)), Type::Wrapped),
198
199            ExprKind::ListFind { list, value } => (format_compact!("({}.index({}) + snap.wrap(1))", wrap(self.translate_expr(list)?), self.translate_expr(value)?.0), Type::Wrapped),
200            ExprKind::ListContains { list, value } => (format_compact!("({} in {})", wrap(self.translate_expr(value)?), wrap(self.translate_expr(list)?)), Type::Wrapped),
201
202            ExprKind::ListLen { value } | ExprKind::StrLen { value } => (format_compact!("len({})", self.translate_expr(value)?.0), Type::Unknown), // builtin __len__ can't be overloaded to return wrapped
203            ExprKind::ListIsEmpty { value } => (format_compact!("(len({}) == 0)", self.translate_expr(value)?.0), Type::Wrapped),
204
205            ExprKind::ListRank { value } => (format_compact!("len({}.shape)", wrap(self.translate_expr(value)?)), Type::Unknown), // builtin __len__ can't be overloaded to return wrapped
206            ExprKind::ListDims { value } => (format_compact!("{}.shape", wrap(self.translate_expr(value)?)), Type::Wrapped),
207            ExprKind::ListFlatten { value } => (format_compact!("{}.flat", wrap(self.translate_expr(value)?)), Type::Wrapped),
208            ExprKind::ListColumns { value } => (format_compact!("{}.T", wrap(self.translate_expr(value)?)), Type::Wrapped),
209            ExprKind::ListRev { value } => (format_compact!("{}[::-1]", wrap(self.translate_expr(value)?)), Type::Wrapped),
210
211            ExprKind::ListLines { value } => (format_compact!("'\\n'.join(str(x) for x in {})", wrap(self.translate_expr(value)?)), Type::Wrapped),
212            ExprKind::ListCsv { value } => (format_compact!("{}.csv", wrap(self.translate_expr(value)?)), Type::Wrapped),
213            ExprKind::ListJson { value } => (format_compact!("{}.json", wrap(self.translate_expr(value)?)), Type::Wrapped),
214
215            ExprKind::ListReshape { value, dims } => (format_compact!("{}.reshaped({})", wrap(self.translate_expr(value)?), self.translate_expr(dims)?.0), Type::Wrapped),
216
217            ExprKind::Map { f, list } => (format_compact!("[{}(x) for x in {}]", self.translate_expr(f)?.0, wrap(self.translate_expr(list)?)), Type::Unknown),
218            ExprKind::Keep { f, list } => (format_compact!("[x for x in {} if {}(x)]", wrap(self.translate_expr(list)?), self.translate_expr(f)?.0), Type::Unknown),
219            ExprKind::FindFirst { f, list } => (format_compact!("{}.index_where({})", wrap(self.translate_expr(list)?), self.translate_expr(f)?.0), Type::Wrapped),
220            ExprKind::Combine { f, list } => (format_compact!("{}.fold({})", wrap(self.translate_expr(list)?), self.translate_expr(f)?.0), Type::Wrapped),
221
222            ExprKind::StrGet { string, index } => (format_compact!("{}[{} - snap.wrap(1)]", wrap(self.translate_expr(string)?), wrap(self.translate_expr(index)?)), Type::Wrapped),
223            ExprKind::StrGetLast { string } => (format_compact!("{}.last", wrap(self.translate_expr(string)?)), Type::Wrapped),
224            ExprKind::StrGetRandom { string } => (format_compact!("{}.rand", wrap(self.translate_expr(string)?)), Type::Wrapped),
225
226            ExprKind::Neg { value } => (format_compact!("-{}", wrap(self.translate_expr(value)?)), Type::Wrapped),
227            ExprKind::Not { value } => (format_compact!("snap.lnot({})", self.translate_expr(value)?.0), Type::Wrapped),
228            ExprKind::Abs { value } => (format_compact!("abs({})", wrap(self.translate_expr(value)?)), Type::Wrapped),
229            ExprKind::Sign { value } => (format_compact!("snap.sign({})", self.translate_expr(value)?.0), Type::Wrapped),
230
231            ExprKind::Atan2 { y, x } => (format_compact!("snap.atan2({}, {})", self.translate_expr(y)?.0, self.translate_expr(x)?.0), Type::Wrapped),
232
233            ExprKind::ListCombinations { sources } => match &sources.kind {
234                ExprKind::Value(Value::List(values, _)) => (format_compact!("snap.combinations({})", values.iter().map(|x| Ok(self.translate_value(x)?.0)).collect::<Result<Vec<_>,TranslateError>>()?.join(", ")), Type::Wrapped),
235                ExprKind::MakeList { values } => (format_compact!("snap.combinations({})", values.iter().map(|x| Ok(self.translate_expr(x)?.0)).collect::<Result<Vec<_>,TranslateError>>()?.join(", ")), Type::Wrapped),
236                _ => (format_compact!("snap.combinations(*{})", wrap(self.translate_expr(sources)?)), Type::Wrapped),
237            }
238            ExprKind::Add { values } => match &values.kind {
239                ExprKind::Value(Value::List(values, _)) => match values.as_slice() {
240                    [] => ("0".into(), Type::Unknown),
241                    _ => (format_compact!("({})", values.iter().map(|x| Ok(wrap(self.translate_value(x)?))).collect::<Result<Vec<_>,TranslateError>>()?.join(" + ")), Type::Wrapped),
242                }
243                ExprKind::MakeList { values } => match values.as_slice() {
244                    [] => ("0".into(), Type::Unknown),
245                    _ => (format_compact!("({})", values.iter().map(|x| Ok(wrap(self.translate_expr(x)?))).collect::<Result<Vec<_>,TranslateError>>()?.join(" + ")), Type::Wrapped),
246                }
247                _ => (format_compact!("sum({})", wrap(self.translate_expr(values)?)), Type::Unknown),
248            }
249            ExprKind::Mul { values } => match &values.kind {
250                ExprKind::Value(Value::List(values, _)) => match values.as_slice() {
251                    [] => ("1".into(), Type::Unknown),
252                    _ => (format_compact!("({})", values.iter().map(|x| Ok(wrap(self.translate_value(x)?))).collect::<Result<Vec<_>,TranslateError>>()?.join(" * ")), Type::Wrapped),
253                }
254                ExprKind::MakeList { values } => match values.as_slice() {
255                    [] => ("1".into(), Type::Unknown),
256                    _ => (format_compact!("({})", values.iter().map(|x| Ok(wrap(self.translate_expr(x)?))).collect::<Result<Vec<_>,TranslateError>>()?.join(" * ")), Type::Wrapped),
257                }
258                _ => (format_compact!("snap.prod({})", self.translate_expr(values)?.0), Type::Wrapped),
259            }
260
261            ExprKind::Min { values } => (format_compact!("min({})", wrap(self.translate_expr(values)?)), Type::Wrapped),
262            ExprKind::Max { values } => (format_compact!("max({})", wrap(self.translate_expr(values)?)), Type::Wrapped),
263
264            ExprKind::Sub { left, right } => (format_compact!("({} - {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
265            ExprKind::Div { left, right } => (format_compact!("({} / {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
266            ExprKind::Mod { left, right } => (format_compact!("({} % {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
267
268            ExprKind::Pow { base, power } => (format_compact!("({} ** {})", wrap(self.translate_expr(base)?), wrap(self.translate_expr(power)?)), Type::Wrapped),
269            ExprKind::Log { value, base } => (format_compact!("snap.log({}, {})", self.translate_expr(value)?.0, self.translate_expr(base)?.0), Type::Wrapped),
270
271            ExprKind::Sqrt { value } => (format_compact!("snap.sqrt({})", self.translate_expr(value)?.0), Type::Wrapped),
272
273            ExprKind::Round { value } => (format_compact!("round({})", wrap(self.translate_expr(value)?)), Type::Wrapped),
274            ExprKind::Floor { value } => (format_compact!("math.floor({})", wrap(self.translate_expr(value)?)), Type::Wrapped),
275            ExprKind::Ceil { value } => (format_compact!("math.ceil({})", wrap(self.translate_expr(value)?)), Type::Wrapped),
276
277            ExprKind::Sin { value } => (format_compact!("snap.sin({})", self.translate_expr(value)?.0), Type::Wrapped),
278            ExprKind::Cos { value } => (format_compact!("snap.cos({})", self.translate_expr(value)?.0), Type::Wrapped),
279            ExprKind::Tan { value } => (format_compact!("snap.tan({})", self.translate_expr(value)?.0), Type::Wrapped),
280
281            ExprKind::Asin { value } => (format_compact!("snap.asin({})", self.translate_expr(value)?.0), Type::Wrapped),
282            ExprKind::Acos { value } => (format_compact!("snap.acos({})", self.translate_expr(value)?.0), Type::Wrapped),
283            ExprKind::Atan { value } => (format_compact!("snap.atan({})", self.translate_expr(value)?.0), Type::Wrapped),
284
285            ExprKind::And { left, right } => (format_compact!("({} and {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
286            ExprKind::Or { left, right } => (format_compact!("({} or {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
287            ExprKind::Conditional { condition, then, otherwise } => {
288                let (then, otherwise) = (self.translate_expr(then)?, self.translate_expr(otherwise)?);
289                (format_compact!("({} if {} else {})", then.0, wrap(self.translate_expr(condition)?), otherwise.0), if then.1 == otherwise.1 { then.1 } else { Type::Unknown })
290            }
291
292            ExprKind::Identical { left, right } => (format_compact!("snap.identical({}, {})", self.translate_expr(left)?.0, self.translate_expr(right)?.0), Type::Wrapped), // bool is considered wrapped
293
294            ExprKind::Less { left, right } => (format_compact!("({} < {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
295            ExprKind::LessEq { left, right } => (format_compact!("({} <= {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
296            ExprKind::Eq { left, right } => (format_compact!("({} == {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
297            ExprKind::Neq { left, right } => (format_compact!("({} != {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
298            ExprKind::Greater { left, right } => (format_compact!("({} > {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
299            ExprKind::GreaterEq { left, right } => (format_compact!("({} >= {})", wrap(self.translate_expr(left)?), wrap(self.translate_expr(right)?)), Type::Wrapped),
300
301            ExprKind::Random { a, b } => (format_compact!("snap.rand({}, {})", self.translate_expr(a)?.0, self.translate_expr(b)?.0), Type::Wrapped), // python impl returns wrapped
302            ExprKind::Range { start, stop } => (format_compact!("snap.srange({}, {})", self.translate_expr(start)?.0, self.translate_expr(stop)?.0), Type::Wrapped), // python impl returns wrapped
303
304            ExprKind::CostumeNumber => (format_compact!("(self.costumes.index(self.costume, -1) + 1)"), Type::Unknown),
305
306            ExprKind::TextSplit { text, mode } => match mode {
307                TextSplitMode::Custom(x) => (format_compact!("snap.split({}, {})", self.translate_expr(text)?.0, self.translate_expr(x)?.0), Type::Wrapped),
308                TextSplitMode::LF => (format_compact!("snap.split({}, '\\n')", self.translate_expr(text)?.0), Type::Wrapped),
309                TextSplitMode::CR => (format_compact!("snap.split({}, '\\r')", self.translate_expr(text)?.0), Type::Wrapped),
310                TextSplitMode::Tab => (format_compact!("snap.split({}, '\\t')", self.translate_expr(text)?.0), Type::Wrapped),
311                TextSplitMode::Letter => (format_compact!("snap.split({}, '')", self.translate_expr(text)?.0), Type::Wrapped),
312                TextSplitMode::Word => (format_compact!("snap.split_words({})", self.translate_expr(text)?.0), Type::Wrapped),
313                TextSplitMode::Csv => (format_compact!("snap.split_csv({})", self.translate_expr(text)?.0), Type::Wrapped),
314                TextSplitMode::Json => (format_compact!("snap.split_json({})", self.translate_expr(text)?.0), Type::Wrapped),
315            }
316
317            ExprKind::TypeQuery { value, ty } => match ty {
318                ValueType::Bool => (format_compact!("snap.is_bool({})", self.translate_expr(value)?.0), Type::Wrapped),
319                ValueType::Text => (format_compact!("snap.is_text({})", self.translate_expr(value)?.0), Type::Wrapped),
320                ValueType::Number => (format_compact!("snap.is_number({})", self.translate_expr(value)?.0), Type::Wrapped),
321                ValueType::List => (format_compact!("snap.is_list({})", self.translate_expr(value)?.0), Type::Wrapped),
322                ValueType::Sprite => (format_compact!("snap.is_sprite({})", self.translate_expr(value)?.0), Type::Wrapped),
323                ValueType::Costume => (format_compact!("snap.is_costume({})", self.translate_expr(value)?.0), Type::Wrapped),
324                ValueType::Sound => (format_compact!("snap.is_sound({})", self.translate_expr(value)?.0), Type::Wrapped),
325                ValueType::Command | ValueType::Reporter | ValueType::Predicate => return Err(TranslateError::RingTypeQuery),
326            }
327
328            ExprKind::ListCat { lists } => match &lists.kind {
329                ExprKind::Value(Value::List(values, _)) => (format_compact!("[{}]", values.iter().map(|x| Ok(format_compact!("*{}", wrap(self.translate_value(x)?)))).collect::<Result<Vec<_>,TranslateError>>()?.join(", ")), Type::Unknown),
330                _ => (format_compact!("[y for x in {} for y in x]", wrap(self.translate_expr(lists)?)), Type::Unknown),
331            }
332            ExprKind::StrCat { values } => {
333                fn as_str_lit(mut s: &str) -> Option<(&str, bool)> {
334                    let mut fmt_str = false;
335                    if s.starts_with('f') { s = &s[1..]; fmt_str = true; }
336                    s.chars().next().filter(|&c| (c == '"' || c == '\'') && s.len() >= 2 && s.ends_with(c)).map(|_| (&s[1..s.len() - 1], fmt_str))
337                }
338                fn escape_braces(s: &str) -> CompactString {
339                    let mut res = CompactString::with_capacity(s.len());
340                    for c in s.chars() {
341                        res.push(c);
342                        if c == '{' || c == '}' { res.push(c); }
343                    }
344                    res
345                }
346                fn handle_segments(segments: Vec<(CompactString, Type)>) -> (CompactString, Type) {
347                    let mut fmt_str = false;
348                    let mut res = CompactString::default();
349                    for segment in segments.iter() {
350                        match as_str_lit(&segment.0) {
351                            Some(lit) => {
352                                if !fmt_str && lit.1 { res = escape_braces(&res) }
353                                fmt_str |= lit.1;
354                                if fmt_str && !lit.1 { res.push_str(&escape_braces(&lit.0)) } else { res.push_str(&lit.0) }
355                            }
356                            None => {
357                                if !fmt_str { res = escape_braces(&res) }
358                                fmt_str = true;
359                                write!(res, "{{{}}}", segment.0).unwrap();
360                            }
361                        }
362                    }
363                    let quote_char = match (res.contains('\''), res.contains('"')) {
364                        (false, _) => '\'',
365                        (true, false) => '"',
366                        (true, true) => return (format_compact!("({})", segments.into_iter().map(|x| format_compact!("str({})", wrap(x))).collect::<Vec<_>>().join(" + ")), Type::Unknown),
367                    };
368                    (format_compact!("{}{quote_char}{res}{quote_char}", if fmt_str { "f" } else { "" }), Type::Unknown)
369                }
370                match &values.kind {
371                    ExprKind::Value(Value::List(values, _)) => handle_segments(values.iter().map(|x| self.translate_value(x)).collect::<Result<Vec<_>,_>>()?),
372                    ExprKind::MakeList { values } => handle_segments(values.iter().map(|x| self.translate_expr(x)).collect::<Result<Vec<_>,_>>()?),
373                    _ => (format_compact!("''.join(str(x) for x in {})", wrap(self.translate_expr(values)?)), Type::Unknown),
374                }
375            }
376
377            ExprKind::UnicodeToChar { value } => (format_compact!("snap.get_chr({})", self.translate_expr(value)?.0), Type::Wrapped),
378            ExprKind::CharToUnicode { value } => (format_compact!("snap.get_ord({})", self.translate_expr(value)?.0), Type::Wrapped),
379
380            ExprKind::CallRpc { service, host: _, rpc, args } => (self.translate_rpc(service, rpc, args)?, Type::Unknown),
381            ExprKind::CallFn { function, args, upvars } => (self.translate_fn_call(function, args, upvars)?, Type::Wrapped),
382            ExprKind::CallClosure { new_entity, closure, args } => (self.translate_closure_call(new_entity.as_deref(), closure, args)?, Type::Wrapped),
383
384            ExprKind::XPos => ("self.x_pos".into(), Type::Unknown),
385            ExprKind::YPos => ("self.y_pos".into(), Type::Unknown),
386            ExprKind::Heading => ("self.heading".into(), Type::Unknown),
387
388            ExprKind::Answer => (format_compact!("{}.last_answer", self.stage_name), Type::Wrapped),
389
390            ExprKind::MouseX => (format_compact!("{}.mouse_pos[0]", self.stage_name), Type::Unknown),
391            ExprKind::MouseY => (format_compact!("{}.mouse_pos[1]", self.stage_name), Type::Unknown),
392
393            ExprKind::StageWidth => (format_compact!("{}.width", self.stage_name), Type::Unknown),
394            ExprKind::StageHeight => (format_compact!("{}.height", self.stage_name), Type::Unknown),
395
396            ExprKind::Latitude => (format_compact!("{}.gps_location[0]", self.stage_name), Type::Unknown),
397            ExprKind::Longitude => (format_compact!("{}.gps_location[1]", self.stage_name), Type::Unknown),
398
399            ExprKind::KeyDown { key } => (format_compact!("{stage_name}.is_key_down({key})", key = self.translate_expr(key)?.0, stage_name = self.stage_name), Type::Wrapped), // bool is considered wrapped
400
401            ExprKind::PenDown => ("self.drawing".into(), Type::Wrapped), // bool is considered wrapped
402            ExprKind::Size => ("(self.scale * 100)".into(), Type::Wrapped),
403            ExprKind::IsVisible => ("self.visible".into(), Type::Wrapped), // bool is considered wrapped
404
405            ExprKind::RpcError => ("(get_error() or '')".into(), Type::Unknown),
406
407            ExprKind::Clone { target } => (format_compact!("{}.clone()", self.translate_expr(target)?.0), Type::Wrapped), // sprites are considered wrapped
408
409            ExprKind::Timer => (format_compact!("{}.timer", self.stage_name), Type::Unknown),
410
411            ExprKind::SoundDuration { sound } => (format_compact!("self.sounds.lookup({}).duration", self.translate_expr(sound)?.0), Type::Wrapped), // sounds are considered wrapped
412
413            _ => return Err(TranslateError::UnsupportedExpr(Box::new(expr.clone()))),
414        })
415    }
416    fn translate_stmts(&mut self, stmts: &[Stmt]) -> Result<CompactString, TranslateError> {
417        if stmts.is_empty() { return Ok("pass".into()) }
418
419        let mut lines = Vec::with_capacity(stmts.len());
420        for stmt in stmts {
421            match &stmt.kind {
422                StmtKind::DeclareLocals { vars } => lines.extend(vars.iter().map(|x| format_compact!("{} = snap.wrap(0)", x.trans_name))),
423                StmtKind::Assign { var, value } => lines.push(format_compact!("{} = {}{}", translate_var(var), wrap(self.translate_expr(value)?), fmt_comment(stmt.info.comment.as_deref()))),
424                StmtKind::AddAssign { var, value } => lines.push(format_compact!("{} += {}{}", translate_var(var), wrap(self.translate_expr(value)?), fmt_comment(stmt.info.comment.as_deref()))),
425                StmtKind::ListAssign { list, index, value } => lines.push(format_compact!("{}[{} - snap.wrap(1)] = {}{}", wrap(self.translate_expr(list)?), wrap(self.translate_expr(index)?), self.translate_expr(value)?.0, fmt_comment(stmt.info.comment.as_deref()))),
426                StmtKind::ListAssignLast { list, value } => lines.push(format_compact!("{}.last = {}{}", wrap(self.translate_expr(list)?), self.translate_expr(value)?.0, fmt_comment(stmt.info.comment.as_deref()))),
427                StmtKind::ListAssignRandom { list, value } => lines.push(format_compact!("{}.rand = {}{}", wrap(self.translate_expr(list)?), self.translate_expr(value)?.0, fmt_comment(stmt.info.comment.as_deref()))),
428                StmtKind::ListInsert { list, index, value } => lines.push(format_compact!("{}.insert({}, {}){}", wrap(self.translate_expr(list)?), self.translate_expr(index)?.0, self.translate_expr(value)?.0, fmt_comment(stmt.info.comment.as_deref()))),
429                StmtKind::ListInsertLast { list, value } => lines.push(format_compact!("{}.append({}){}", wrap(self.translate_expr(list)?), wrap(self.translate_expr(value)?), fmt_comment(stmt.info.comment.as_deref()))),
430                StmtKind::ListInsertRandom { list, value } => lines.push(format_compact!("{}.insert_rand({}){}", wrap(self.translate_expr(list)?), self.translate_expr(value)?.0, fmt_comment(stmt.info.comment.as_deref()))),
431                StmtKind::ListRemoveLast { list } => lines.push(format_compact!("{}.pop(){}", wrap(self.translate_expr(list)?), fmt_comment(stmt.info.comment.as_deref()))),
432                StmtKind::ListRemove { list, index } => lines.push(format_compact!("del {}[{} - snap.wrap(1)]{}", wrap(self.translate_expr(list)?), wrap(self.translate_expr(index)?), fmt_comment(stmt.info.comment.as_deref()))),
433                StmtKind::ListRemoveAll { list } => lines.push(format_compact!("{}.clear(){}", wrap(self.translate_expr(list)?), fmt_comment(stmt.info.comment.as_deref()))),
434                StmtKind::Throw { error } => lines.push(format_compact!("raise RuntimeError(str({})){}", wrap(self.translate_expr(error)?), fmt_comment(stmt.info.comment.as_deref()))),
435                StmtKind::Warp { stmts } => {
436                    let code = self.translate_stmts(stmts)?;
437                    lines.push(format_compact!("with NoYield():{}\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
438                }
439                StmtKind::If { condition, then } => {
440                    let condition = wrap(self.translate_expr(condition)?);
441                    let then = self.translate_stmts(then)?;
442                    lines.push(format_compact!("if {condition}:{}\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&then)));
443                }
444                StmtKind::IfElse { condition, then, otherwise } => {
445                    let condition = wrap(self.translate_expr(condition)?);
446                    let then_code = self.translate_stmts(then)?;
447                    let otherwise_code = self.translate_stmts(otherwise)?;
448
449                    match otherwise.as_slice() {
450                        [Stmt { kind: StmtKind::If { .. } | StmtKind::IfElse { .. }, .. }] => {
451                            lines.push(format_compact!("if {condition}:{}\n{}\nel{otherwise_code}", fmt_comment(stmt.info.comment.as_deref()), indent(&then_code)));
452                        }
453                        _ => {
454                            lines.push(format_compact!("if {condition}:{}\n{}\nelse:\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&then_code), indent(&otherwise_code)));
455                        }
456                    }
457                }
458                StmtKind::TryCatch { code, var, handler } => {
459                    let code = self.translate_stmts(code)?;
460                    let handler = self.translate_stmts(handler)?;
461                    lines.push(format_compact!("try:{}\n{}\nexcept Exception as {}:\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&code), var.trans_name, indent(&handler)));
462                }
463                StmtKind::InfLoop { stmts } => {
464                    let code = self.translate_stmts(stmts)?;
465                    lines.push(format_compact!("while True:{}\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
466                }
467                StmtKind::ForLoop { var, start, stop, stmts } => {
468                    let start = wrap_number(self.translate_expr(start)?, false);
469                    let stop = wrap_number(self.translate_expr(stop)?, false);
470                    let code = self.translate_stmts(stmts)?;
471                    lines.push(format_compact!("for {} in snap.sxrange({start}, {stop}):{}\n{}", var.trans_name, fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
472                }
473                StmtKind::ForeachLoop { var, items, stmts } => {
474                    let items = wrap(self.translate_expr(items)?);
475                    let code = self.translate_stmts(stmts)?;
476                    lines.push(format_compact!("for {} in {items}:{}\n{}", var.trans_name, fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
477                }
478                StmtKind::Repeat { times, stmts } => {
479                    let times = wrap_number(self.translate_expr(times)?, true);
480                    let code = self.translate_stmts(stmts)?;
481                    lines.push(format_compact!("for _ in range({times}):{}\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
482                }
483                StmtKind::UntilLoop { condition, stmts } => {
484                    let condition = wrap(self.translate_expr(condition)?);
485                    let code = self.translate_stmts(stmts)?;
486                    lines.push(format_compact!("while not {condition}:{}\n{}", fmt_comment(stmt.info.comment.as_deref()), indent(&code)));
487                }
488                StmtKind::SetCostume { costume } => {
489                    let costume = self.translate_expr(costume)?.0;
490                    lines.push(format_compact!("self.costume = {costume}{}", fmt_comment(stmt.info.comment.as_deref())));
491                }
492                StmtKind::NextCostume => lines.push(format_compact!("self.costume = (self.costumes.index(self.costume, -1) + 1) % len(self.costumes)")),
493                StmtKind::PlaySound { sound, blocking } => {
494                    let blocking_suffix = if *blocking { ", wait = True" } else { "" };
495                    let sound = self.translate_expr(sound)?.0;
496                    lines.push(format_compact!("self.play_sound({sound}{blocking_suffix}){}", fmt_comment(stmt.info.comment.as_deref())));
497                }
498                StmtKind::StopSounds => lines.push(format_compact!("{}.stop_sounds()", self.stage_name)),
499
500                StmtKind::SetX { value } => lines.push(format_compact!("self.x_pos = {}{}", wrap_number(self.translate_expr(value)?, false), fmt_comment(stmt.info.comment.as_deref()))),
501                StmtKind::SetY { value } => lines.push(format_compact!("self.y_pos = {}{}", wrap_number(self.translate_expr(value)?, false), fmt_comment(stmt.info.comment.as_deref()))),
502
503                StmtKind::ChangeX { delta } => lines.push(format_compact!("self.x_pos += {}{}", wrap_number(self.translate_expr(delta)?, false), fmt_comment(stmt.info.comment.as_deref()))),
504                StmtKind::ChangeY { delta } => lines.push(format_compact!("self.y_pos += {}{}", wrap_number(self.translate_expr(delta)?, false), fmt_comment(stmt.info.comment.as_deref()))),
505
506                StmtKind::Goto { target } => match &target.kind {
507                    ExprKind::Value(Value::List(values, _)) if values.len() == 2 => lines.push(format_compact!("self.pos = ({}, {}){}", self.translate_value(&values[0])?.0, self.translate_value(&values[1])?.0, fmt_comment(stmt.info.comment.as_deref()))),
508                    _ => lines.push(format_compact!("self.pos = {}{}", self.translate_expr(target)?.0, fmt_comment(stmt.info.comment.as_deref()))),
509                }
510                StmtKind::GotoXY { x, y } => lines.push(format_compact!("self.pos = ({}, {}){}", wrap_number(self.translate_expr(x)?, false), wrap_number(self.translate_expr(y)?, false), fmt_comment(stmt.info.comment.as_deref()))),
511
512                StmtKind::SendLocalMessage { target, msg_type, wait } => {
513                    if *wait { unimplemented!() }
514                    if target.is_some() { unimplemented!() }
515
516                    match &msg_type.kind {
517                        ExprKind::Value(Value::String(msg_type)) => lines.push(format_compact!("nb.send_message('local::{}'){}", escape(msg_type), fmt_comment(stmt.info.comment.as_deref()))),
518                        _  => lines.push(format_compact!("nb.send_message('local::' + str({})){}", self.translate_expr(msg_type)?.0, fmt_comment(stmt.info.comment.as_deref()))),
519                    }
520                }
521                StmtKind::SendNetworkMessage { target, msg_type, values } => {
522                    let kwargs_str = self.translate_kwargs(values, ", ", false)?;
523                    lines.push(format_compact!("nb.send_message('{}', {}{}){}", escape(msg_type), self.translate_expr(target)?.0, kwargs_str, fmt_comment(stmt.info.comment.as_deref())));
524                }
525                StmtKind::Say { content, duration } | StmtKind::Think { content, duration } => match duration {
526                    Some(duration) => lines.push(format_compact!("self.say({}, duration = {}){}", self.translate_expr(content)?.0, self.translate_expr(duration)?.0, fmt_comment(stmt.info.comment.as_deref()))),
527                    None => lines.push(format_compact!("self.say({}){}", self.translate_expr(content)?.0, fmt_comment(stmt.info.comment.as_deref()))),
528                }
529                StmtKind::CallRpc { service, host: _, rpc, args } => lines.push(format_compact!("{}{}", self.translate_rpc(service, rpc, args)?, fmt_comment(stmt.info.comment.as_deref()))),
530                StmtKind::CallFn { function, args, upvars } => lines.push(format_compact!("{}{}", self.translate_fn_call(function, args, upvars)?, fmt_comment(stmt.info.comment.as_deref()))),
531                StmtKind::CallClosure { new_entity, closure, args } => lines.push(format_compact!("{}{}", self.translate_closure_call(new_entity.as_deref(), closure, args)?, fmt_comment(stmt.info.comment.as_deref()))),
532                StmtKind::ChangePenSize { delta } => lines.push(format_compact!("self.pen_size += {}{}", wrap_number(self.translate_expr(delta)?, false), fmt_comment(stmt.info.comment.as_deref()))),
533                StmtKind::SetPenSize { value } => lines.push(format_compact!("self.pen_size = {}{}", wrap_number(self.translate_expr(value)?, false), fmt_comment(stmt.info.comment.as_deref()))),
534                StmtKind::SetVisible { value } => lines.push(format_compact!("self.visible = {}{}", if *value { "True" } else { "False" }, fmt_comment(stmt.info.comment.as_deref()))),
535                StmtKind::WaitUntil { condition } => lines.push(format_compact!("while not {}:{}\n    time.sleep(0.05)", wrap(self.translate_expr(condition)?), fmt_comment(stmt.info.comment.as_deref()))),
536                StmtKind::BounceOffEdge => lines.push(format_compact!("self.keep_on_stage(bounce = True){}", fmt_comment(stmt.info.comment.as_deref()))),
537                StmtKind::Sleep { seconds } => lines.push(format_compact!("time.sleep({}){}", wrap_number(self.translate_expr(seconds)?, true), fmt_comment(stmt.info.comment.as_deref()))),
538                StmtKind::Forward { distance } => lines.push(format_compact!("self.forward({}){}", wrap_number(self.translate_expr(distance)?, false), fmt_comment(stmt.info.comment.as_deref()))),
539                StmtKind::TurnRight { angle } => lines.push(format_compact!("self.turn_right({}){}", wrap_number(self.translate_expr(angle)?, false), fmt_comment(stmt.info.comment.as_deref()))),
540                StmtKind::TurnLeft { angle } => lines.push(format_compact!("self.turn_left({}){}", wrap_number(self.translate_expr(angle)?, false), fmt_comment(stmt.info.comment.as_deref()))),
541                StmtKind::SetHeading { value } => lines.push(format_compact!("self.heading = {}{}", wrap_number(self.translate_expr(value)?, false), fmt_comment(stmt.info.comment.as_deref()))),
542                StmtKind::Return { value } => lines.push(format_compact!("return {}{}", wrap(self.translate_expr(value)?), fmt_comment(stmt.info.comment.as_deref()))),
543                StmtKind::Stamp => lines.push(format_compact!("self.stamp(){}", fmt_comment(stmt.info.comment.as_deref()))),
544                StmtKind::Write { content, font_size } => lines.push(format_compact!("self.write({}, size = {}){}", wrap(self.translate_expr(content)?), wrap(self.translate_expr(font_size)?), fmt_comment(stmt.info.comment.as_deref()))),
545                StmtKind::SetPenDown { value } => lines.push(format_compact!("self.drawing = {}{}", if *value { "True" } else { "False" }, fmt_comment(stmt.info.comment.as_deref()))),
546                StmtKind::PenClear => lines.push(format_compact!("{}.clear_drawings(){}", self.stage_name, fmt_comment(stmt.info.comment.as_deref()))),
547                StmtKind::SetPenColor { color } => lines.push(format_compact!("self.pen_color = '#{:02x}{:02x}{:02x}'{}", color.0, color.1, color.2, fmt_comment(stmt.info.comment.as_deref()))),
548                StmtKind::ChangeSize { delta } => lines.push(format_compact!("self.scale += {} / 100{}", wrap_number(self.translate_expr(delta)?, false), fmt_comment(stmt.info.comment.as_deref()))),
549                StmtKind::SetSize { value } => lines.push(format_compact!("self.scale = {} / 100{}", wrap_number(self.translate_expr(value)?, false), fmt_comment(stmt.info.comment.as_deref()))),
550                StmtKind::Clone { target } => lines.push(format_compact!("{}.clone(){}", self.translate_expr(target)?.0, fmt_comment(stmt.info.comment.as_deref()))),
551                StmtKind::Ask { prompt } => lines.push(format_compact!("{stage_name}.last_answer = snap.wrap(input({prompt})){comment}", prompt = self.translate_expr(prompt)?.0, stage_name = self.stage_name, comment = fmt_comment(stmt.info.comment.as_deref()))),
552                StmtKind::ResetTimer => lines.push(format_compact!("{}.timer = 0", self.stage_name)),
553                _ => return Err(TranslateError::UnsupportedStmt(Box::new(stmt.clone()))),
554            }
555        }
556
557        Ok(lines.join("\n").into())
558    }
559}
560
561struct RoleInfo {
562    name: CompactString,
563    sprites: Vec<SpriteInfo>,
564}
565impl RoleInfo {
566    fn new(name: CompactString) -> Self {
567        Self { name, sprites: vec![] }
568    }
569}
570
571struct SpriteInfo {
572    name: CompactString,
573    scripts: Vec<CompactString>,
574    fields: Vec<(CompactString, CompactString)>,
575    funcs: Vec<Function>,
576    costumes: Vec<(CompactString, Rc<(Vec<u8>, Option<(f64, f64)>, CompactString)>)>,
577    sounds: Vec<(CompactString, Rc<(Vec<u8>, CompactString)>)>,
578
579    active_costume: Option<usize>,
580    visible: bool,
581    color: (u8, u8, u8, u8),
582    pos: (f64, f64),
583    heading: f64,
584    scale: f64,
585}
586impl SpriteInfo {
587    fn new(src: &Entity) -> Self {
588        Self {
589            name: src.trans_name.clone(),
590            scripts: vec![],
591            fields: vec![],
592            funcs: src.funcs.clone(),
593            costumes: vec![],
594            sounds: vec![],
595
596            active_costume: src.active_costume,
597            visible: src.visible,
598            color: src.color,
599            pos: src.pos,
600            heading: src.heading,
601            scale: src.scale,
602        }
603    }
604    fn translate_hat(&mut self, hat: &Hat, stage_name: &str) -> Result<CompactString, TranslateError> {
605        Ok(match &hat.kind {
606            HatKind::OnFlag => format_compact!("@onstart(){}\ndef my_onstart_{}(self):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
607            HatKind::OnClone => format_compact!("@onstart('clone'){}\ndef my_onstart_{}(self):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
608            HatKind::OnKey { key } => format_compact!("@onkey('{}'){}\ndef my_onkey_{}(self):\n", key, fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
609            HatKind::MouseDown => format_compact!("@onmouse('down'){}\ndef my_onmouse_{}(self, x, y):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
610            HatKind::MouseUp => format_compact!("@onmouse('up'){}\ndef my_onmouse_{}(self, x, y):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
611            HatKind::ScrollDown => format_compact!("@onmouse('scroll-down'){}\ndef my_onmouse_{}(self, x, y):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
612            HatKind::ScrollUp => format_compact!("@onmouse('scroll-up'){}\ndef my_onmouse_{}(self, x, y):\n", fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
613            HatKind::When { condition } => {
614                format_compact!(r#"@onstart(){comment}
615def my_onstart{idx}(self):
616    while True:
617        try:
618            time.sleep(0.05)
619            if {condition}:
620                self.my_oncondition{idx}()
621        except Exception as e:
622            import traceback, sys
623            print(traceback.format_exc(), file = sys.stderr)
624def my_oncondition{idx}(self):
625"#,
626                comment = fmt_comment(hat.info.comment.as_deref()),
627                idx = self.scripts.len() + 1,
628                condition = wrap(ScriptInfo::new(stage_name).translate_expr(condition)?))
629            }
630            HatKind::LocalMessage { msg_type } => match msg_type {
631                Some(msg_type) => format_compact!("@nb.on_message('local::{}'){}\ndef my_on_message_{}(self):\n", escape(msg_type), fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1),
632                None => return Err(TranslateError::AnyMessage),
633            }
634            HatKind::NetworkMessage { msg_type, fields } => {
635                let mut res = format_compact!("@nb.on_message('{}'){}\ndef my_on_message_{}(self, **kwargs):\n", escape(msg_type), fmt_comment(hat.info.comment.as_deref()), self.scripts.len() + 1);
636                for field in fields {
637                    writeln!(&mut res, "    {} = snap.wrap(kwargs['{}'])", field.trans_name, escape(&field.name)).unwrap();
638                }
639                if !fields.is_empty() { res.push('\n') }
640                res
641            }
642            _ => return Err(TranslateError::UnsupportedHat(Box::new(hat.clone()))),
643        })
644    }
645}
646
647/// Translates NetsBlox project XML into PyBlox project JSON
648///
649/// On success, returns the project name and project json content as a tuple.
650pub fn translate(source: &str) -> Result<(CompactString, CompactString), TranslateError> {
651    let parser = Parser {
652        name_transformer: Box::new(netsblox_ast::util::c_ident),
653        autofill_generator: Box::new(|x| Ok(format_compact!("_{x}"))),
654        omit_nonhat_scripts: true, // we don't need dangling blocks of code since they can't do anything
655        expr_replacements: vec![],
656        stmt_replacements: vec![],
657    };
658    let project = parser.parse(source)?;
659    if project.roles.is_empty() { return Err(TranslateError::NoRoles) }
660
661    let mut roles = vec![];
662    for role in project.roles.iter() {
663        let mut role_info = RoleInfo::new(role.name.clone());
664        let mut stage_name = None;
665
666        for sprite in role.entities.iter() {
667            let mut sprite_info = SpriteInfo::new(sprite);
668            if stage_name.is_none() {
669                stage_name = Some(sprite_info.name.clone());
670            }
671
672            for costume in sprite.costumes.iter() {
673                let info = match &costume.init {
674                    Value::Image(x) => x.clone(),
675                    _ => panic!(), // the parser lib would never do this
676                };
677                sprite_info.costumes.push((costume.def.trans_name.clone(), info));
678            }
679            for sound in sprite.sounds.iter() {
680                let info = match &sound.init {
681                    Value::Audio(x) => x.clone(),
682                    _ => panic!(), // the parser lib would never do this
683                };
684                sprite_info.sounds.push((sound.def.trans_name.clone(), info));
685            }
686            for field in sprite.fields.iter() {
687                let value = wrap(ScriptInfo::new(stage_name.as_deref().unwrap()).translate_value(&field.init)?);
688                sprite_info.fields.push((field.def.trans_name.clone(), value));
689            }
690            for script in sprite.scripts.iter() {
691                let func_def = match script.hat.as_ref() {
692                    Some(x) => sprite_info.translate_hat(x, stage_name.as_deref().unwrap())?,
693                    None => continue, // dangling blocks of code need not be translated
694                };
695                let body = ScriptInfo::new(stage_name.as_deref().unwrap()).translate_stmts(&script.stmts)?;
696                let res = format_compact!("{}{}", func_def, indent(&body));
697                sprite_info.scripts.push(res);
698            }
699            role_info.sprites.push(sprite_info);
700        }
701        let stage_name = &role_info.sprites[0].name;
702
703        let mut editors = vec![];
704
705        let mut content = String::new();
706        content += "from netsblox import snap\n\n";
707        for global in role.globals.iter() {
708            let value = wrap(ScriptInfo::new(stage_name).translate_value(&global.init)?);
709            writeln!(&mut content, "{} = {}", global.def.trans_name, value).unwrap();
710        }
711        if !role.globals.is_empty() { content.push('\n') }
712        for func in role.funcs.iter() {
713            let params = func.params.iter().map(|v| v.trans_name.as_str());
714            let code = ScriptInfo::new(stage_name).translate_stmts(&func.stmts)?;
715            write!(&mut content, "def {}({}):\n{}\n\n", func.trans_name, Punctuated(params, ", "), indent(&code)).unwrap();
716        }
717        editors.push(json!({
718            "type": "globals",
719            "name": "globals",
720            "value": content,
721        }));
722
723        for (i, sprite) in role_info.sprites.iter().enumerate() {
724            let mut content = String::new();
725
726            for (field, value) in sprite.fields.iter() {
727                writeln!(&mut content, "{} = {}", field, value).unwrap();
728            }
729            if !sprite.fields.is_empty() { content.push('\n'); }
730
731            if i == 0 { // don't generate these for sprites
732                content += "last_answer = snap.wrap('')\n";
733                content.push('\n');
734            }
735
736            content += "def __init__(self):\n";
737            if i != 0 { // don't generate these for stage
738                writeln!(&mut content, "    self.pos = ({}, {})", sprite.pos.0, sprite.pos.1).unwrap();
739                writeln!(&mut content, "    self.heading = {}", sprite.heading).unwrap();
740                writeln!(&mut content, "    self.pen_color = ({}, {}, {})", sprite.color.0, sprite.color.1, sprite.color.2).unwrap();
741                writeln!(&mut content, "    self.scale = {}", sprite.scale).unwrap();
742                writeln!(&mut content, "    self.visible = {}", if sprite.visible { "True" } else { "False" }).unwrap();
743
744                if !sprite.sounds.is_empty() {
745                    content.push('\n');
746                }
747                for (trans_name, info) in sprite.sounds.iter() {
748                    writeln!(&mut content, "    self.sounds.add('{}', sounds.{}_snd_{})", escape(&info.1), sprite.name, trans_name).unwrap();
749                }
750
751                if !sprite.costumes.is_empty() {
752                    content.push('\n');
753                }
754                for (trans_name, info) in sprite.costumes.iter() {
755                    writeln!(&mut content, "    self.costumes.add('{}', images.{}_cst_{})", escape(&info.2), sprite.name, trans_name).unwrap();
756                }
757            }
758            if !sprite.sounds.is_empty() || !sprite.costumes.is_empty() {
759                content.push('\n');
760            }
761            match sprite.active_costume {
762                Some(idx) => writeln!(&mut content, "    self.costume = '{}'", escape(&sprite.costumes[idx].1.2)).unwrap(),
763                None => content += "    self.costume = None\n",
764            }
765            content.push('\n');
766
767            for func in sprite.funcs.iter() {
768                let params = iter::once("self").chain(func.params.iter().map(|v| v.trans_name.as_str()));
769                let code = ScriptInfo::new(stage_name).translate_stmts(&func.stmts)?;
770                write!(&mut content, "def {}({}):\n{}\n\n", func.trans_name, Punctuated(params, ", "), indent(&code)).unwrap();
771            }
772
773            for script in sprite.scripts.iter() {
774                content += script;
775                content += "\n\n";
776            }
777
778            editors.push(json!({
779                "type": if i == 0 { "stage" } else { "sprite" },
780                "name": sprite.name,
781                "value": content,
782            }));
783        }
784
785        let mut images = serde_json::Map::new();
786        for sprite in role_info.sprites.iter() {
787            for (costume, info) in sprite.costumes.iter() {
788                let center = match info.1 {
789                    Some(ui_center) => match image::load_from_memory(&info.0) {
790                        Ok(img) => (ui_center.0 - img.width() as f64 / 2.0, -(ui_center.1 - img.height() as f64 / 2.0)),
791                        Err(_) => return Err(TranslateError::UnknownImageFormat),
792                    }
793                    None => (0.0, 0.0),
794                };
795                images.insert(format!("{}_cst_{}", sprite.name, costume), json!({
796                    "img": base64::engine::general_purpose::STANDARD.encode(info.0.as_slice()),
797                    "center": center,
798                }));
799            }
800        }
801
802        let mut sounds = serde_json::Map::new();
803        for sprite in role_info.sprites.iter() {
804            for (sound, info) in sprite.sounds.iter() {
805                sounds.insert(format!("{}_snd_{}", sprite.name, sound), json!({
806                    "snd": base64::engine::general_purpose::STANDARD.encode(info.0.as_slice()),
807                }));
808            }
809        }
810
811        roles.push(json!({
812            "name": role_info.name,
813            "stage_size": role.stage_size,
814            "block_sources": [ "netsblox://assets/default-blocks.json" ],
815            "blocks": [],
816            "imports": ["time", "math", "random"],
817            "editors": editors,
818            "images": images,
819            "sounds": sounds,
820        }));
821    }
822
823    let res = json!({
824        "roles": roles,
825    });
826
827    Ok((project.name, res.to_compact_string()))
828}