aether/pytranspile/
mod.rs

1pub mod diagnostics;
2pub mod emitter;
3pub mod ir;
4pub mod options;
5pub mod python;
6
7pub use diagnostics::{Diagnostic, Diagnostics, Severity};
8pub use ir::{Expr, Module, Span, Stmt};
9pub use options::{DecimalMode, TranspileOptions};
10
11#[derive(Debug)]
12pub struct PythonToAetherResult {
13    pub aether: Option<String>,
14    pub diagnostics: Diagnostics,
15    pub numpy_used: bool,
16    pub io_used: bool,
17    pub console_used: bool,
18}
19
20/// Transpile Python source into Aether source.
21///
22/// - By default (`TranspileOptions::default()`), numpy / filesystem / network / console IO are rejected.
23/// - When the `pytranspile` cargo feature is disabled, this always returns an error diagnostic.
24pub fn python_to_aether(source: &str, opts: &TranspileOptions) -> PythonToAetherResult {
25    let ir_res = python::python_to_ir(source, opts);
26
27    // If python parsing already failed, return early.
28    if ir_res.diagnostics.has_errors() || ir_res.module.is_none() {
29        return PythonToAetherResult {
30            aether: None,
31            diagnostics: ir_res.diagnostics,
32            numpy_used: ir_res.numpy_used,
33            io_used: ir_res.io_used,
34            console_used: ir_res.console_used,
35        };
36    }
37
38    // Sanity: module present.
39    let module = ir_res.module.unwrap();
40
41    let emit_res = emitter::ir_to_aether(&module, opts);
42
43    // Combine diagnostics (python stage + emit stage).
44    let mut diags = ir_res.diagnostics;
45    for d in emit_res.diagnostics.0 {
46        diags.push(d);
47    }
48
49    if diags.has_errors() {
50        PythonToAetherResult {
51            aether: None,
52            diagnostics: diags,
53            numpy_used: ir_res.numpy_used,
54            io_used: ir_res.io_used,
55            console_used: ir_res.console_used,
56        }
57    } else {
58        PythonToAetherResult {
59            aether: emit_res.code,
60            diagnostics: diags,
61            numpy_used: ir_res.numpy_used,
62            io_used: ir_res.io_used,
63            console_used: ir_res.console_used,
64        }
65    }
66}
67
68/// Convenience: returns Ok(code) if transpilation succeeded, Err(diagnostics) otherwise.
69pub fn python_to_aether_checked(
70    source: &str,
71    opts: &TranspileOptions,
72) -> Result<String, Diagnostics> {
73    let res = python_to_aether(source, opts);
74    match res.aether {
75        Some(code) if !res.diagnostics.has_errors() => Ok(code),
76        _ => Err(res.diagnostics),
77    }
78}
79
80pub fn aether_parse_ast(code: &str) -> Result<crate::ast::Program, crate::parser::ParseError> {
81    let mut parser = crate::parser::Parser::new(code);
82    parser.parse_program()
83}
84
85/// Parse Aether code into a JSON AST (best-effort, stable enough for debugging/tooling).
86pub fn aether_parse_ast_json(code: &str) -> Result<serde_json::Value, Diagnostics> {
87    let program = aether_parse_ast(code).map_err(|e| {
88        let mut d = Diagnostics::new();
89        d.push(Diagnostic::error(
90            "AETHER_PARSE_ERROR",
91            e.to_string(),
92            Span::default(),
93        ));
94        d
95    })?;
96
97    Ok(program_to_json(&program))
98}
99
100/// Evaluate Aether code in DSL-safe mode (no filesystem/network permissions) and with
101/// static pre-checks enforced (console/io rejection).
102pub fn aether_eval_safe(
103    code: &str,
104    opts: &TranspileOptions,
105) -> Result<crate::value::Value, Diagnostics> {
106    let diags = aether_check(code, opts);
107    if diags.has_errors() {
108        return Err(diags);
109    }
110
111    let mut engine = crate::Aether::new();
112    engine.eval(code).map_err(|e| {
113        let mut d = Diagnostics::new();
114        d.push(Diagnostic::error("AETHER_EVAL_ERROR", e, Span::default()));
115        d
116    })
117}
118
119/// Aether-side static checker for disallowed builtins (best-effort).
120///
121/// Note: Aether AST nodes currently don't carry spans, so diagnostics use default spans.
122pub fn aether_check(code: &str, opts: &TranspileOptions) -> Diagnostics {
123    let mut diagnostics = Diagnostics::new();
124
125    let program = match aether_parse_ast(code) {
126        Ok(p) => p,
127        Err(e) => {
128            diagnostics.push(Diagnostic::error(
129                "AETHER_PARSE_ERROR",
130                e.to_string(),
131                crate::pytranspile::ir::Span::default(),
132            ));
133            return diagnostics;
134        }
135    };
136
137    for stmt in &program {
138        check_stmt(stmt, opts, &mut diagnostics);
139    }
140
141    diagnostics
142}
143
144fn check_stmt(stmt: &crate::ast::Stmt, opts: &TranspileOptions, d: &mut Diagnostics) {
145    use crate::ast::Stmt;
146    match stmt {
147        Stmt::Set { value, .. } => check_expr(value, opts, d),
148        Stmt::SetIndex {
149            object,
150            index,
151            value,
152        } => {
153            check_expr(object, opts, d);
154            check_expr(index, opts, d);
155            check_expr(value, opts, d);
156        }
157        Stmt::FuncDef { body, .. } | Stmt::GeneratorDef { body, .. } => {
158            for s in body {
159                check_stmt(s, opts, d);
160            }
161        }
162        Stmt::LazyDef { expr, .. } => check_expr(expr, opts, d),
163        Stmt::Return(expr) | Stmt::Yield(expr) => check_expr(expr, opts, d),
164        Stmt::While { condition, body } => {
165            check_expr(condition, opts, d);
166            for s in body {
167                check_stmt(s, opts, d);
168            }
169        }
170        Stmt::For {
171            var,
172            iterable,
173            body,
174        } => {
175            let _ = var;
176            check_expr(iterable, opts, d);
177            for s in body {
178                check_stmt(s, opts, d);
179            }
180        }
181        Stmt::ForIndexed {
182            index_var,
183            value_var,
184            iterable,
185            body,
186        } => {
187            let _ = (index_var, value_var);
188            check_expr(iterable, opts, d);
189            for s in body {
190                check_stmt(s, opts, d);
191            }
192        }
193        Stmt::Switch {
194            expr,
195            cases,
196            default,
197        } => {
198            check_expr(expr, opts, d);
199            for (c, b) in cases {
200                check_expr(c, opts, d);
201                for s in b {
202                    check_stmt(s, opts, d);
203                }
204            }
205            if let Some(b) = default {
206                for s in b {
207                    check_stmt(s, opts, d);
208                }
209            }
210        }
211        Stmt::Expression(expr) => check_expr(expr, opts, d),
212        Stmt::Import { .. } | Stmt::Export(_) => {}
213        Stmt::Throw(expr) => check_expr(expr, opts, d),
214        Stmt::Break | Stmt::Continue => {}
215    }
216}
217
218fn check_expr(expr: &crate::ast::Expr, opts: &TranspileOptions, d: &mut Diagnostics) {
219    use crate::ast::Expr;
220    match expr {
221        Expr::Binary { left, right, .. } => {
222            check_expr(left, opts, d);
223            check_expr(right, opts, d);
224        }
225        Expr::Unary { expr, .. } => check_expr(expr, opts, d),
226        Expr::Call { func, args } => {
227            if let Expr::Identifier(name) = func.as_ref() {
228                if opts.reject_console && is_console_builtin(name) {
229                    d.push(Diagnostic::error(
230                        "AETHER_CONSOLE_REJECTED",
231                        format!("console builtin '{name}' is rejected"),
232                        crate::pytranspile::ir::Span::default(),
233                    ));
234                }
235                if opts.reject_io && is_io_builtin(name) {
236                    d.push(Diagnostic::error(
237                        "AETHER_IO_REJECTED",
238                        format!("io builtin '{name}' is rejected"),
239                        crate::pytranspile::ir::Span::default(),
240                    ));
241                }
242            }
243            check_expr(func, opts, d);
244            for a in args {
245                check_expr(a, opts, d);
246            }
247        }
248        Expr::Array(items) => {
249            for e in items {
250                check_expr(e, opts, d);
251            }
252        }
253        Expr::Dict(items) => {
254            for (_k, v) in items {
255                check_expr(v, opts, d);
256            }
257        }
258        Expr::Index { object, index } => {
259            check_expr(object, opts, d);
260            check_expr(index, opts, d);
261        }
262        Expr::If {
263            condition,
264            then_branch,
265            elif_branches,
266            else_branch,
267        } => {
268            check_expr(condition, opts, d);
269            for s in then_branch {
270                check_stmt(s, opts, d);
271            }
272            for (c, b) in elif_branches {
273                check_expr(c, opts, d);
274                for s in b {
275                    check_stmt(s, opts, d);
276                }
277            }
278            if let Some(b) = else_branch {
279                for s in b {
280                    check_stmt(s, opts, d);
281                }
282            }
283        }
284        Expr::Lambda { body, .. } => {
285            for s in body {
286                check_stmt(s, opts, d);
287            }
288        }
289        Expr::Number(_)
290        | Expr::BigInteger(_)
291        | Expr::String(_)
292        | Expr::Boolean(_)
293        | Expr::Null
294        | Expr::Identifier(_) => {}
295    }
296}
297
298fn is_console_builtin(name: &str) -> bool {
299    matches!(name, "PRINT" | "PRINTLN" | "INPUT")
300}
301
302fn is_io_builtin(name: &str) -> bool {
303    matches!(
304        name,
305        "READ_FILE"
306            | "WRITE_FILE"
307            | "APPEND_FILE"
308            | "DELETE_FILE"
309            | "FILE_EXISTS"
310            | "LIST_DIR"
311            | "CREATE_DIR"
312            | "HTTP_GET"
313            | "HTTP_POST"
314            | "HTTP_PUT"
315            | "HTTP_DELETE"
316    ) || name.starts_with("EXCEL_")
317}
318
319fn program_to_json(program: &crate::ast::Program) -> serde_json::Value {
320    serde_json::Value::Array(program.iter().map(stmt_to_json).collect())
321}
322
323fn stmt_to_json(stmt: &crate::ast::Stmt) -> serde_json::Value {
324    use crate::ast::Stmt;
325    match stmt {
326        Stmt::Set { name, value } => {
327            serde_json::json!({"type":"Set","name":name,"value":expr_to_json(value)})
328        }
329        Stmt::SetIndex {
330            object,
331            index,
332            value,
333        } => {
334            serde_json::json!({"type":"SetIndex","object":expr_to_json(object),"index":expr_to_json(index),"value":expr_to_json(value)})
335        }
336        Stmt::FuncDef { name, params, body } => {
337            serde_json::json!({"type":"FuncDef","name":name,"params":params,"body": program_to_json(body)})
338        }
339        Stmt::GeneratorDef { name, params, body } => {
340            serde_json::json!({"type":"GeneratorDef","name":name,"params":params,"body": program_to_json(body)})
341        }
342        Stmt::LazyDef { name, expr } => {
343            serde_json::json!({"type":"LazyDef","name":name,"expr":expr_to_json(expr)})
344        }
345        Stmt::Return(expr) => serde_json::json!({"type":"Return","value":expr_to_json(expr)}),
346        Stmt::Yield(expr) => serde_json::json!({"type":"Yield","value":expr_to_json(expr)}),
347        Stmt::Break => serde_json::json!({"type":"Break"}),
348        Stmt::Continue => serde_json::json!({"type":"Continue"}),
349        Stmt::While { condition, body } => {
350            serde_json::json!({"type":"While","condition":expr_to_json(condition),"body": program_to_json(body)})
351        }
352        Stmt::For {
353            var,
354            iterable,
355            body,
356        } => {
357            serde_json::json!({"type":"For","var":var,"iterable":expr_to_json(iterable),"body": program_to_json(body)})
358        }
359        Stmt::ForIndexed {
360            index_var,
361            value_var,
362            iterable,
363            body,
364        } => {
365            serde_json::json!({"type":"ForIndexed","index_var":index_var,"value_var":value_var,"iterable":expr_to_json(iterable),"body": program_to_json(body)})
366        }
367        Stmt::Switch {
368            expr,
369            cases,
370            default,
371        } => {
372            let cases_json: Vec<_> = cases
373                .iter()
374                .map(|(c, b)| serde_json::json!({"when":expr_to_json(c),"body":program_to_json(b)}))
375                .collect();
376            let default_json = default.as_ref().map(program_to_json);
377            serde_json::json!({"type":"Switch","expr":expr_to_json(expr),"cases":cases_json,"default":default_json})
378        }
379        Stmt::Import {
380            names,
381            path,
382            aliases,
383            namespace,
384        } => serde_json::json!({
385            "type":"Import",
386            "names": names,
387            "path": path,
388            "aliases": aliases,
389            "namespace": namespace,
390        }),
391        Stmt::Export(name) => serde_json::json!({"type":"Export","name":name}),
392        Stmt::Throw(expr) => serde_json::json!({"type":"Throw","value":expr_to_json(expr)}),
393        Stmt::Expression(expr) => {
394            serde_json::json!({"type":"Expression","value":expr_to_json(expr)})
395        }
396    }
397}
398
399fn expr_to_json(expr: &crate::ast::Expr) -> serde_json::Value {
400    use crate::ast::Expr;
401    match expr {
402        Expr::Number(n) => serde_json::json!({"type":"Number","value":n}),
403        Expr::BigInteger(s) => serde_json::json!({"type":"BigInteger","value":s}),
404        Expr::String(s) => serde_json::json!({"type":"String","value":s}),
405        Expr::Boolean(b) => serde_json::json!({"type":"Boolean","value":b}),
406        Expr::Null => serde_json::json!({"type":"Null"}),
407        Expr::Identifier(name) => serde_json::json!({"type":"Identifier","name":name}),
408        Expr::Binary { left, op, right } => {
409            serde_json::json!({"type":"Binary","op":binop_to_str(op),"left":expr_to_json(left),"right":expr_to_json(right)})
410        }
411        Expr::Unary { op, expr } => {
412            serde_json::json!({"type":"Unary","op":unaryop_to_str(op),"expr":expr_to_json(expr)})
413        }
414        Expr::Call { func, args } => {
415            serde_json::json!({"type":"Call","func":expr_to_json(func),"args": serde_json::Value::Array(args.iter().map(expr_to_json).collect())})
416        }
417        Expr::Array(items) => {
418            serde_json::json!({"type":"Array","items": serde_json::Value::Array(items.iter().map(expr_to_json).collect())})
419        }
420        Expr::Dict(items) => {
421            let pairs: Vec<_> = items
422                .iter()
423                .map(|(k, v)| serde_json::json!({"key":k,"value":expr_to_json(v)}))
424                .collect();
425            serde_json::json!({"type":"Dict","items":pairs})
426        }
427        Expr::Index { object, index } => {
428            serde_json::json!({"type":"Index","object":expr_to_json(object),"index":expr_to_json(index)})
429        }
430        Expr::If {
431            condition,
432            then_branch,
433            elif_branches,
434            else_branch,
435        } => {
436            let elifs: Vec<_> = elif_branches.iter().map(|(c, b)| serde_json::json!({"condition":expr_to_json(c),"body":program_to_json(b)})).collect();
437            let else_json = else_branch.as_ref().map(program_to_json);
438            serde_json::json!({"type":"If","condition":expr_to_json(condition),"then":program_to_json(then_branch),"elifs":elifs,"else":else_json})
439        }
440        Expr::Lambda { params, body } => {
441            serde_json::json!({"type":"Lambda","params":params,"body":program_to_json(body)})
442        }
443    }
444}
445
446fn binop_to_str(op: &crate::ast::BinOp) -> &'static str {
447    use crate::ast::BinOp;
448    match op {
449        BinOp::Add => "+",
450        BinOp::Subtract => "-",
451        BinOp::Multiply => "*",
452        BinOp::Divide => "/",
453        BinOp::Modulo => "%",
454        BinOp::Equal => "==",
455        BinOp::NotEqual => "!=",
456        BinOp::Less => "<",
457        BinOp::LessEqual => "<=",
458        BinOp::Greater => ">",
459        BinOp::GreaterEqual => ">=",
460        BinOp::And => "&&",
461        BinOp::Or => "||",
462    }
463}
464
465fn unaryop_to_str(op: &crate::ast::UnaryOp) -> &'static str {
466    use crate::ast::UnaryOp;
467    match op {
468        UnaryOp::Minus => "-",
469        UnaryOp::Not => "!",
470    }
471}