diff --git a/orchid-extension/src/lib.rs b/orchid-extension/src/lib.rs index 898b17f..7249aaa 100644 --- a/orchid-extension/src/lib.rs +++ b/orchid-extension/src/lib.rs @@ -1,6 +1,8 @@ #![allow(refining_impl_trait, reason = "Has various false-positives around lints")] use orchid_api as api; +mod streams; +pub use streams::*; mod atom; pub use atom::*; mod cmd_atom; diff --git a/orchid-extension/src/std_reqs.rs b/orchid-extension/src/std_reqs.rs index 82bba39..3b1f642 100644 --- a/orchid-extension/src/std_reqs.rs +++ b/orchid-extension/src/std_reqs.rs @@ -1,3 +1,4 @@ +use std::io; use std::num::NonZero; use chrono::{DateTime, Utc}; @@ -58,6 +59,20 @@ pub struct IoError { pub message: String, pub kind: IoErrorKind, } +impl From for IoError { + fn from(value: io::Error) -> Self { + Self { + message: value.to_string(), + kind: match value.kind() { + io::ErrorKind::Interrupted + | io::ErrorKind::BrokenPipe + | io::ErrorKind::NetworkDown + | io::ErrorKind::ConnectionReset => IoErrorKind::Interrupted, + _ => IoErrorKind::Other, + }, + } + } +} #[derive(Clone, Debug, Coding)] pub enum ReadLimit { @@ -69,7 +84,9 @@ pub enum ReadLimit { /// Read all available data from a stream. If the returned vector is empty, the /// stream has reached its end. #[derive(Clone, Debug, Coding, Hierarchy)] -pub struct ReadReq(pub ReadLimit); +pub struct ReadReq { + pub limit: ReadLimit, +} impl Request for ReadReq { type Response = Result, IoError>; } diff --git a/orchid-extension/src/streams.rs b/orchid-extension/src/streams.rs new file mode 100644 index 0000000..3a7d498 --- /dev/null +++ b/orchid-extension/src/streams.rs @@ -0,0 +1,103 @@ +use std::borrow::Cow; +use std::io::Result; +use std::pin::Pin; +use std::rc::Rc; + +use futures::io::BufReader; +use futures::lock::Mutex; +use futures::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use never::Never; +use orchid_base::{Receipt, ReqHandle, ReqHandleExt}; + +use crate::gen_expr::{GExpr, new_atom}; +use crate::std_reqs::{CloseReq, FlushReq, OutputReq, ReadLimit, ReadReq, WriteReq}; +use crate::{Atomic, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, ToExpr}; + +struct WriterState { + buf: Vec, + writer: Pin>, +} + +pub struct OrcWriter(T); +impl ToExpr for OrcWriter { + async fn to_gen(self) -> GExpr { + new_atom(WriterAtom(Rc::new(Mutex::new(WriterState { + buf: Vec::new(), + writer: Box::pin(self.0), + })))) + } +} + +#[derive(Clone)] +pub struct WriterAtom(Rc>); +impl Atomic for WriterAtom { + type Variant = OwnedVariant; + type Data = (); + fn reg_methods() -> MethodSetBuilder { MethodSetBuilder::new().handle::() } +} +impl OwnedAtom for WriterAtom { + type Refs = Never; + async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) } +} +impl Supports for WriterAtom { + async fn handle<'a>( + &self, + hand: Box + '_>, + req: OutputReq, + ) -> Result> { + match req { + OutputReq::WriteReq(ref wr @ WriteReq { ref data }) => { + self.0.lock().await.buf.extend(data); + hand.reply(wr, &Ok(())).await + }, + OutputReq::FlushReq(ref fr @ FlushReq) => { + let mut g = self.0.lock().await; + let WriterState { buf, writer } = &mut *g; + hand.reply(fr, &writer.write_all(&buf[..]).await.map_err(|e| e.into())).await + }, + OutputReq::CloseReq(ref cr @ CloseReq) => + hand.reply(cr, &self.0.lock().await.writer.close().await.map_err(|e| e.into())).await, + } + } +} + +pub struct OrcReader(T); +impl ToExpr for OrcReader { + async fn to_gen(self) -> GExpr { + new_atom(ReaderAtom(Rc::new(Mutex::new(BufReader::new(Box::pin(self.0)))))) + } +} + +#[derive(Clone)] +pub struct ReaderAtom(Rc>>>>); +impl Atomic for ReaderAtom { + type Variant = OwnedVariant; + type Data = (); + fn reg_methods() -> MethodSetBuilder { MethodSetBuilder::new().handle::() } +} +impl OwnedAtom for ReaderAtom { + type Refs = Never; + async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) } +} +impl Supports for ReaderAtom { + async fn handle<'a>( + &self, + hand: Box + '_>, + req: ReadReq, + ) -> Result> { + let mut buf = Vec::new(); + let mut reader = self.0.lock().await; + let rep = match match req.limit { + ReadLimit::End => reader.read_to_end(&mut buf).await.map(|_| ()), + ReadLimit::Delimiter(b) => reader.read_until(b, &mut buf).await.map(|_| ()), + ReadLimit::Length(n) => { + buf = vec![0u8; n.get() as usize]; + reader.read_exact(&mut buf).await + }, + } { + Err(e) => Err(e.into()), + Ok(()) => Ok(buf), + }; + hand.reply(&req, &rep).await + } +} diff --git a/orchid-extension/src/system_card.rs b/orchid-extension/src/system_card.rs index 19e958e..fb02dd3 100644 --- a/orchid-extension/src/system_card.rs +++ b/orchid-extension/src/system_card.rs @@ -5,7 +5,10 @@ use std::num::NonZero; use orchid_api_traits::Coding; use orchid_base::BoxedIter; -use crate::{AtomOps, AtomTypeId, Atomic, AtomicFeatures, Fun, Lambda, Replier, SystemCtor}; +use crate::{ + AtomOps, AtomTypeId, Atomic, AtomicFeatures, CmdAtom, Fun, Lambda, ReaderAtom, Replier, + SystemCtor, WriterAtom, +}; /// Description of a system. This is intended to be a ZST storing the static /// properties of a [SystemCtor] which should be known to foreign systems @@ -56,5 +59,13 @@ pub(crate) trait DynSystemCardExt: DynSystemCard { /// The indices of these are bitwise negated, such that the MSB of an atom index /// marks whether it belongs to this package (0) or the importer (1) pub(crate) fn general_atoms() -> impl Iterator>> { - [Some(Fun::ops()), Some(Lambda::ops()), Some(Replier::ops())].into_iter() + [ + Some(Fun::ops()), + Some(Lambda::ops()), + Some(Replier::ops()), + Some(CmdAtom::ops()), + Some(WriterAtom::ops()), + Some(ReaderAtom::ops()), + ] + .into_iter() } diff --git a/orchid-std/src/std/stream/stream_cmds.rs b/orchid-std/src/std/stream/stream_cmds.rs index d0f4bbd..c4e5f4b 100644 --- a/orchid-std/src/std/stream/stream_cmds.rs +++ b/orchid-std/src/std/stream/stream_cmds.rs @@ -31,7 +31,7 @@ impl Supports for ReadStreamCmd { hand: Box + '_>, req: RunCommand, ) -> io::Result> { - let ret = match self.hand.call(ReadReq(self.limit.clone())).await { + let ret = match self.hand.call(ReadReq { limit: self.limit.clone() }).await { None => Err(mk_errv( is("Atom is not readable").await, format!("Expected a readable stream handle, found {}", fmt(&self.hand).await), diff --git a/orcx/Cargo.toml b/orcx/Cargo.toml index 11b07cf..2dfa31a 100644 --- a/orcx/Cargo.toml +++ b/orcx/Cargo.toml @@ -2,20 +2,21 @@ name = "orcx" version = "0.1.0" edition = "2024" +authors = ["Lawrence Bethlenfalvy "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" } camino = "1.2.2" -clap = { version = "4.5.54", features = ["derive", "env"] } +clap = { version = "4.5.54", features = ["derive", "env", "cargo"] } ctrlc = "3.5.1" futures = "0.3.31" itertools = "0.14.0" orchid-api = { version = "0.1.0", path = "../orchid-api" } orchid-base = { version = "0.1.0", path = "../orchid-base" } orchid-host = { version = "0.1.0", path = "../orchid-host", features = [ - "tokio", + "tokio", ] } stacker = "0.1.23" substack = "1.1.1" diff --git a/orcx/src/main.rs b/orcx/src/main.rs index 9d4af37..17a13fa 100644 --- a/orcx/src/main.rs +++ b/orcx/src/main.rs @@ -41,46 +41,125 @@ use tokio::task::{LocalSet, spawn_local}; use crate::parse_folder::parse_folder; use crate::repl::repl; +/// Native interpreter for the Orchid programming language #[derive(Parser, Debug)] -#[command(version, about, long_about)] +#[command(version, about, long_about, verbatim_doc_comment)] pub struct Args { - #[arg(short, long, env = "ORCHID_EXTENSIONS", value_delimiter = ';')] + /// Load an extension from a file. The file extension should be omitted, the + /// loader checks for a range of platform-specific file extensions (foo.exe + /// and foo.dll on Windows, libfoo.so or foo on other platforms) + #[arg( + short, + long, + env = "ORCHID_EXTENSIONS", + value_delimiter = ';', + next_line_help = true, + verbatim_doc_comment + )] extension: Vec, - #[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')] + /// Instantiate a system by name. The system must be provided by one of the + /// loaded extensions + #[arg( + short, + long, + env = "ORCHID_DEFAULT_SYSTEMS", + value_delimiter = ';', + next_line_help = true, + verbatim_doc_comment + )] system: Vec, - #[arg(short, long, default_value = "off", default_missing_value = "stderr")] + /// Send a log stream to a specific destination. + /// + /// Supported formats: + /// - `--logs=msg>messaging.log`: the log channel `msg` will be routed to the + /// file `messaging.log`. If this is used, the following format should also + /// appear to specify what happens to other categories + /// - `--logs=orchid.log`: all unspecified channels will be routed to the file + /// `orchid.log` + /// - `--logs` only once with no value: all unspecified channels will be + /// routed to stderr + /// + /// Some destination names have special meanings: + /// - `stderr` designates the platform-specific standard error output of the + /// interpreter process + /// - `off` discards received messages + /// + /// Defaults for specific channels + /// - `warn` is routed the same as `debug` + /// - `error` is routed the same as `warn` + /// - `msg` is discarded (routed to `off`) + #[arg( + short, + long, + default_value = "off", + default_missing_value = "stderr", + next_line_help = true, + verbatim_doc_comment + )] logs: Vec, - #[command(subcommand)] - command: Commands, + /// Measure and report the timings of various events #[arg(long, action)] time: bool, + /// Project folder for subcommand-specific purpose + #[arg(long)] + proj: Option, + /// Number of execution steps the interpreter is allowed to take. Use + /// `--no-gas` to disable the limit. + #[arg(long, default_value("10000"), next_line_help = true, verbatim_doc_comment)] + gas: u64, + /// Disable gas limiting, may cause infinite loops + #[arg(long, action)] + no_gas: bool, + #[command(subcommand)] + command: Commands, } #[derive(Subcommand, Debug)] pub enum Commands { + /// Tokenize some code using the current lexer plugins. Either --file or + /// --line, but not both, can specify the source code input + #[command(next_line_help = true, verbatim_doc_comment)] Lex { + /// Source file input #[arg(long)] file: Option, + /// Raw source code input #[arg(long)] line: Option, }, + /// Parse some code into module tree - the stage after tokenization but before + /// any execution + #[command(next_line_help = true, verbatim_doc_comment)] Parse { #[arg(short, long)] file: Utf8PathBuf, }, + /// Open an interactive shell Repl, + /// Print the module tree after parsing. This is similar to `parse`, but it + /// can traverse folders and show extension modules + #[command(next_line_help = true, verbatim_doc_comment)] ModTree { - #[arg(long)] - proj: Option, + /// Module to show #[arg(long)] prefix: Option, }, - Exec { - #[arg(long)] - proj: Option, + /// Evaluate an expression. The most obvious use case is to read project + /// metadata from scripts written in other languages. If proj is set, the + /// expression can refer to constants within this project by fully qualified + /// path + #[command(next_line_help = true, verbatim_doc_comment)] + Eval { + /// Expression to evaluate #[arg()] code: String, }, + /// Execute effectful Orchid code + Exec { + /// Entrypoint or startup command + #[arg()] + main: String, + }, } static mut STARTUP: Option = None; @@ -187,11 +266,11 @@ impl Spawner for SpawnerImpl { } 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(); + eprintln!("Orcx v{} is free software provided without warranty.", clap::crate_version!()); let exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS)); let local_set = LocalSet::new(); let exit_code1 = exit_code.clone(); @@ -248,9 +327,9 @@ fn main() -> io::Result { } }, Commands::Repl => repl(&args, &extensions, ctx.clone()).await?, - Commands::ModTree { proj, prefix } => { + Commands::ModTree { prefix } => { let (mut root, _systems) = init_systems(&args.system, &extensions).await.unwrap(); - if let Some(proj_path) = proj { + if let Some(proj_path) = args.proj { let path = proj_path.into_std_path_buf(); root = try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())) .await @@ -263,11 +342,11 @@ fn main() -> io::Result { let root_data = root.0.read().await; print_mod::print_mod(&root_data.root, prefix, &root_data).await; }, - Commands::Exec { proj, code } => { + Commands::Eval { 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 { + if let Some(proj_path) = args.proj { let path = proj_path.into_std_path_buf(); root = try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())) .await @@ -298,7 +377,9 @@ fn main() -> io::Result { .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)); + if !args.no_gas { + xctx.set_gas(Some(args.gas)); + } match xctx.execute().await { ExecResult::Value(val, _) => { println!("{}", take_first(&val.print(&FmtCtxImpl::default()).await, false)) @@ -307,6 +388,9 @@ fn main() -> io::Result { ExecResult::Gas(_) => println!("Ran out of gas!"), } }, + Commands::Exec { main: _ } => { + todo!("Integration of the command system is in-dev") + }, }; Ok(()) })