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        } => serde_json::json!({"type":"Import","names":names,"path":path,"aliases":aliases}),
384        Stmt::Export(name) => serde_json::json!({"type":"Export","name":name}),
385        Stmt::Throw(expr) => serde_json::json!({"type":"Throw","value":expr_to_json(expr)}),
386        Stmt::Expression(expr) => {
387            serde_json::json!({"type":"Expression","value":expr_to_json(expr)})
388        }
389    }
390}
391
392fn expr_to_json(expr: &crate::ast::Expr) -> serde_json::Value {
393    use crate::ast::Expr;
394    match expr {
395        Expr::Number(n) => serde_json::json!({"type":"Number","value":n}),
396        Expr::BigInteger(s) => serde_json::json!({"type":"BigInteger","value":s}),
397        Expr::String(s) => serde_json::json!({"type":"String","value":s}),
398        Expr::Boolean(b) => serde_json::json!({"type":"Boolean","value":b}),
399        Expr::Null => serde_json::json!({"type":"Null"}),
400        Expr::Identifier(name) => serde_json::json!({"type":"Identifier","name":name}),
401        Expr::Binary { left, op, right } => {
402            serde_json::json!({"type":"Binary","op":binop_to_str(op),"left":expr_to_json(left),"right":expr_to_json(right)})
403        }
404        Expr::Unary { op, expr } => {
405            serde_json::json!({"type":"Unary","op":unaryop_to_str(op),"expr":expr_to_json(expr)})
406        }
407        Expr::Call { func, args } => {
408            serde_json::json!({"type":"Call","func":expr_to_json(func),"args": serde_json::Value::Array(args.iter().map(expr_to_json).collect())})
409        }
410        Expr::Array(items) => {
411            serde_json::json!({"type":"Array","items": serde_json::Value::Array(items.iter().map(expr_to_json).collect())})
412        }
413        Expr::Dict(items) => {
414            let pairs: Vec<_> = items
415                .iter()
416                .map(|(k, v)| serde_json::json!({"key":k,"value":expr_to_json(v)}))
417                .collect();
418            serde_json::json!({"type":"Dict","items":pairs})
419        }
420        Expr::Index { object, index } => {
421            serde_json::json!({"type":"Index","object":expr_to_json(object),"index":expr_to_json(index)})
422        }
423        Expr::If {
424            condition,
425            then_branch,
426            elif_branches,
427            else_branch,
428        } => {
429            let elifs: Vec<_> = elif_branches.iter().map(|(c, b)| serde_json::json!({"condition":expr_to_json(c),"body":program_to_json(b)})).collect();
430            let else_json = else_branch.as_ref().map(program_to_json);
431            serde_json::json!({"type":"If","condition":expr_to_json(condition),"then":program_to_json(then_branch),"elifs":elifs,"else":else_json})
432        }
433        Expr::Lambda { params, body } => {
434            serde_json::json!({"type":"Lambda","params":params,"body":program_to_json(body)})
435        }
436    }
437}
438
439fn binop_to_str(op: &crate::ast::BinOp) -> &'static str {
440    use crate::ast::BinOp;
441    match op {
442        BinOp::Add => "+",
443        BinOp::Subtract => "-",
444        BinOp::Multiply => "*",
445        BinOp::Divide => "/",
446        BinOp::Modulo => "%",
447        BinOp::Equal => "==",
448        BinOp::NotEqual => "!=",
449        BinOp::Less => "<",
450        BinOp::LessEqual => "<=",
451        BinOp::Greater => ">",
452        BinOp::GreaterEqual => ">=",
453        BinOp::And => "&&",
454        BinOp::Or => "||",
455    }
456}
457
458fn unaryop_to_str(op: &crate::ast::UnaryOp) -> &'static str {
459    use crate::ast::UnaryOp;
460    match op {
461        UnaryOp::Minus => "-",
462        UnaryOp::Not => "!",
463    }
464}