Files
orchid/orcx/src/main.rs

354 lines
12 KiB
Rust

use orchid_base::Logger;
use orchid_host::dylib::ext_dylib;
use tokio::time::Instant;
pub mod parse_folder;
mod print_mod;
mod repl;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::pin::pin;
use std::process::{Command, ExitCode};
use std::rc::Rc;
use std::time::Duration;
use async_fn_stream::try_stream;
use camino::{Utf8Path, Utf8PathBuf};
use clap::{Parser, Subcommand};
use futures::future::LocalBoxFuture;
use futures::{FutureExt, Stream, TryStreamExt, io};
use itertools::Itertools;
use orchid_base::local_interner::local_interner;
use orchid_base::{
FmtCtxImpl, Format, Snippet, SrcRange, Token, VPath, fmt, fmt_v, is, log, sym, take_first,
try_with_reporter, ttv_fmt, with_interner, with_logger, with_reporter, with_stash,
};
use orchid_host::ctx::{Ctx, JoinHandle, Spawner};
use orchid_host::execute::{ExecCtx, ExecResult};
use orchid_host::expr::ExprKind;
use orchid_host::extension::Extension;
use orchid_host::lex::lex;
use orchid_host::logger::LoggerImpl;
use orchid_host::parse::{HostParseCtxImpl, parse_item, parse_items};
use orchid_host::parsed::{ParsTokTree, ParsedModule};
use orchid_host::subprocess::ext_command;
use orchid_host::system::init_systems;
use substack::Substack;
use tokio::task::{LocalSet, spawn_local};
use crate::parse_folder::parse_folder;
use crate::repl::repl;
#[derive(Parser, Debug)]
#[command(version, about, long_about)]
pub struct Args {
#[arg(short, long, env = "ORCHID_EXTENSIONS", value_delimiter = ';')]
extension: Vec<Utf8PathBuf>,
#[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')]
system: Vec<String>,
#[arg(short, long, default_value = "off", default_missing_value = "stderr")]
logs: Vec<String>,
#[command(subcommand)]
command: Commands,
#[arg(long, action)]
time: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Lex {
#[arg(long)]
file: Option<Utf8PathBuf>,
#[arg(long)]
line: Option<String>,
},
Parse {
#[arg(short, long)]
file: Utf8PathBuf,
},
Repl,
ModTree {
#[arg(long)]
proj: Option<Utf8PathBuf>,
#[arg(long)]
prefix: Option<String>,
},
Exec {
#[arg(long)]
proj: Option<Utf8PathBuf>,
#[arg()]
code: String,
},
}
static mut STARTUP: Option<Instant> = None;
fn time_print(args: &Args, msg: &str) {
if !args.time {
return;
}
let ms = unsafe { STARTUP }.unwrap().elapsed().as_millis();
let secs = ms / 1000;
let mins = secs / 60;
if mins == 0 {
eprintln!("{secs}.{ms:>40} {msg}");
}
eprintln!("{mins}:{secs:>2}.{ms:>40} {msg}")
}
fn get_all_extensions<'a>(
args: &'a Args,
ctx: &'a Ctx,
) -> impl Stream<Item = io::Result<Extension>> + 'a {
fn not_found_error(ext_path: &Utf8Path) -> io::Error {
io::Error::new(
std::io::ErrorKind::NotFound,
format!("None of the file candidates for {ext_path} were found"),
)
}
try_stream(async |mut cx| {
for ext_path in args.extension.iter() {
let Some(file_name) = ext_path.file_name() else {
return Err(io::Error::new(
std::io::ErrorKind::IsADirectory,
format!("Extensions are always files, but {ext_path} points at a directory"),
));
};
let init = if cfg!(windows) {
if ext_path.with_extension("dll").exists() {
ext_dylib(ext_path.with_extension("dll").as_std_path(), ctx.clone()).await.unwrap()
} else if ext_path.with_extension("exe").exists() {
ext_command(Command::new(ext_path.with_extension("exe").as_os_str()), ctx.clone()).await?
} else {
return Err(not_found_error(ext_path));
}
} else {
let lib_path = ext_path.with_file_name(format!("lib{file_name}.so"));
if lib_path.exists() {
ext_dylib(lib_path.as_std_path(), ctx.clone()).await.unwrap()
} else if ext_path.exists() {
ext_command(Command::new(ext_path.as_os_str()), ctx.clone()).await?
} else {
return Err(not_found_error(ext_path));
}
};
cx.emit(Extension::new(init, ctx.clone()).await?).await;
}
Ok(cx)
})
}
fn parse_log_dest(dest: &str) -> orchid_api::LogStrategy {
if dest == "off" {
orchid_api::LogStrategy::Discard
} else if dest == "stderr" {
orchid_api::LogStrategy::Default
} else if let Some(path) = dest.strip_prefix('>') {
orchid_api::LogStrategy::File { path: path.to_string(), append: true }
} else {
orchid_api::LogStrategy::File { path: dest.to_string(), append: false }
}
}
fn get_logger(args: &Args) -> LoggerImpl {
let mut logger = LoggerImpl::default();
for cmd in &args.logs {
match cmd.split_once(">") {
None => logger.set_default(parse_log_dest(cmd)),
Some((category, dest)) => logger.set_category(category, parse_log_dest(dest)),
}
}
if !logger.has_category("warn") {
logger.set_category("warn", logger.strat("debug"));
}
if !logger.has_category("error") {
logger.set_category("error", logger.strat("warn"));
}
if !logger.has_category("msg") {
logger.set_category("msg", orchid_api::LogStrategy::Discard);
}
logger
}
struct JoinHandleImpl(tokio::task::JoinHandle<()>);
impl JoinHandle for JoinHandleImpl {
fn abort(&self) { self.0.abort() }
fn join(self: Box<Self>) -> LocalBoxFuture<'static, ()> {
Box::pin(async { self.0.await.unwrap() })
}
}
struct SpawnerImpl;
impl Spawner for SpawnerImpl {
fn spawn_obj(&self, delay: Duration, fut: LocalBoxFuture<'static, ()>) -> Box<dyn JoinHandle> {
Box::new(JoinHandleImpl(spawn_local(tokio::time::sleep(delay).then(|()| fut))))
}
}
fn main() -> io::Result<ExitCode> {
eprintln!("Orcx v0.1 is free software provided without warranty.");
// Use a 10MB stack for single-threaded, unoptimized operation
stacker::grow(10 * 1024 * 1024, || {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
let args = Args::parse();
let exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS));
let local_set = LocalSet::new();
let exit_code1 = exit_code.clone();
let logger = get_logger(&args);
let logger2 = logger.clone();
unsafe { STARTUP = Some(Instant::now()) };
let ctx = Ctx::new(SpawnerImpl, logger2);
let (signal_end_main, on_end_main) = futures::channel::oneshot::channel();
let ctx1 = ctx.clone();
local_set.spawn_local(async move {
let ctx = &ctx1;
let res = with_stash(async move {
let extensions =
get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap();
time_print(&args, "Extensions loaded");
match args.command {
Commands::Lex { file, line } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut buf = String::new();
match (&file, &line) {
(Some(file), None) => {
let mut file = File::open(file.as_std_path()).unwrap();
file.read_to_string(&mut buf).unwrap();
},
(None, Some(line)) => buf = line.clone(),
(None, None) | (Some(_), Some(_)) =>
return Err("`lex` expected exactly one of --file and --line".to_string()),
};
let lexemes = lex(is(&buf).await, sym!(usercode), &systems, ctx)
.await
.map_err(|e| e.to_string())?;
println!("{}", take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true))
},
Commands::Parse { file } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut file = File::open(file.as_std_path()).unwrap();
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
let lexemes = lex(is(&buf).await, sym!(usercode), &systems, ctx).await.unwrap();
let first = lexemes.first().ok_or("File empty!".to_string())?;
let pctx =
HostParseCtxImpl { systems: &systems, ctx: ctx.clone(), src: sym!(usercode) };
let snip = Snippet::new(first, &lexemes);
let ptree = try_with_reporter(parse_items(&pctx, Substack::Bottom, snip))
.await
.map_err(|e| e.to_string())?;
if ptree.is_empty() {
return Err(
"File empty only after parsing, but no errors were reported".to_string(),
);
}
for item in ptree {
println!("{}", take_first(&item.print(&FmtCtxImpl::default()).await, true))
}
},
Commands::Repl => repl(&args, &extensions, ctx.clone()).await?,
Commands::ModTree { proj, prefix } => {
let (mut root, _systems) = init_systems(&args.system, &extensions).await.unwrap();
if let Some(proj_path) = proj {
let path = proj_path.into_std_path_buf();
root = try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone()))
.await
.map_err(|e| e.to_string())?
}
let prefix = match prefix {
Some(pref) => VPath::parse(&pref).await,
None => VPath::new([]),
};
let root_data = root.0.read().await;
print_mod::print_mod(&root_data.root, prefix, &root_data).await;
},
Commands::Exec { proj, code } => {
let path = sym!(usercode);
let prefix_sr = SrcRange::zw(path.clone(), 0);
let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap();
if let Some(proj_path) = proj {
let path = proj_path.into_std_path_buf();
root = try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone()))
.await
.map_err(|e| e.to_string())?;
}
let mut lexemes = lex(is(code.trim()).await, path.clone(), &systems, ctx)
.await
.map_err(|e| e.to_string())?;
writeln!(
log("debug"),
"lexed: {}",
fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")
)
.await;
let parse_ctx =
HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] };
let prefix =
[is("export").await, is("let").await, is("entrypoint").await, is("=").await];
lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone())));
let snippet = Snippet::new(&lexemes[0], &lexemes);
let items =
try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
.await
.map_err(|e| e.to_string())?;
let entrypoint = ParsedModule::new(true, items);
let root = with_reporter(root.add_parsed(&entrypoint, path.clone()))
.await
.map_err(|e| e.to_string())?;
let expr = ExprKind::Const(sym!(usercode::entrypoint)).at(prefix_sr.pos());
let mut xctx = ExecCtx::new(root.clone(), expr).await;
xctx.set_gas(Some(10_000));
match xctx.execute().await {
ExecResult::Value(val, _) => {
println!("{}", take_first(&val.print(&FmtCtxImpl::default()).await, false))
},
ExecResult::Err(e, _) => println!("error: {e}"),
ExecResult::Gas(_) => println!("Ran out of gas!"),
}
},
};
Ok(())
})
.await;
if let Err(s) = res {
eprintln!("{s}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
}
signal_end_main.send(()).expect("cleanup should still be waiting");
});
let cleanup = async {
if on_end_main.await.is_err() {
return;
}
tokio::time::sleep(Duration::from_secs(2)).await;
let mut extensions = HashMap::new();
let systems = ctx.systems.read().await.values().filter_map(|v| v.upgrade()).collect_vec();
let exprs = ctx.exprs.iter().collect_vec();
for system in &systems {
extensions.insert(system.ext().name().clone(), system.ext().clone());
}
if extensions.is_empty() && systems.is_empty() && exprs.is_empty() {
return;
}
eprintln!("Shutdown is taking long. The following language constructs are still live:");
eprintln!("Extensions: {}", extensions.keys().join(", "));
for sys in &systems {
eprintln!("System: {:?} = {}", sys.id(), sys.ctor().name())
}
for (rc, expr) in &exprs {
eprintln!("{rc}x {:?} = {}", expr.id(), fmt(expr).await)
}
std::process::abort()
};
futures::future::select(
pin!(cleanup),
pin!(with_interner(local_interner(), with_logger(logger, local_set))),
)
.await;
let x = *exit_code.borrow();
Ok(x)
})
})
}