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.).
10use std::collections::HashMap;
11use std::rc::Rc;
12
13use crate::nan_value::{Arena, NanValue};
14use crate::value::{RuntimeError, Value, aver_display};
15
16pub fn register(global: &mut HashMap<String, Value>) {
17    let mut members = HashMap::new();
18    for method in &["print", "error", "warn", "readLine"] {
19        members.insert(
20            method.to_string(),
21            Value::Builtin(format!("Console.{}", method)),
22        );
23    }
24    global.insert(
25        "Console".to_string(),
26        Value::Namespace {
27            name: "Console".to_string(),
28            members,
29        },
30    );
31}
32
33pub fn effects(name: &str) -> &'static [&'static str] {
34    match name {
35        "Console.print" => &["Console.print"],
36        "Console.error" => &["Console.error"],
37        "Console.warn" => &["Console.warn"],
38        "Console.readLine" => &["Console.readLine"],
39        _ => &[],
40    }
41}
42
43/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
44pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
45    match name {
46        "Console.print" => Some(one_msg(name, args, |s| {
47            println!("{}", s);
48        })),
49        "Console.error" => Some(one_msg(name, args, |s| {
50            eprintln!("{}", s);
51        })),
52        "Console.warn" => Some(one_msg(name, args, |s| {
53            eprintln!("[warn] {}", s);
54        })),
55        "Console.readLine" => Some(read_line(args)),
56        _ => None,
57    }
58}
59
60// ─── Private helpers ──────────────────────────────────────────────────────────
61
62fn one_msg(name: &str, args: &[Value], emit: impl Fn(&str)) -> Result<Value, RuntimeError> {
63    if args.len() != 1 {
64        return Err(RuntimeError::Error(format!(
65            "{}() takes 1 argument, got {}",
66            name,
67            args.len()
68        )));
69    }
70    if let Some(s) = aver_display(&args[0]) {
71        emit(&s);
72    }
73    Ok(Value::Unit)
74}
75
76fn read_line(args: &[Value]) -> Result<Value, RuntimeError> {
77    if !args.is_empty() {
78        return Err(RuntimeError::Error(format!(
79            "Console.readLine() takes 0 arguments, got {}",
80            args.len()
81        )));
82    }
83    match aver_rt::read_line() {
84        Ok(line) => Ok(Value::Ok(Box::new(Value::Str(line)))),
85        Err(e) => Ok(Value::Err(Box::new(Value::Str(e)))),
86    }
87}
88
89// ─── NanValue-native API ─────────────────────────────────────────────────────
90
91pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
92    let methods = &["print", "error", "warn", "readLine"];
93    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
94    for method in methods {
95        let idx = arena.push_builtin(&format!("Console.{}", method));
96        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
97    }
98    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
99        name: Rc::from("Console"),
100        members,
101    });
102    global.insert("Console".to_string(), NanValue::new_namespace(ns_idx));
103}
104
105pub fn call_nv(
106    name: &str,
107    args: &[NanValue],
108    arena: &mut Arena,
109) -> Option<Result<NanValue, RuntimeError>> {
110    match name {
111        "Console.print" => Some(one_msg_nv(name, args, arena, |s| {
112            println!("{}", s);
113        })),
114        "Console.error" => Some(one_msg_nv(name, args, arena, |s| {
115            eprintln!("{}", s);
116        })),
117        "Console.warn" => Some(one_msg_nv(name, args, arena, |s| {
118            eprintln!("[warn] {}", s);
119        })),
120        "Console.readLine" => Some(read_line_nv(args, arena)),
121        _ => None,
122    }
123}
124
125fn one_msg_nv(
126    name: &str,
127    args: &[NanValue],
128    arena: &mut Arena,
129    emit: impl Fn(&str),
130) -> Result<NanValue, RuntimeError> {
131    if args.len() != 1 {
132        return Err(RuntimeError::Error(format!(
133            "{}() takes 1 argument, got {}",
134            name,
135            args.len()
136        )));
137    }
138    if let Some(s) = args[0].display(arena) {
139        emit(&s);
140    }
141    Ok(NanValue::UNIT)
142}
143
144fn read_line_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
145    if !args.is_empty() {
146        return Err(RuntimeError::Error(format!(
147            "Console.readLine() takes 0 arguments, got {}",
148            args.len()
149        )));
150    }
151    match aver_rt::read_line() {
152        Ok(line) => {
153            let inner = NanValue::new_string_value(&line, arena);
154            Ok(NanValue::new_ok_value(inner, arena))
155        }
156        Err(e) => {
157            let inner = NanValue::new_string_value(&e, arena);
158            Ok(NanValue::new_err_value(inner, arena))
159        }
160    }
161}