use anyhow::Result;
use crate::cli::style;
#[cfg(feature = "wasm")]
pub fn run() -> Result<()> {
use super::{Flow, read_loop};
use std::cell::RefCell;
let runtime = afterburner_wasi::ruby_runner::resolve_ruby_runtime()
.map_err(|e| anyhow::anyhow!("{e}"))?;
style::repl_banner_lang(env!("CARGO_PKG_VERSION"), "ruby");
eprintln!(
" {}",
style::muted(
"each line re-runs the session (one CRuby boot per line; two when echoing a value)"
)
);
let session: RefCell<Vec<String>> = RefCell::new(Vec::new());
let baseline: RefCell<usize> = RefCell::new(0);
read_loop("rb", |trimmed| {
if let Some(rest) = trimmed.strip_prefix(':') {
match rest.trim() {
"clear" | "reset" => {
session.borrow_mut().clear();
*baseline.borrow_mut() = 0;
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 display = build_program(&session.borrow(), trimmed, true);
let prev = *baseline.borrow();
match run_program(&runtime, &display) {
Ok(stdout) => {
let suffix = if stdout.len() >= prev {
&stdout[prev..]
} else {
&stdout[..]
};
if !suffix.is_empty() {
print!("{suffix}");
use std::io::Write;
let _ = std::io::stdout().flush();
}
session.borrow_mut().push(trimmed.to_string());
let plain = build_program(&session.borrow(), "", false);
let new_baseline = run_program(&runtime, &plain)
.map(|s| s.len())
.unwrap_or(stdout.len());
*baseline.borrow_mut() = new_baseline;
}
Err(e) => {
eprintln!(" {}", style::fail(&clean_rb_err(&e.to_string())));
}
}
Flow::Continue
})
}
#[cfg(not(feature = "wasm"))]
pub fn run() -> Result<()> {
let _ = style::muted("");
anyhow::bail!("Ruby REPL requires the `wasm` cargo feature (rebuild with `--features wasm`).")
}
#[cfg(feature = "wasm")]
fn run_program(rt: &afterburner_wasi::ruby_runner::RubyRuntime, program: &str) -> Result<String> {
use afterburner_wasi::ruby_runner::run_ruby_with;
let out = run_ruby_with(rt, program).map_err(|e| anyhow::anyhow!("ruby runtime error: {e}"))?;
if out.exit_code != 0 {
let err = String::from_utf8_lossy(&out.stderr);
let text = if err.trim().is_empty() {
String::from_utf8_lossy(&out.stdout).into_owned()
} else {
err.into_owned()
};
anyhow::bail!("{}", text.trim());
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn build_program(session: &[String], line: &str, echo: bool) -> String {
let mut out = String::new();
for prior in session {
out.push_str(prior);
out.push('\n');
}
let line = line.trim();
if line.is_empty() {
return out;
}
if echo {
out.push_str("__burn_v = (");
out.push_str(line);
out.push_str(")\np(__burn_v) unless __burn_v.nil?\n");
} else {
out.push_str(line);
out.push('\n');
}
out
}
#[cfg(feature = "wasm")]
fn clean_rb_err(raw: &str) -> String {
let trimmed = raw.trim();
let last = trimmed
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.unwrap_or(trimmed)
.trim();
let after_loc = last
.strip_prefix("-e:")
.and_then(|s| s.split_once(": ").map(|(_, rest)| rest))
.unwrap_or(last);
after_loc.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 line_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("p(__burn_v) unless __burn_v.nil?"),
"echoes: {p}"
);
}
#[test]
fn plain_line_is_not_echoed() {
let p = build_program(&[], "x = 5", false);
assert!(p.contains("x = 5"), "got: {p}");
assert!(!p.contains("__burn_v"), "plain build has no echo: {p}");
}
#[test]
fn empty_current_line_yields_committed_plain_program() {
let session = vec!["x = 1".to_string(), "puts x".to_string()];
let p = build_program(&session, "", false);
assert!(p.contains("x = 1"), "prior assignment present: {p}");
assert!(p.contains("puts x"), "prior call present: {p}");
assert!(!p.contains("__burn_v"), "no echo wrapper: {p}");
}
#[test]
fn prior_session_lines_precede_the_current_line() {
let session = vec!["x = 10".to_string()];
let p = build_program(&session, "x * 2", true);
let x_at = p.find("x = 10").expect("session line present");
let expr_at = p.find("x * 2").expect("current line present");
assert!(x_at < expr_at, "session replays before the line: {p}");
}
#[cfg(feature = "wasm")]
#[test]
fn clean_rb_err_keeps_the_message_and_drops_the_locator() {
let raw = "-e:1:in '<main>': undefined local variable or method 'z' (NameError)";
let cleaned = clean_rb_err(raw);
assert!(
cleaned.contains("undefined local variable"),
"keeps the message: {cleaned}"
);
assert!(
!cleaned.starts_with("-e:"),
"drops the -e: locator: {cleaned}"
);
}
#[cfg(feature = "wasm")]
#[test]
fn clean_rb_err_handles_a_plain_message() {
let raw = "some error\nSyntaxError: unexpected end";
assert_eq!(clean_rb_err(raw), "SyntaxError: unexpected end");
}
}