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, #[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')] system: Vec, #[arg(short, long, default_value = "off", default_missing_value = "stderr")] logs: Vec, #[command(subcommand)] command: Commands, #[arg(long, action)] time: bool, } #[derive(Subcommand, Debug)] pub enum Commands { Lex { #[arg(long)] file: Option, #[arg(long)] line: Option, }, Parse { #[arg(short, long)] file: Utf8PathBuf, }, Repl, ModTree { #[arg(long)] proj: Option, #[arg(long)] prefix: Option, }, Exec { #[arg(long)] proj: Option, #[arg()] code: String, }, } static mut STARTUP: Option = 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> + '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) -> 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 { Box::new(JoinHandleImpl(spawn_local(tokio::time::sleep(delay).then(|()| fut)))) } } fn main() -> io::Result { 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::>().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::(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) }) }) }