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), 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(", "))) }
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), 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(¶m.trans_name);
173 }
174 (format_compact!("(lambda{}: {})",params_string, wrap(self.translate_expr(value)?)), Type::Wrapped) },
176 _ => return Err(TranslateError::CommandRing),
177 }
178
179 ExprKind::This => ("self".into(), Type::Wrapped), ExprKind::Entity { trans_name, .. } => (trans_name.clone(), Type::Wrapped), ExprKind::ImageOfEntity { entity } => (format_compact!("{}.get_image()", self.translate_expr(entity)?.0), Type::Wrapped), ExprKind::ImageOfDrawings => (format_compact!("{}.get_drawings()", self.stage_name), Type::Wrapped), ExprKind::IsTouchingEntity { entity } => (format_compact!("self.is_touching({})", self.translate_expr(entity)?.0), Type::Wrapped), 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), 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), 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), 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), ExprKind::Range { start, stop } => (format_compact!("snap.srange({}, {})", self.translate_expr(start)?.0, self.translate_expr(stop)?.0), Type::Wrapped), 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), ExprKind::PenDown => ("self.drawing".into(), Type::Wrapped), ExprKind::Size => ("(self.scale * 100)".into(), Type::Wrapped),
403 ExprKind::IsVisible => ("self.visible".into(), Type::Wrapped), ExprKind::RpcError => ("(get_error() or '')".into(), Type::Unknown),
406
407 ExprKind::Clone { target } => (format_compact!("{}.clone()", self.translate_expr(target)?.0), Type::Wrapped), 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), _ => 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
647pub 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, 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!(), };
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!(), };
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, };
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 { content += "last_answer = snap.wrap('')\n";
733 content.push('\n');
734 }
735
736 content += "def __init__(self):\n";
737 if i != 0 { 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}