use std::io::{BufRead, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicU8, Ordering};
use blue_lang_runtime::session::{
frame_granting, incomplete, protocol, Config, Load, Outcome, Session, Step, Stop,
};
use blue_lang_waku::Capability;
#[derive(clap::Args, Clone, Debug, Default)]
pub struct SessionArgs {
#[arg(long = "allow", value_name = "CAPABILITY")]
allow: Vec<String>,
#[arg(long, value_name = "STEPS")]
budget: Option<usize>,
}
impl SessionArgs {
fn config(&self) -> Result<Config, String> {
let mut granted = Vec::new();
for name in &self.allow {
if name == "host" {
granted.extend(Capability::host_effects());
continue;
}
match Capability::host_effects()
.into_iter()
.find(|c| c.label() == name)
{
Some(c) => granted.push(c),
None => {
return Err(format!(
"--allow {name}: the host capabilities are host, {}",
Capability::host_effects()
.iter()
.map(|c| c.label())
.collect::<Vec<_>>()
.join(", ")
))
}
}
}
let mut config = Config::new(Box::new(
blue_lang_pkg::load_path::LoadPath::from_env().with_standard(),
));
config.frame = frame_granting(&granted);
config.budget = self.budget;
Ok(config)
}
}
pub fn serve(args: &SessionArgs) -> Result<ExitCode, String> {
let stdin = std::io::stdin();
protocol::serve(args.config()?, stdin.lock(), std::io::stdout())
.map_err(|e| format!("serve: {e}"))?;
Ok(ExitCode::SUCCESS)
}
pub fn eval(target: &str, args: &SessionArgs) -> Result<ExitCode, String> {
let mut session = Session::new(args.config()?);
let outcome = match at_point(target) {
Some((file, line, col)) => {
let text =
std::fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?;
let form = form_at(&text, line, col)
.ok_or_else(|| format!("{target}: no top-level form at that point"))?;
session.eval(&text[form], Some(&file), None)
}
None => session.eval(target, None, None),
};
let Outcome { result, out } = outcome;
print!("{out}");
match result {
Ok(value) => {
if !out.is_empty() && !out.ends_with('\n') {
println!();
}
println!("{value}");
Ok(ExitCode::SUCCESS)
}
Err(stop) => {
let _ = std::io::stdout().flush();
eprintln!("blue: {}", describe(&stop));
Ok(ExitCode::FAILURE)
}
}
}
fn at_point(target: &str) -> Option<(PathBuf, usize, usize)> {
let mut parts = target.rsplitn(3, ':');
let col = parts.next()?.parse().ok()?;
let line = parts.next()?.parse().ok()?;
let file = PathBuf::from(parts.next()?);
file.is_file().then_some((file, line, col))
}
fn form_at(text: &str, line: usize, col: usize) -> Option<std::ops::Range<usize>> {
let start = text
.split_inclusive('\n')
.take(line.checked_sub(1)?)
.map(str::len)
.sum::<usize>();
let row = &text[start..];
let offset = start
+ row
.char_indices()
.nth(col.checked_sub(1)?)
.map_or(row.len(), |(i, _)| i);
blue_lang_syntax::parse_program_tree(text)
.ok()?
.into_iter()
.find(|f| f.span.start <= offset && offset < f.span.end.max(f.span.start + 1))
.map(|f| f.span.start..f.span.end)
}
fn describe(stop: &Stop) -> String {
match stop {
Stop::Error(f) => f.message.clone(),
Stop::Volatile { reach, name } => format!(
"volatile: `{name}` reaches the host ({}), which this session does not allow; \
pass --allow {} (or --allow host) to evaluate it",
reach.label(),
reach.label()
),
Stop::Exhausted { limit } => format!("exhausted: the budget of {limit} steps ran out"),
Stop::Interrupted => "interrupted".to_string(),
}
}
static WROTE: AtomicU8 = AtomicU8::new(0);
struct Stdout;
impl Write for Stdout {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Some(last) = buf.last() {
WROTE.store(if *last == b'\n' { 1 } else { 2 }, Ordering::SeqCst);
}
std::io::stdout().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
std::io::stdout().flush()
}
}
const HELP: &str = "\
:load FILE evaluate FILE and evaluate in its context from now on
:expand CODE expand a macro call fully
:expand1 CODE expand it one step
:doc NAME a name's signature and doc
:reset forget every definition and loaded file
:quit leave (so does ctrl-d); ctrl-c stops a running evaluation";
pub fn repl(args: &SessionArgs) -> Result<ExitCode, String> {
let mut config = args.config()?;
config.live = Some(Box::new(Stdout));
let mut session = Session::new(config);
let flag = session.interrupt_flag();
sigint::install(&flag);
let interactive = std::io::stdin().is_terminal();
let mut history = history();
let mut context: Option<PathBuf> = None;
let mut buffer = String::new();
let stdin = std::io::stdin();
let mut lines = stdin.lock();
loop {
if interactive {
print!(
"{}",
if buffer.is_empty() {
"blue> "
} else {
" .. "
}
);
let _ = std::io::stdout().flush();
}
let mut line = String::new();
let read = lines.read_line(&mut line).map_err(|e| e.to_string())?;
if interactive && flag.swap(false, Ordering::SeqCst) {
buffer.clear();
println!();
continue;
}
if read == 0 && buffer.trim().is_empty() {
break;
}
buffer.push_str(&line);
if buffer.trim().is_empty() {
buffer.clear();
continue;
}
if read != 0 && !buffer.trim_start().starts_with(':') && incomplete(&buffer) {
continue;
}
let entry = std::mem::take(&mut buffer);
let entry = entry.trim();
if let Some(h) = history.as_mut() {
let _ = writeln!(h, "{entry}");
}
flag.store(false, Ordering::SeqCst);
WROTE.store(0, Ordering::SeqCst);
if let Some(command) = entry.strip_prefix(':') {
let (name, rest) = command
.split_once(char::is_whitespace)
.unwrap_or((command, ""));
let rest = rest.trim();
match name {
"quit" | "q" => break,
"help" | "h" => println!("{HELP}"),
"reset" => {
session.reset();
context = None;
}
"load" => {
let file = PathBuf::from(rest);
match session.load(&file, None, Load::Whole).result {
Ok(defined) => {
fresh_line();
println!("loaded {}: {}", file.display(), defined.join(", "));
context = Some(file);
}
Err(stop) => report(&stop),
}
}
"expand" | "expand1" => {
let step = if name == "expand" {
Step::All
} else {
Step::One
};
show(session.expand(rest, context.as_deref(), step));
}
"doc" => match session.doc(rest, context.as_deref()) {
Some(item) => {
println!(
"{} ({})",
item.signature.unwrap_or(item.name),
item.namespace
);
if let Some(doc) = item.doc {
println!(" {doc}");
}
}
None => println!("nothing named `{rest}` here"),
},
other => println!("no command :{other}; :help lists them"),
}
} else {
show(session.eval(entry, context.as_deref(), None));
}
if read == 0 {
break;
}
}
Ok(ExitCode::SUCCESS)
}
fn show(outcome: Outcome<String>) {
match outcome.result {
Ok(v) => {
fresh_line();
println!("=> {v}");
}
Err(stop) => report(&stop),
}
}
fn report(stop: &Stop) {
fresh_line();
println!("{}", describe(stop));
}
fn fresh_line() {
if WROTE.swap(0, Ordering::SeqCst) == 2 {
println!();
}
}
fn history() -> Option<std::fs::File> {
let state = std::env::var_os("XDG_STATE_HOME")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".local/state")))?;
let dir = state.join("blue");
std::fs::create_dir_all(&dir).ok()?;
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(dir.join("history"))
.ok()
}
mod sigint {
#![deny(unsafe_code)]
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
static FLAG: OnceLock<Arc<AtomicBool>> = OnceLock::new();
#[cfg(unix)]
extern "C" fn on_sigint(_: libc::c_int) {
if let Some(flag) = FLAG.get() {
flag.store(true, Ordering::SeqCst);
}
}
pub fn install(flag: &Arc<AtomicBool>) {
if FLAG.set(Arc::clone(flag)).is_err() {
return;
}
#[cfg(unix)]
#[allow(unsafe_code)]
unsafe {
libc::signal(
libc::SIGINT,
on_sigint as extern "C" fn(libc::c_int) as libc::sighandler_t,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_point_names_the_top_level_form_around_it() {
let text = "x = 1\ndef f(a)\n a + x\nend\nf(2)\n";
assert_eq!(form_at(text, 1, 1).map(|r| &text[r]), Some("x = 1"));
assert_eq!(
form_at(text, 3, 5).map(|r| &text[r]),
Some("def f(a)\n a + x\nend")
);
assert_eq!(form_at(text, 5, 2).map(|r| &text[r]), Some("f(2)"));
assert_eq!(form_at(text, 9, 1), None);
}
#[test]
fn an_expression_is_not_mistaken_for_a_point() {
assert_eq!(at_point("1 + 2"), None);
assert_eq!(at_point("no/such/file.b:1:1"), None);
}
}