pub mod parse_folder; use std::cell::RefCell; use std::fs::File; use std::io::{Read, Write}; use std::process::{Command, ExitCode}; use std::rc::Rc; use async_fn_stream::try_stream; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use futures::future::LocalBoxFuture; use futures::{FutureExt, Stream, TryStreamExt, io}; use itertools::Itertools; use orchid_base::error::{try_with_reporter, with_reporter}; use orchid_base::format::{FmtCtxImpl, Format, fmt, fmt_v, take_first}; use orchid_base::interner::local_interner::local_interner; use orchid_base::interner::{is, with_interner}; use orchid_base::location::SrcRange; use orchid_base::logging::{LogStrategy, Logger, with_logger}; use orchid_base::name::{NameLike, VPath}; use orchid_base::parse::{Import, Snippet}; use orchid_base::sym; use orchid_base::tree::{Token, ttv_fmt}; 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::parse::{HostParseCtxImpl, parse_item, parse_items}; use orchid_host::parsed::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule}; use orchid_host::subprocess::ext_command; use orchid_host::system::init_systems; use orchid_host::tree::{MemberKind, Module, RootData}; use substack::Substack; use tokio::io::{AsyncBufReadExt, BufReader, stdin}; use tokio::task::{LocalSet, spawn_local}; use crate::parse_folder::parse_folder; #[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)] logs: bool, #[arg(short, long)] msg_logs: bool, #[command(subcommand)] command: Commands, } #[derive(Subcommand, Debug)] pub enum Commands { Lex { #[arg(short, long)] file: Utf8PathBuf, }, Parse { #[arg(short, long)] file: Utf8PathBuf, }, Repl, ModTree { #[arg(long)] proj: Option, #[arg(long)] prefix: Option, }, Exec { #[arg(long)] proj: Option, #[arg()] code: String, }, } fn get_all_extensions<'a>( args: &'a Args, ctx: &'a Ctx, ) -> impl Stream> + 'a { try_stream(async |mut cx| { for ext_path in args.extension.iter() { let exe = if cfg!(windows) { ext_path.with_extension("exe") } else { ext_path.clone() }; let init = ext_command(Command::new(exe.as_os_str()), ctx.clone()).await?; cx.emit(Extension::new(init, ctx.clone()).await?).await; } Ok(cx) }) } 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, fut: LocalBoxFuture<'static, ()>) -> Box { Box::new(JoinHandleImpl(spawn_local(fut))) } } #[tokio::main] async fn main() -> io::Result { eprintln!("orcx launched"); let exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS)); let local_set = LocalSet::new(); let exit_code1 = exit_code.clone(); let args = Args::parse(); let logger = Logger::new(if args.logs { LogStrategy::StdErr } else { LogStrategy::Discard }); let cx_logger = logger.clone(); let msg_logger = Logger::new(if args.msg_logs { LogStrategy::StdErr } else { LogStrategy::Discard }); local_set.spawn_local(async move { let ctx = &Ctx::new(msg_logger.clone(), SpawnerImpl); let extensions = get_all_extensions(&args, ctx).try_collect::>().await.unwrap(); match args.command { Commands::Lex { 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(); 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 Some(first) = lexemes.first() else { println!("File empty!"); return; }; let pctx = HostParseCtxImpl { systems: &systems, ctx: ctx.clone(), src: sym!(usercode) }; let snip = Snippet::new(first, &lexemes); match with_reporter(parse_items(&pctx, Substack::Bottom, snip)).await.unwrap() { Err(errv) => { eprintln!("{errv}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; }, Ok(ptree) if ptree.is_empty() => { eprintln!("File empty only after parsing, but no errors were reported"); *exit_code1.borrow_mut() = ExitCode::FAILURE; }, Ok(ptree) => for item in ptree { println!("{}", take_first(&item.print(&FmtCtxImpl::default()).await, true)) }, }; }, Commands::Repl => { let mut counter = 0; let mut imports = Vec::new(); let usercode_path = sym!(usercode); let mut stdin = BufReader::new(stdin()); loop { counter += 1; let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap(); print!("\\.> "); std::io::stdout().flush().unwrap(); let mut prompt = String::new(); stdin.read_line(&mut prompt).await.unwrap(); let name = is(&format!("_{counter}")).await; let path = usercode_path.suffix([name.clone()]).await; let mut lexemes = lex(is(prompt.trim()).await, path.clone(), &systems, ctx).await.unwrap(); let Some(discr) = lexemes.first() else { continue }; if args.logs { println!( "lexed: {}", take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true) ); } let prefix_sr = SrcRange::zw(path.clone(), 0); let process_lexemes = async |lexemes: &[ParsTokTree]| { let snippet = Snippet::new(&lexemes[0], lexemes); let parse_ctx = HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] }; match try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet)).await { Ok(items) => Some(items), Err(e) => { eprintln!("{e}"); None }, } }; let add_imports = |items: &mut Vec, imports: &[Import]| { items.extend(imports.iter().map(|import| Item::new(import.sr.clone(), import.clone()))); }; if discr.is_kw(is("import").await) { let Some(import_lines) = process_lexemes(&lexemes).await else { continue }; imports.extend(import_lines.into_iter().map(|it| match it.kind { ItemKind::Import(imp) => imp, _ => panic!("Expected imports from import line"), })); continue; } if !discr.is_kw(is("let").await) { let prefix = [is("export").await, is("let").await, name.clone(), is("=").await]; lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone()))); } let Some(mut new_lines) = process_lexemes(&lexemes).await else { continue }; let const_decl = new_lines.iter().exactly_one().expect("Multiple lines from let"); let input_sr = const_decl.sr.map_range(|_| 0..0); let const_name = match &const_decl.kind { ItemKind::Member(ParsedMember { name: const_name, .. }) => const_name.clone(), _ => panic!("Expected exactly one constant declaration from let"), }; add_imports(&mut new_lines, &imports); imports.push(Import::new(input_sr.clone(), VPath::new(path.segs()), const_name.clone())); let new_module = ParsedModule::new(true, new_lines); match with_reporter(root.add_parsed(&new_module, path.clone())).await { Ok(new) => root = new, Err(errv) => { eprintln!("{errv}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, } eprintln!("parsed"); let entrypoint = ExprKind::Const(path.suffix([const_name.clone()]).await).at(input_sr.pos()); let mut xctx = ExecCtx::new(root.clone(), entrypoint).await; eprintln!("executed"); xctx.set_gas(Some(1000)); xctx.execute().await; match xctx.result() { ExecResult::Value(val) => println!( "{const_name} = {}", take_first(&val.print(&FmtCtxImpl::default()).await, false) ), ExecResult::Err(e) => println!("error: {e}"), ExecResult::Gas(_) => println!("Ran out of gas!"), } } }, 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(); match try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())).await { Ok(r) => root = r, Err(e) => { eprintln!("{e}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, } } let prefix = match prefix { Some(pref) => VPath::parse(&pref).await, None => VPath::new([]), }; let root_data = root.0.read().await; print_mod(&root_data.root, prefix, &root_data).await; async fn print_mod(module: &Module, path: VPath, root: &RootData) { let indent = " ".repeat(path.len()); for (key, tgt) in &module.imports { match tgt { Ok(tgt) => println!("{indent}import {key} => {}", tgt.target), Err(opts) => println!( "{indent}import {key} conflicts between {}", opts.iter().map(|i| &i.target).join(" ") ), } } for (key, mem) in &module.members { let new_path = path.clone().name_with_suffix(key.clone()).to_sym().await; match mem.kind(root.ctx.clone(), &root.consts).await { MemberKind::Module(module) => { println!("{indent}module {key} {{"); print_mod(module, VPath::new(new_path.segs()), root).boxed_local().await; println!("{indent}}}") }, MemberKind::Const => { let value = root.consts.get(&new_path).expect("Missing const!"); println!("{indent}const {key} = {}", fmt(value).await) }, } } } }, Commands::Exec { proj, code } => { eprintln!("exec branch"); 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(); match try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())).await { Ok(r) => root = r, Err(e) => { eprintln!("{e}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, } } let mut lexemes = match lex(is(code.trim()).await, path.clone(), &systems, ctx).await { Ok(lexemes) => { if args.logs { println!("lexed: {}", fmt_v::(lexemes.iter()).await.join(" ")); } lexemes }, Err(e) => { eprintln!("{e}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, }; 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 entrypoint = match try_with_reporter(parse_item( &parse_ctx, Substack::Bottom, vec![], snippet, )) .await { Ok(items) => ParsedModule::new(true, items), Err(e) => { eprintln!("{e}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, }; let root = match with_reporter(root.add_parsed(&entrypoint, path.clone())).await { Err(e) => { eprintln!("{e}"); *exit_code1.borrow_mut() = ExitCode::FAILURE; return; }, Ok(new_root) => new_root, }; let expr = ExprKind::Const(sym!(usercode::entrypoint)).at(prefix_sr.pos()); let mut xctx = ExecCtx::new(root, expr).await; xctx.set_gas(Some(10_000)); xctx.execute().await; match xctx.result() { 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!"), } }, } }); with_interner(local_interner(), with_logger(cx_logger, local_set)).await; let x = *exit_code.borrow(); Ok(x) }