Skip to main content

aver/services/
console.rs

1/// Console service — terminal I/O.
2///
3/// Methods:
4///   Console.print(msg)    — print to stdout (same as top-level `print`)
5///   Console.error(msg)    — print to stderr (no colour, raw message)
6///   Console.warn(msg)     — print to stderr prefixed with "[warn] "
7///   Console.readLine()    — read one line from stdin; Ok(line) or Err("EOF")
8///
9/// Each method requires its own exact effect (`Console.print`, `Console.error`, etc.).
10///
11/// Output capture (Iron 0.21 Hardcore Fuzz — parity target):
12///   `capture_output(|| { ... })` redirects every `Console.print` /
13///   `Console.error` / `Console.warn` call inside the closure into
14///   thread-local in-memory buffers. The closure's return value
15///   plus `(stdout_bytes, stderr_bytes)` come back to the caller.
16///   When no capture is active, the macros fall through to
17///   `println!` / `eprintln!` exactly as before. The flag is per
18///   thread so a long-running aver process that spawns a verify
19///   worker doesn't accidentally swallow the user's output.
20use std::cell::RefCell;
21use std::collections::HashMap;
22use std::sync::Arc as Rc;
23
24use crate::nan_value::{Arena, NanValue};
25use crate::value::{RuntimeError, Value, aver_display};
26
27thread_local! {
28    /// Per-thread capture buffers. `None` (default) means writes go
29    /// through to the process stdio fds; `Some((out, err))` means
30    /// they accumulate in the buffers and the caller will retrieve
31    /// them via [`finish_capture`].
32    static CAPTURE: RefCell<Option<(Vec<u8>, Vec<u8>)>> = const { RefCell::new(None) };
33}
34
35/// Run `f` with all `Console.*` writes inside it redirected to
36/// in-memory buffers. Returns `(f_result, stdout_bytes,
37/// stderr_bytes)`. Nesting is illegal — the outer call's buffers
38/// would mix with the inner's; we panic rather than silently
39/// merge. The expected user is the fuzz harness, which never
40/// nests captures.
41pub fn capture_output<F, R>(f: F) -> (R, Vec<u8>, Vec<u8>)
42where
43    F: FnOnce() -> R,
44{
45    CAPTURE.with(|c| {
46        let mut slot = c.borrow_mut();
47        if slot.is_some() {
48            panic!("capture_output: nested capture is not supported");
49        }
50        *slot = Some((Vec::new(), Vec::new()));
51    });
52    let result = f();
53    let (out, err) = CAPTURE.with(|c| c.borrow_mut().take()).unwrap_or_default();
54    (result, out, err)
55}
56
57/// Capture-aware `println!`-equivalent — embedders that bypass the
58/// `Console.print` builtin (currently the wasm-gc backend's import
59/// handler in `runtime::wasm_gc::imports::lm`) should call this
60/// instead of raw `println!` so the thread-local `capture_output`
61/// buffer sees their writes too.
62pub fn write_stdout_str(s: &str) {
63    write_stdout(s);
64}
65
66/// Capture-aware `eprintln!`-equivalent. Same rationale as
67/// `write_stdout_str`.
68pub fn write_stderr_plain_str(s: &str) {
69    write_stderr_plain(s);
70}
71
72fn write_stdout(s: &str) {
73    CAPTURE.with(|c| {
74        if let Some((out, _)) = c.borrow_mut().as_mut() {
75            out.extend_from_slice(s.as_bytes());
76            out.push(b'\n');
77        } else {
78            println!("{}", s);
79        }
80    });
81}
82
83fn write_stderr_plain(s: &str) {
84    CAPTURE.with(|c| {
85        if let Some((_, err)) = c.borrow_mut().as_mut() {
86            err.extend_from_slice(s.as_bytes());
87            err.push(b'\n');
88        } else {
89            eprintln!("{}", s);
90        }
91    });
92}
93
94fn write_stderr_warn(s: &str) {
95    CAPTURE.with(|c| {
96        if let Some((_, err)) = c.borrow_mut().as_mut() {
97            err.extend_from_slice(b"[warn] ");
98            err.extend_from_slice(s.as_bytes());
99            err.push(b'\n');
100        } else {
101            eprintln!("[warn] {}", s);
102        }
103    });
104}
105
106pub fn register(global: &mut HashMap<String, Value>) {
107    let mut members = HashMap::new();
108    for method in &["print", "error", "warn", "readLine"] {
109        members.insert(
110            method.to_string(),
111            Value::Builtin(format!("Console.{}", method)),
112        );
113    }
114    global.insert(
115        "Console".to_string(),
116        Value::Namespace {
117            name: "Console".to_string(),
118            members,
119        },
120    );
121}
122
123pub const DECLARED_EFFECTS: &[&str] = &[
124    "Console.print",
125    "Console.error",
126    "Console.warn",
127    "Console.readLine",
128];
129
130pub fn effects(name: &str) -> &'static [&'static str] {
131    match name {
132        "Console.print" => &["Console.print"],
133        "Console.error" => &["Console.error"],
134        "Console.warn" => &["Console.warn"],
135        "Console.readLine" => &["Console.readLine"],
136        _ => &[],
137    }
138}
139
140/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
141pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
142    match name {
143        "Console.print" => Some(one_msg(name, args, write_stdout)),
144        "Console.error" => Some(one_msg(name, args, write_stderr_plain)),
145        "Console.warn" => Some(one_msg(name, args, write_stderr_warn)),
146        "Console.readLine" => Some(read_line(args)),
147        _ => None,
148    }
149}
150
151// ─── Private helpers ──────────────────────────────────────────────────────────
152
153fn one_msg(name: &str, args: &[Value], emit: impl Fn(&str)) -> Result<Value, RuntimeError> {
154    if args.len() != 1 {
155        return Err(RuntimeError::Error(format!(
156            "{}() takes 1 argument, got {}",
157            name,
158            args.len()
159        )));
160    }
161    if let Some(s) = aver_display(&args[0]) {
162        emit(&s);
163    }
164    Ok(Value::Unit)
165}
166
167fn read_line(args: &[Value]) -> Result<Value, RuntimeError> {
168    if !args.is_empty() {
169        return Err(RuntimeError::Error(format!(
170            "Console.readLine() takes 0 arguments, got {}",
171            args.len()
172        )));
173    }
174    match aver_rt::read_line() {
175        Ok(line) => Ok(Value::Ok(Box::new(Value::Str(line)))),
176        Err(e) => Ok(Value::Err(Box::new(Value::Str(e)))),
177    }
178}
179
180// ─── NanValue-native API ─────────────────────────────────────────────────────
181
182pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
183    let methods = &["print", "error", "warn", "readLine"];
184    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
185    for method in methods {
186        let idx = arena.push_builtin(&format!("Console.{}", method));
187        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
188    }
189    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
190        name: Rc::from("Console"),
191        members,
192    });
193    global.insert("Console".to_string(), NanValue::new_namespace(ns_idx));
194}
195
196pub fn call_nv(
197    name: &str,
198    args: &[NanValue],
199    arena: &mut Arena,
200) -> Option<Result<NanValue, RuntimeError>> {
201    match name {
202        "Console.print" => Some(one_msg_nv(name, args, arena, write_stdout)),
203        "Console.error" => Some(one_msg_nv(name, args, arena, write_stderr_plain)),
204        "Console.warn" => Some(one_msg_nv(name, args, arena, write_stderr_warn)),
205        "Console.readLine" => Some(read_line_nv(args, arena)),
206        _ => None,
207    }
208}
209
210fn one_msg_nv(
211    name: &str,
212    args: &[NanValue],
213    arena: &mut Arena,
214    emit: impl Fn(&str),
215) -> Result<NanValue, RuntimeError> {
216    if args.len() != 1 {
217        return Err(RuntimeError::Error(format!(
218            "{}() takes 1 argument, got {}",
219            name,
220            args.len()
221        )));
222    }
223    if let Some(s) = args[0].display(arena) {
224        emit(&s);
225    }
226    Ok(NanValue::UNIT)
227}
228
229fn read_line_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
230    if !args.is_empty() {
231        return Err(RuntimeError::Error(format!(
232            "Console.readLine() takes 0 arguments, got {}",
233            args.len()
234        )));
235    }
236    match aver_rt::read_line() {
237        Ok(line) => {
238            let inner = NanValue::new_string_value(&line, arena);
239            Ok(NanValue::new_ok_value(inner, arena))
240        }
241        Err(e) => {
242            let inner = NanValue::new_string_value(&e, arena);
243            Ok(NanValue::new_err_value(inner, arena))
244        }
245    }
246}