use crate::cli::style;
use anyhow::Result;
use super::super::args::Cli;
#[cfg(feature = "wasm")]
pub fn run(_cli: &Cli) -> Result<()> {
use super::{Flow, read_loop};
use afterburner_wasi::pyodide_runner::WarmPyInterpreter;
use std::io::Write;
style::repl_banner_lang(env!("CARGO_PKG_VERSION"), "python");
eprintln!(
" {}",
style::muted("warming a persistent CPython interpreter (state carries across lines)...")
);
let mut interp = WarmPyInterpreter::boot_resolved().map_err(|e| anyhow::anyhow!("{e}"))?;
read_loop("py", move |trimmed| {
if let Some(rest) = trimmed.strip_prefix(':') {
match rest.trim() {
"clear" | "reset" => {
let _ = interp.reset_persistent();
eprintln!(" {}", style::muted("session cleared"));
}
"help" | "?" => print_help(),
"exit" | "quit" => return Flow::Exit,
other => eprintln!(
" {}",
style::fail(&format!("unknown command :{other}, try :help"))
),
}
return Flow::Continue;
}
let is_expr = looks_like_expression(trimmed);
let program = build_program(trimmed, is_expr);
match interp.run_persistent(&program) {
Ok(out) => {
let err = String::from_utf8_lossy(&out.stderr);
if !err.trim().is_empty() {
eprintln!(" {}", style::fail(&clean_py_err(&err)));
} else {
let stdout = String::from_utf8_lossy(&out.stdout);
if !stdout.is_empty() {
print!("{stdout}");
let _ = std::io::stdout().flush();
}
}
}
Err(e) => eprintln!(" {}", style::fail(&clean_py_err(&e.to_string()))),
}
Flow::Continue
})
}
#[cfg(not(feature = "wasm"))]
pub fn run(_cli: &Cli) -> Result<()> {
let _ = style::muted("");
anyhow::bail!("Python REPL requires the `wasm` cargo feature (rebuild with `--features wasm`).")
}
fn build_program(line: &str, echo: bool) -> String {
let line = line.trim();
if line.is_empty() {
return String::new();
}
if echo {
format!("__burn_v = ({line})\nif __burn_v is not None:\n print(repr(__burn_v))\n")
} else {
format!("{line}\n")
}
}
fn looks_like_expression(line: &str) -> bool {
let t = line.trim_start();
if t.is_empty() {
return false;
}
const STMT_KW: &[&str] = &[
"import ",
"from ",
"def ",
"class ",
"if ",
"elif ",
"else",
"for ",
"while ",
"with ",
"try",
"except",
"finally",
"return",
"raise ",
"pass",
"break",
"continue",
"global ",
"nonlocal ",
"assert ",
"del ",
"yield",
"async ",
"await ",
"@",
];
if STMT_KW.iter().any(|kw| t.starts_with(kw)) {
return false;
}
if is_top_level_assignment(t) {
return false;
}
true
}
fn is_top_level_assignment(s: &str) -> bool {
let bytes = s.as_bytes();
let mut depth: i32 = 0;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b'=' if depth == 0 => {
let prev = if i > 0 { bytes[i - 1] } else { b' ' };
let next = if i + 1 < bytes.len() {
bytes[i + 1]
} else {
b' '
};
let is_comparison = next == b'=' || matches!(prev, b'=' | b'!' | b'<' | b'>');
if !is_comparison {
return true;
}
}
_ => {}
}
i += 1;
}
false
}
#[cfg(feature = "wasm")]
fn clean_py_err(raw: &str) -> String {
let trimmed = raw.trim();
trimmed
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.unwrap_or(trimmed)
.trim()
.to_string()
}
#[cfg(feature = "wasm")]
fn print_help() {
for (cmd, desc) in [
(":clear", "forget the session"),
(":help", "show commands"),
(":exit | :quit", "leave the REPL"),
] {
eprintln!(
" {} {}",
style::accent(&format!("{cmd:<16}")),
style::muted(desc)
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expression_is_wrapped_to_echo_its_value() {
let p = build_program("1 + 1", true);
assert!(p.contains("__burn_v = (1 + 1)"), "got: {p}");
assert!(p.contains("print(repr(__burn_v))"), "echoes value: {p}");
}
#[test]
fn assignment_is_a_statement_not_echoed() {
let p = build_program("x = 5", false);
assert!(p.contains("x = 5"), "got: {p}");
assert!(!p.contains("__burn_v"), "assignment is not echoed: {p}");
}
#[test]
fn empty_line_yields_empty_program() {
assert!(build_program("", false).is_empty());
assert!(build_program(" ", true).is_empty());
}
#[test]
fn def_is_a_statement() {
assert!(!looks_like_expression("def f(): return 1"));
assert!(!looks_like_expression("import os"));
assert!(!looks_like_expression("from os import path"));
assert!(!looks_like_expression("for i in range(3): pass"));
}
#[test]
fn call_and_arithmetic_are_expressions() {
assert!(looks_like_expression("print('hi')"));
assert!(looks_like_expression("len([1,2,3])"));
assert!(looks_like_expression("2 ** 10"));
assert!(looks_like_expression("x == 5"));
assert!(looks_like_expression("x <= 5"));
}
#[test]
fn keyword_arg_call_is_expression_not_assignment() {
assert!(!is_top_level_assignment("f(a=1)"));
assert!(looks_like_expression("sorted([3,1], key=abs)"));
}
#[test]
fn augmented_assignment_is_statement() {
assert!(is_top_level_assignment("x += 1"));
assert!(!looks_like_expression("x += 1"));
}
#[test]
fn comparison_is_not_assignment() {
assert!(!is_top_level_assignment("a == b"));
assert!(!is_top_level_assignment("a != b"));
assert!(!is_top_level_assignment("a >= b"));
}
#[cfg(feature = "wasm")]
#[test]
fn clean_py_err_keeps_the_exception_line() {
let raw = "Traceback (most recent call last):\n File \"<stdin>\"\nNameError: name 'z' is not defined";
assert_eq!(clean_py_err(raw), "NameError: name 'z' is not defined");
}
}