Traditional route appears to work

Beginnings of dylib extensions, entirely untestted
This commit is contained in:
2026-01-12 01:38:10 +01:00
parent 32d6237dc5
commit 1a7230ce9b
40 changed files with 1560 additions and 1135 deletions

770
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,4 +7,4 @@ edition = "2024"
futures = { version = "0.3.31", features = ["std"], default-features = false }
[dev-dependencies]
test_executors = "0.3.5"
test_executors = "0.4.1"

View File

@@ -9,8 +9,8 @@ proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
quote = "1.0.40"
syn = { version = "2.0.106" }
quote = "1.0.42"
syn = { version = "2.0.112" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
proc-macro2 = "1.0.101"
proc-macro2 = "1.0.104"
itertools = "0.14.0"

View File

@@ -9,4 +9,4 @@ edition = "2024"
futures = { version = "0.3.31", features = ["std"], default-features = false }
itertools = "0.14.0"
never = "0.1.0"
ordered-float = "5.0.0"
ordered-float = "5.1.0"

View File

@@ -6,7 +6,7 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ordered-float = "5.0.0"
ordered-float = "5.1.0"
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" }
futures = { version = "0.3.31", features = ["std"], default-features = false }
@@ -14,4 +14,4 @@ itertools = "0.14.0"
unsync-pipe = { version = "0.2.0", path = "../unsync-pipe" }
[dev-dependencies]
test_executors = "0.3.5"
test_executors = "0.4.1"

View File

@@ -1,32 +1,46 @@
//! # Binary extension definition
//!
//! A binary extension is a DLL / shared object / dylib with a symbol called
//! `orchid_extension_main` which accepts a single argument of type [ExtCtx].
//! Once that is received, communication continuees through the channel with the
//! same protocol outlined in [crate::proto]
//! `orchid_extension_main` which accepts a single argument of type
//! [ExtensionContext]. Once that is received, communication continuees through
//! the channel with the same protocol outlined in [crate::proto]
use unsync_pipe::{Reader, Writer};
/// !Send !Sync owned waker
///
/// This object is [Clone] for convenience but it has `drop` and no `clone` so
/// interactions must reflect a single logical owner
#[derive(Clone, Copy)]
#[repr(C)]
pub struct OwnedWakerVT {
data: *const (),
pub data: *const (),
/// `self`
drop: extern "C" fn(*const ()),
pub drop: extern "C" fn(*const ()),
/// `self`
pub wake: extern "C" fn(*const ()),
/// `&self`
wake: extern "C" fn(*const ()),
pub wake_ref: extern "C" fn(*const ()),
}
/// !Send !Sync, equivalent to `&mut Context<'a>`, hence no `drop`.
/// When received in [FutureVT::poll], it must not outlive the call.
///
/// You cannot directly wake using this waker, because such a trampoline would
/// pass through the binary interface twice for no reason. An efficient
/// implementation should implement that trampoline action internally, whereas
/// an inefficient but compliant implementation can clone a fresh waker and use
/// it up.
#[derive(Clone, Copy)]
#[repr(C)]
pub struct FutureContextVT {
data: *const (),
pub data: *const (),
/// `&self`
waker: extern "C" fn(*const ()) -> OwnedWakerVT,
pub waker: extern "C" fn(*const ()) -> OwnedWakerVT,
}
/// ABI-stable `Poll<()>`
#[derive(Clone, Copy)]
#[repr(C)]
pub enum UnitPoll {
Pending,
@@ -34,34 +48,43 @@ pub enum UnitPoll {
}
/// ABI-stable `Pin<Box<dyn Future<Output = ()>>>`
///
/// This object is [Clone] for convenience, but it has `drop` and no `clone` so
/// interactions must reflect a single logical owner
#[derive(Clone, Copy)]
#[repr(C)]
pub struct FutureVT {
data: *const (),
pub data: *const (),
/// `self`
drop: extern "C" fn(*const ()),
pub drop: extern "C" fn(*const ()),
/// `&mut self` Equivalent to [Future::poll]
poll: extern "C" fn(*const (), FutureContextVT) -> UnitPoll,
pub poll: extern "C" fn(*const (), FutureContextVT) -> UnitPoll,
}
/// Owned extension context.
/// Handle for a runtime that allows its holder to spawn futures across dynamic
/// library boundaries
#[derive(Clone, Copy)]
#[repr(C)]
pub struct Spawner {
pub data: *const (),
/// `self`
pub drop: extern "C" fn(*const ()),
/// `&self` Add a future to this extension's task
pub spawn: extern "C" fn(*const (), FutureVT),
}
/// Extension context.
///
/// When an extension starts, this is passed to
/// This struct is a plain old value, all of the contained values have a
/// distinct `drop` member
#[repr(C)]
pub struct ExtensionContext {
data: *const (),
/// `self`
drop: extern "C" fn(*const ()),
/// `self` upgrade to a later version of this struct. May only be called if
/// none of the other elements have been used yet. If a newer version isn't
/// supported, the server must return null, otherwise the the return value is
/// a pointer to the immediate next version of this struct
next: extern "C" fn(*const ()) -> *const (),
/// `&self` Add a future to this extension's task
spawn: extern "C" fn(*const (), FutureVT),
/// Spawns tasks associated with this extension
pub spawner: Spawner,
/// serialized [crate::HostExtChannel]
input: Reader,
pub input: Reader,
/// serialized [crate::ExtHostChannel]
output: Writer,
pub output: Writer,
/// UTF-8 log stream directly to log service.
log: Writer,
pub log: Writer,
}

View File

@@ -1,14 +1,30 @@
use std::collections::HashMap;
use orchid_api_derive::{Coding, Hierarchy};
use crate::ExtHostNotif;
use crate::{ExtHostNotif, TStr};
/// Describes what to do with a log stream.
/// Log streams are unstructured utf8 text unless otherwise stated.
#[derive(Clone, Debug, Coding, PartialEq, Eq, Hash)]
pub enum LogStrategy {
StdErr,
File(String),
/// Context-dependent default stream, often stderr
Default,
/// A file on the local filesystem
File { path: String, append: bool },
/// Discard any log output
Discard,
}
#[derive(Clone, Debug, Coding)]
pub struct Logger {
pub routing: HashMap<String, LogStrategy>,
pub default: Option<LogStrategy>,
}
#[derive(Clone, Debug, Coding, Hierarchy)]
#[extends(ExtHostNotif)]
pub struct Log(pub String);
pub struct Log {
pub category: TStr,
pub message: String,
}

View File

@@ -34,23 +34,18 @@ use crate::{Sweeped, atom, expr, interner, lexer, logging, parser, system, tree}
static HOST_INTRO: &[u8] = b"Orchid host, binary API v0\n";
#[derive(Clone, Debug)]
pub struct HostHeader {
pub log_strategy: logging::LogStrategy,
pub msg_logs: logging::LogStrategy,
pub logger: logging::Logger,
}
impl Decode for HostHeader {
async fn decode<R: AsyncRead + ?Sized>(mut read: Pin<&mut R>) -> io::Result<Self> {
read_exact(read.as_mut(), HOST_INTRO).await?;
Ok(Self {
log_strategy: logging::LogStrategy::decode(read.as_mut()).await?,
msg_logs: logging::LogStrategy::decode(read.as_mut()).await?,
})
Ok(Self { logger: logging::Logger::decode(read).await? })
}
}
impl Encode for HostHeader {
async fn encode<W: AsyncWrite + ?Sized>(&self, mut write: Pin<&mut W>) -> io::Result<()> {
write.write_all(HOST_INTRO).await?;
self.log_strategy.encode(write.as_mut()).await?;
self.msg_logs.encode(write.as_mut()).await
self.logger.encode(write.as_mut()).await
}
}
@@ -159,19 +154,19 @@ impl MsgSet for HostMsgSet {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use orchid_api_traits::enc_vec;
use ordered_float::NotNan;
use test_executors::spin_on;
use super::*;
use crate::Logger;
#[test]
fn host_header_enc() {
spin_on(async {
let hh = HostHeader {
log_strategy: logging::LogStrategy::File("SomeFile".to_string()),
msg_logs: logging::LogStrategy::File("SomeFile".to_string()),
};
let hh = HostHeader { logger: Logger { routing: HashMap::new(), default: None } };
let mut enc = &enc_vec(&hh)[..];
eprintln!("Encoded to {enc:?}");
HostHeader::decode(Pin::new(&mut enc)).await.unwrap();

View File

@@ -13,7 +13,7 @@ bound = "0.6.0"
derive_destructure = "1.0.0"
dyn-clone = "1.0.20"
futures = { version = "0.3.31", features = ["std"], default-features = false }
hashbrown = "0.16.0"
hashbrown = "0.16.1"
itertools = "0.14.0"
lazy_static = "1.5.0"
never = "0.1.0"
@@ -21,13 +21,13 @@ num-traits = "0.2.19"
orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
ordered-float = "5.0.0"
regex = "1.11.2"
rust-embed = "8.7.2"
ordered-float = "5.1.0"
regex = "1.12.2"
rust-embed = "8.9.0"
substack = "1.1.1"
trait-set = "0.3.0"
task-local = "0.1.0"
[dev-dependencies]
futures = "0.3.31"
test_executors = "0.4.0"
test_executors = "0.4.1"

118
orchid-base/src/binary.rs Normal file
View File

@@ -0,0 +1,118 @@
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use orchid_api::binary::{FutureContextVT, FutureVT, OwnedWakerVT, UnitPoll};
type WideBox = Box<dyn Future<Output = ()>>;
static OWNED_VTABLE: RawWakerVTable = RawWakerVTable::new(
|data| {
let data = unsafe { Rc::<OwnedWakerVT>::from_raw(data as *const _) };
let val = RawWaker::new(Rc::into_raw(data.clone()) as *const (), &OWNED_VTABLE);
// Clone must create a duplicate of the Rc, so it has to be un-leaked, cloned,
// then leaked again.
let _ = Rc::into_raw(data);
val
},
|data| {
// Wake must awaken the task and then clean up the state, so the waker must be
// un-leaked
let data = unsafe { Rc::<OwnedWakerVT>::from_raw(data as *const _) };
(data.wake)(data.data);
},
|data| {
// Wake-by-ref must awaken the task while preserving the future, so the Rc is
// untouched
let data = unsafe { (data as *const OwnedWakerVT).as_ref() }.unwrap();
(data.wake_ref)(data.data);
},
|data| {
// Drop must clean up the state, so the waker must be un-leaked
let data = unsafe { Rc::<OwnedWakerVT>::from_raw(data as *const _) };
(data.drop)(data.data);
},
);
struct BorrowedWakerData<'a> {
go_around: &'a mut bool,
cx: FutureContextVT,
}
static BORROWED_VTABLE: RawWakerVTable = RawWakerVTable::new(
|data| {
let data = unsafe { (data as *mut BorrowedWakerData).as_mut() }.unwrap();
let owned_data = Rc::<OwnedWakerVT>::new((data.cx.waker)(data.cx.data));
RawWaker::new(Rc::into_raw(owned_data) as *const (), &OWNED_VTABLE)
},
|data| *unsafe { (data as *mut BorrowedWakerData).as_mut() }.unwrap().go_around = true,
|data| *unsafe { (data as *mut BorrowedWakerData).as_mut() }.unwrap().go_around = true,
|_data| {},
);
/// Convert a future to a binary-compatible format that can be sent across
/// dynamic library boundaries
pub fn future_to_vt<Fut: Future<Output = ()> + 'static>(fut: Fut) -> FutureVT {
let wide_box = Box::new(fut) as WideBox;
let data = Box::into_raw(Box::new(wide_box));
extern "C" fn drop(raw: *const ()) {
std::mem::drop(unsafe { Box::<WideBox>::from_raw(raw as *mut _) })
}
extern "C" fn poll(raw: *const (), cx: FutureContextVT) -> UnitPoll {
let mut this = unsafe { Pin::new_unchecked(&mut **(raw as *mut WideBox).as_mut().unwrap()) };
loop {
let mut go_around = false;
let borrowed_waker = unsafe {
Waker::from_raw(RawWaker::new(
&mut BorrowedWakerData { go_around: &mut go_around, cx } as *mut _ as *const (),
&BORROWED_VTABLE,
))
};
let mut ctx = Context::from_waker(&borrowed_waker);
let result = this.as_mut().poll(&mut ctx);
if matches!(result, Poll::Ready(())) {
break UnitPoll::Ready;
}
if !go_around {
break UnitPoll::Pending;
}
}
}
FutureVT { data: data as *const _, drop, poll }
}
struct VirtualFuture {
vt: FutureVT,
}
impl Unpin for VirtualFuture {}
impl Future for VirtualFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
extern "C" fn waker(raw: *const ()) -> OwnedWakerVT {
let waker = unsafe { (raw as *mut Context).as_mut() }.unwrap().waker().clone();
let data = Box::into_raw(Box::<Waker>::new(waker)) as *const ();
return OwnedWakerVT { data, drop, wake, wake_ref };
extern "C" fn drop(raw: *const ()) {
std::mem::drop(unsafe { Box::<Waker>::from_raw(raw as *mut Waker) })
}
extern "C" fn wake(raw: *const ()) {
unsafe { Box::<Waker>::from_raw(raw as *mut Waker) }.wake();
}
extern "C" fn wake_ref(raw: *const ()) {
unsafe { (raw as *mut Waker).as_mut() }.unwrap().wake_by_ref();
}
}
let cx = FutureContextVT { data: cx as *mut Context as *const (), waker };
let result = (self.vt.poll)(self.vt.data, cx);
match result {
UnitPoll::Pending => Poll::Pending,
UnitPoll::Ready => Poll::Ready(()),
}
}
}
impl Drop for VirtualFuture {
fn drop(&mut self) { (self.vt.drop)(self.vt.data) }
}
/// Receive a future sent across dynamic library boundaries and convert it into
/// an owned object
pub fn vt_to_future(vt: FutureVT) -> impl Future<Output = ()> { VirtualFuture { vt } }

View File

@@ -128,6 +128,7 @@ impl OrcErrv {
pub async fn from_api<'a>(api: impl IntoIterator<Item = &'a api::OrcError>) -> Self {
Self(join_all(api.into_iter().map(OrcErr::from_api)).await)
}
pub fn iter(&self) -> impl Iterator<Item = OrcErr> + '_ { self.0.iter().cloned() }
}
impl From<OrcErr> for OrcErrv {
fn from(value: OrcErr) -> Self { Self(vec![value]) }

View File

@@ -1,6 +1,7 @@
pub use async_once_cell;
use orchid_api as api;
pub mod binary;
pub mod box_cow;
pub mod boxed_iter;
pub mod char_filter;

View File

@@ -1,47 +1,74 @@
use std::any::Any;
use std::cell::RefCell;
use std::fmt::Arguments;
use std::fs::File;
use std::io::{Write, stderr};
use std::io::Write;
use std::rc::Rc;
pub use api::LogStrategy;
use itertools::Itertools;
use futures::future::LocalBoxFuture;
use task_local::task_local;
use crate::api;
#[derive(Clone)]
pub struct Logger(api::LogStrategy);
impl Logger {
pub fn new(strat: api::LogStrategy) -> Self { Self(strat) }
pub fn log(&self, msg: impl AsRef<str>) { writeln!(self, "{}", msg.as_ref()) }
pub fn strat(&self) -> api::LogStrategy { self.0.clone() }
pub fn is_active(&self) -> bool { !matches!(self.0, api::LogStrategy::Discard) }
pub fn log_buf(&self, event: impl AsRef<str>, buf: &[u8]) {
if std::env::var("ORCHID_LOG_BUFFERS").is_ok_and(|v| !v.is_empty()) {
writeln!(self, "{}: [{}]", event.as_ref(), buf.iter().map(|b| format!("{b:02x}")).join(" "))
}
}
pub fn write_fmt(&self, fmt: Arguments) {
match &self.0 {
api::LogStrategy::Discard => (),
api::LogStrategy::StdErr => {
stderr().write_fmt(fmt).expect("Could not write to stderr!");
stderr().flush().expect("Could not flush stderr")
},
api::LogStrategy::File(f) => {
let mut file = (File::options().write(true).create(true).truncate(true).open(f))
.expect("Could not open logfile");
file.write_fmt(fmt).expect("Could not write to logfile");
},
}
task_local! {
static DEFAULT_WRITER: RefCell<Box<dyn Write>>
}
/// Set the stream used for [api::LogStrategy::Default]. If not set,
/// [std::io::stderr] will be used.
pub async fn with_default_stream<F: Future>(stderr: impl Write + 'static, fut: F) -> F::Output {
DEFAULT_WRITER.scope(RefCell::new(Box::new(stderr)), fut).await
}
pub trait LogWriter {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()>;
}
pub trait Logger: Any {
fn writer(&self, category: &str) -> Rc<dyn LogWriter>;
fn strat(&self, category: &str) -> api::LogStrategy;
fn is_active(&self, category: &str) -> bool {
!matches!(self.strat(category), api::LogStrategy::Discard)
}
}
task_local! {
static LOGGER: Logger;
static LOGGER: Rc<dyn Logger>;
}
pub async fn with_logger<F: Future>(logger: Logger, fut: F) -> F::Output {
LOGGER.scope(logger, fut).await
pub async fn with_logger<F: Future>(logger: impl Logger + 'static, fut: F) -> F::Output {
LOGGER.scope(Rc::new(logger), fut).await
}
pub fn logger() -> Logger { LOGGER.try_with(|l| l.clone()).expect("Logger not set!") }
pub fn log(category: &str) -> Rc<dyn LogWriter> {
LOGGER.try_with(|l| l.writer(category)).expect("Logger not set!")
}
pub fn get_logger() -> Rc<dyn Logger> { LOGGER.try_with(|l| l.clone()).expect("Logger not set!") }
pub mod test {
use std::fmt::Arguments;
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use crate::clone;
use crate::logging::{LogWriter, Logger};
#[derive(Clone)]
pub struct TestLogger(Rc<dyn Fn(String) -> LocalBoxFuture<'static, ()>>);
impl LogWriter for TestLogger {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
(self.0)(fmt.to_string())
}
}
impl Logger for TestLogger {
fn strat(&self, _category: &str) -> orchid_api::LogStrategy { orchid_api::LogStrategy::Default }
fn writer(&self, _category: &str) -> std::rc::Rc<dyn LogWriter> { Rc::new(self.clone()) }
}
impl TestLogger {
pub fn new(f: impl AsyncFn(String) + 'static) -> Self {
let f = Rc::new(f);
Self(Rc::new(move |s| clone!(f; Box::pin(async move { f(s).await }))))
}
}
}

View File

@@ -1,5 +1,4 @@
use std::cell::RefCell;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::{Pin, pin};
use std::rc::Rc;
@@ -316,13 +315,25 @@ impl CommCtx {
pub fn io_comm(
o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>,
i: Mutex<Pin<Box<dyn AsyncRead>>>,
notif: impl for<'a> AsyncFn(Box<dyn MsgReader<'a> + 'a>) -> io::Result<()>,
req: impl for<'a> AsyncFn(Box<dyn ReqReader<'a> + 'a>) -> io::Result<Receipt<'a>>,
) -> (impl Client + 'static, CommCtx, impl Future<Output = io::Result<()>>) {
) -> (impl Client + 'static, CommCtx, IoCommServer) {
let i = Rc::new(i);
let (onsub, client) = IoClient::new(o.clone());
let (exit, onexit) = channel(1);
(client, CommCtx { exit }, async move {
(client, CommCtx { exit }, IoCommServer { o, i, onsub, onexit })
}
pub struct IoCommServer {
o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>,
i: Rc<Mutex<Pin<Box<dyn AsyncRead>>>>,
onsub: Receiver<ReplySub>,
onexit: Receiver<()>,
}
impl IoCommServer {
pub async fn listen(
self,
notif: impl for<'a> AsyncFn(Box<dyn MsgReader<'a> + 'a>) -> io::Result<()>,
req: impl for<'a> AsyncFn(Box<dyn ReqReader<'a> + 'a>) -> io::Result<Receipt<'a>>,
) -> io::Result<()> {
let Self { o, i, onexit, onsub } = self;
enum Event {
Input(u64, IoGuard<dyn AsyncRead>),
Sub(ReplySub),
@@ -363,6 +374,9 @@ pub fn io_comm(
Err(e) => break 'body Err(e),
Ok(Event::Exit) => {
*exiting.borrow_mut() = true;
let mut out = o.lock().await;
out.as_mut().flush().await?;
out.as_mut().close().await?;
break;
},
Ok(Event::Sub(ReplySub { id, ack, cb })) => {
@@ -399,11 +413,12 @@ pub fn io_comm(
next?
}
Ok(())
})
}
}
#[cfg(test)]
mod test {
use std::cell::RefCell;
use std::rc::Rc;
use futures::channel::mpsc;
@@ -414,6 +429,8 @@ mod test {
use test_executors::spin_on;
use unsync_pipe::pipe;
use crate::logging::test::TestLogger;
use crate::logging::with_logger;
use crate::reqnot::{ClientExt, MsgReaderExt, ReqReaderExt, io_comm};
#[derive(Clone, Debug, PartialEq, Coding, Hierarchy)]
@@ -422,33 +439,36 @@ mod test {
#[test]
fn notification() {
spin_on(async {
let logger = TestLogger::new(async |s| eprint!("{s}"));
spin_on(with_logger(logger, async {
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (received, mut on_receive) = mpsc::channel(2);
let (_, recv_ctx, run_recv) = io_comm(
Rc::new(Mutex::new(Box::pin(in2))),
Mutex::new(Box::pin(out2)),
async |notif| {
received.clone().send(notif.read::<TestNotif>().await?).await.unwrap();
Ok(())
let (_, recv_ctx, recv_srv) =
io_comm(Rc::new(Mutex::new(Box::pin(in2))), Mutex::new(Box::pin(out2)));
let (sender, ..) = io_comm(Rc::new(Mutex::new(Box::pin(in1))), Mutex::new(Box::pin(out1)));
join!(
async {
recv_srv
.listen(
async |notif| {
received.clone().send(notif.read::<TestNotif>().await?).await.unwrap();
Ok(())
},
async |_| panic!("Should receive notif, not request"),
)
.await
.unwrap()
},
async |_| panic!("Should receive notif, not request"),
async {
sender.notify(TestNotif(3)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(3)));
sender.notify(TestNotif(4)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(4)));
recv_ctx.exit().await;
}
);
let (sender, ..) = io_comm(
Rc::new(Mutex::new(Box::pin(in1))),
Mutex::new(Box::pin(out1)),
async |_| panic!("Should not receive notif"),
async |_| panic!("Should not receive request"),
);
join!(async { run_recv.await.unwrap() }, async {
sender.notify(TestNotif(3)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(3)));
sender.notify(TestNotif(4)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(4)));
recv_ctx.exit().await;
});
})
}))
}
#[derive(Clone, Debug, Coding, Hierarchy)]
@@ -460,30 +480,94 @@ mod test {
#[test]
fn request() {
spin_on(async {
let logger = TestLogger::new(async |s| eprint!("{s}"));
spin_on(with_logger(logger, async {
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (_, srv_ctx, run_srv) = io_comm(
Rc::new(Mutex::new(Box::pin(in2))),
Mutex::new(Box::pin(out2)),
async |_| panic!("No notifs expected"),
async |mut req| {
let val = req.read_req::<DummyRequest>().await?;
req.reply(&val, &(val.0 + 1)).await
let (_, srv_ctx, srv) =
io_comm(Rc::new(Mutex::new(Box::pin(in2))), Mutex::new(Box::pin(out2)));
let (client, client_ctx, client_srv) =
io_comm(Rc::new(Mutex::new(Box::pin(in1))), Mutex::new(Box::pin(out1)));
join!(
async {
srv
.listen(
async |_| panic!("No notifs expected"),
async |mut req| {
let val = req.read_req::<DummyRequest>().await?;
req.reply(&val, &(val.0 + 1)).await
},
)
.await
.unwrap()
},
async {
client_srv
.listen(
async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"),
)
.await
.unwrap()
},
async {
let response = client.request(DummyRequest(5)).await.unwrap();
assert_eq!(response, 6);
srv_ctx.exit().await;
client_ctx.exit().await;
}
);
let (client, client_ctx, run_client) = io_comm(
Rc::new(Mutex::new(Box::pin(in1))),
Mutex::new(Box::pin(out1)),
async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"),
);
join!(async { run_srv.await.unwrap() }, async { run_client.await.unwrap() }, async {
let response = client.request(DummyRequest(5)).await.unwrap();
assert_eq!(response, 6);
srv_ctx.exit().await;
client_ctx.exit().await;
});
})
}))
}
#[test]
fn exit() {
let logger = TestLogger::new(async |s| eprint!("{s}"));
spin_on(with_logger(logger, async {
let (input1, output1) = pipe(1024);
let (input2, output2) = pipe(1024);
let (reply_client, reply_context, reply_server) =
io_comm(Rc::new(Mutex::new(Box::pin(input1))), Mutex::new(Box::pin(output2)));
let (req_client, req_context, req_server) =
io_comm(Rc::new(Mutex::new(Box::pin(input2))), Mutex::new(Box::pin(output1)));
let reply_context = RefCell::new(Some(reply_context));
let (exit, onexit) = futures::channel::oneshot::channel::<()>();
join!(
async move {
reply_server
.listen(
async |hand| {
let _notif = hand.read::<TestNotif>().await.unwrap();
let context = reply_context.borrow_mut().take().unwrap();
context.exit().await;
Ok(())
},
async |mut hand| {
let req = hand.read_req::<DummyRequest>().await?;
hand.reply(&req, &(req.0 + 1)).await
},
)
.await
.unwrap();
exit.send(()).unwrap();
let _client = reply_client;
},
async move {
req_server
.listen(
async |_| panic!("Only the other server expected notifs"),
async |_| panic!("Only the other server expected requests"),
)
.await
.unwrap();
let _ctx = req_context;
},
async move {
req_client.request(DummyRequest(0)).await.unwrap();
req_client.notify(TestNotif(0)).await.unwrap();
onexit.await.unwrap();
}
)
}));
}
}

View File

@@ -11,15 +11,15 @@ async-once-cell = "0.5.4"
bound = "0.6.0"
derive_destructure = "1.0.0"
dyn-clone = "1.0.20"
futures = { version = "0.3.31", features = [
futures = { version = "0.3.31", default-features = false, features = [
"std",
"async-await",
], default-features = false }
] }
futures-locks = "0.7.1"
hashbrown = "0.16.0"
hashbrown = "0.16.1"
include_dir = { version = "0.7.4", optional = true }
itertools = "0.14.0"
konst = "0.4.2"
konst = "0.4.3"
lazy_static = "1.5.0"
memo-map = "0.3.3"
never = "0.1.0"
@@ -28,12 +28,12 @@ orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
orchid-base = { version = "0.1.0", path = "../orchid-base" }
ordered-float = "5.0.0"
pastey = "0.1.1"
ordered-float = "5.1.0"
pastey = "0.2.1"
substack = "1.1.1"
task-local = "0.1.0"
tokio = { version = "1.47.1", optional = true, features = [] }
tokio-util = { version = "0.7.16", optional = true, features = ["compat"] }
tokio = { version = "1.49.0", optional = true, features = [] }
tokio-util = { version = "0.7.17", optional = true, features = ["compat"] }
trait-set = "0.3.0"

View File

@@ -19,6 +19,7 @@ use never::Never;
use orchid_api_traits::{Decode, Encode, enc_vec};
use orchid_base::error::OrcRes;
use orchid_base::format::{FmtCtx, FmtCtxImpl, FmtUnit, take_first};
use orchid_base::logging::log;
use orchid_base::name::Sym;
use task_local::task_local;
@@ -338,5 +339,5 @@ pub async fn debug_print_obj_store(show_atoms: bool) {
message += &format!("\n{k:?} -> {}", take_first(&atom.dyn_print().await, true));
}
}
eprintln!("{message}")
writeln!(log("debug"), "{message}").await
}

View File

@@ -8,7 +8,7 @@ use futures::{AsyncRead, AsyncWrite, FutureExt};
use orchid_api_traits::{Coding, enc_vec};
use orchid_base::error::OrcRes;
use orchid_base::format::FmtUnit;
use orchid_base::logging::logger;
use orchid_base::logging::log;
use orchid_base::name::Sym;
use crate::api;
@@ -89,7 +89,7 @@ impl<T: ThinAtom> AtomDynfo for ThinAtomDynfo<T> {
fn drop<'a>(&'a self, AtomCtx(buf, _): AtomCtx<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
let string_self = T::decode_slice(&mut &buf[..]).print().await;
writeln!(logger(), "Received drop signal for non-drop atom {string_self:?}");
writeln!(log("warn"), "Received drop signal for non-drop atom {string_self:?}").await;
})
}
}

View File

@@ -0,0 +1,30 @@
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use orchid_base::binary::future_to_vt;
use crate::api;
use crate::entrypoint::ExtensionBuilder;
use crate::ext_port::ExtPort;
pub type ExtCx = api::binary::ExtensionContext;
struct Spawner(api::binary::Spawner);
impl Drop for Spawner {
fn drop(&mut self) { (self.0.drop)(self.0.data) }
}
impl Spawner {
pub fn spawn(&self, fut: LocalBoxFuture<'static, ()>) {
(self.0.spawn)(self.0.data, future_to_vt(fut))
}
}
pub fn orchid_extension_main_body(cx: ExtCx, builder: ExtensionBuilder) {
let spawner = Spawner(cx.spawner);
builder.build(ExtPort {
input: Box::pin(cx.input),
output: Box::pin(cx.output),
log: Box::pin(cx.log),
spawn: Rc::new(move |fut| spawner.spawn(fut)),
});
}

View File

@@ -15,7 +15,7 @@ use orchid_api_traits::{Decode, Encode, Request, UnderRoot, enc_vec};
use orchid_base::char_filter::{char_filter_match, char_filter_union, mk_char_filter};
use orchid_base::error::try_with_reporter;
use orchid_base::interner::{es, is, with_interner};
use orchid_base::logging::{Logger, with_logger};
use orchid_base::logging::{log, with_logger};
use orchid_base::name::Sym;
use orchid_base::parse::{Comment, Snippet};
use orchid_base::reqnot::{
@@ -35,6 +35,7 @@ use crate::ext_port::ExtPort;
use crate::func_atom::with_funs_ctx;
use crate::interner::new_interner;
use crate::lexer::{LexContext, ekey_cascade, ekey_not_applicable};
use crate::logger::LoggerImpl;
use crate::parser::{PTokTree, ParsCtx, get_const, linev_into_api, with_parsed_const_ctx};
use crate::reflection::with_refl_roots;
use crate::system::{SysCtx, atom_by_idx, cted, with_sys};
@@ -58,9 +59,17 @@ pub async fn with_comm<F: Future>(c: Rc<dyn Client>, ctx: CommCtx, fut: F) -> F:
CLIENT.scope(c, CTX.scope(Rc::new(RefCell::new(Some(ctx))), fut)).await
}
task_local! {
pub static MUTE_REPLY: ();
}
/// Send a request through the global client's [ClientExt::request]
pub async fn request<T: Request + UnderRoot<Root = ExtHostReq>>(t: T) -> T::Response {
get_client().request(t).await.unwrap()
let response = get_client().request(t).await.unwrap();
if MUTE_REPLY.try_with(|b| *b).is_err() {
writeln!(log("msg"), "Got response {response:?}").await;
}
response
}
/// Send a notification through the global client's [ClientExt::notify]
@@ -79,15 +88,7 @@ task_local! {
}
async fn with_sys_record<F: Future>(id: api::SysId, fut: F) -> F::Output {
let cted = SYSTEM_TABLE.with(|tbl| {
eprintln!(
"Existing systems are {}",
tbl.borrow().iter().map(|(k, v)| format!("{k:?}={:?}", v.cted)).join(";")
);
let sys = tbl.borrow().get(&id).expect("Invalid sys ID").cted.clone();
eprintln!("Selected {:?}", sys);
sys
});
let cted = SYSTEM_TABLE.with(|tbl| tbl.borrow().get(&id).expect("Invalid sys ID").cted.clone());
with_sys(SysCtx(id, cted), fut).await
}
@@ -126,8 +127,7 @@ impl ExtensionBuilder {
self.add_context(with_lazy_member_store);
self.add_context(with_refl_roots);
(ctx.spawn)(Box::pin(async move {
let api::HostHeader { log_strategy, msg_logs } =
api::HostHeader::decode(ctx.input.as_mut()).await.unwrap();
let host_header = api::HostHeader::decode(ctx.input.as_mut()).await.unwrap();
let decls = (self.systems.iter().enumerate())
.map(|(id, sys)| (u16::try_from(id).expect("more than u16max system ctors"), sys))
.map(|(id, sys)| sys.decl(api::SysDeclId(NonZero::new(id + 1).unwrap())))
@@ -137,24 +137,26 @@ impl ExtensionBuilder {
.await
.unwrap();
ctx.output.as_mut().flush().await.unwrap();
let logger = Logger::new(log_strategy);
let logger2 = logger.clone();
let msg_logger = Logger::new(msg_logs);
let (client, ctx, run_extension) = io_comm(
Rc::new(Mutex::new(ctx.output)),
Mutex::new(ctx.input),
async move |n: Box<dyn MsgReader<'_>>| {
match n.read().await.unwrap() {
let logger1 = LoggerImpl::from_api(&host_header.logger);
let logger2 = logger1.clone();
let (client, comm_ctx, extension_srv) =
io_comm(Rc::new(Mutex::new(ctx.output)), Mutex::new(ctx.input));
let extension_fut = extension_srv.listen(
async |n: Box<dyn MsgReader<'_>>| {
let notif = n.read().await.unwrap();
match notif {
api::HostExtNotif::Exit => exit().await,
}
Ok(())
},
async move |mut reader| {
async |mut reader| {
with_stash(async {
let req = reader.read_req().await.unwrap();
let handle = reader.finish().await;
// Atom printing is never reported because it generates too much
// noise
if !matches!(req, api::HostExtReq::AtomReq(api::AtomReq::AtomPrint(_))) {
writeln!(msg_logger, "{} extension received request {req:?}", self.name);
writeln!(log("msg"), "{} extension received request {req:?}", self.name).await;
}
match req {
api::HostExtReq::SystemDrop(sys_drop) => {
@@ -266,11 +268,6 @@ impl ExtensionBuilder {
let ekey_na = ekey_not_applicable().await;
let ekey_cascade = ekey_cascade().await;
let lexers = cted().inst().dyn_lexers();
writeln!(
logger,
"sys={sys:?}, tc={trigger_char}, lexers={}",
lexers.iter().map(|l| format!("{l:?}")).join(",")
);
for lx in
lexers.iter().filter(|l| char_filter_match(l.char_filter(), trigger_char))
{
@@ -290,7 +287,7 @@ impl ExtensionBuilder {
},
}
}
writeln!(logger, "Got notified about n/a character '{trigger_char}'");
writeln!(log("warn"), "Got notified about n/a character '{trigger_char}'").await;
expr_store.dispose().await;
handle.reply(&lex, &None).await
})
@@ -423,16 +420,16 @@ impl ExtensionBuilder {
logger2,
with_comm(
Rc::new(client),
ctx,
comm_ctx,
(self.context.into_iter()).fold(
Box::pin(async move { run_extension.await.unwrap() }) as LocalBoxFuture<()>,
Box::pin(async { extension_fut.await.unwrap() }) as LocalBoxFuture<()>,
|fut, cx| cx.apply(fut),
),
),
),
),
)
.await
.await;
}) as Pin<Box<_>>);
}
}

View File

@@ -6,7 +6,7 @@ use orchid_base::interner::local_interner::{Int, StrBranch, StrvBranch};
use orchid_base::interner::{IStr, IStrv, InternerSrv};
use crate::api;
use crate::entrypoint::request;
use crate::entrypoint::{MUTE_REPLY, request};
#[derive(Default)]
struct ExtInterner {
@@ -17,7 +17,9 @@ impl InternerSrv for ExtInterner {
fn is<'a>(&'a self, v: &'a str) -> LocalBoxFuture<'a, IStr> {
match self.str.i(v) {
Ok(i) => Box::pin(ready(i)),
Err(e) => Box::pin(async { e.set_if_empty(request(api::InternStr(v.to_owned())).await) }),
Err(e) => Box::pin(async {
e.set_if_empty(MUTE_REPLY.scope((), request(api::InternStr(v.to_owned()))).await)
}),
}
}
fn es(&self, t: api::TStr) -> LocalBoxFuture<'_, IStr> {

View File

@@ -12,6 +12,7 @@ pub mod func_atom;
pub mod gen_expr;
pub mod interner;
pub mod lexer;
pub mod logger;
pub mod other_system;
pub mod parser;
pub mod reflection;
@@ -19,3 +20,4 @@ pub mod system;
pub mod system_ctor;
pub mod tokio;
pub mod tree;
pub mod binary;

View File

@@ -0,0 +1,57 @@
use std::fmt::Arguments;
use std::fs::File;
use std::io::Write;
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use hashbrown::HashMap;
use orchid_base::interner::is;
use orchid_base::logging::{LogWriter, Logger};
use crate::api;
use crate::entrypoint::notify;
pub struct LogWriterImpl {
category: String,
strat: api::LogStrategy,
}
impl LogWriter for LogWriterImpl {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
match &self.strat {
api::LogStrategy::Discard => (),
api::LogStrategy::Default =>
notify(api::Log { category: is(&self.category).await.to_api(), message: fmt.to_string() })
.await,
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
file.write_fmt(fmt).unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
})
}
}
#[derive(Clone)]
pub struct LoggerImpl {
default: Option<api::LogStrategy>,
routing: HashMap<String, api::LogStrategy>,
}
impl LoggerImpl {
pub fn from_api(api: &api::Logger) -> Self {
Self {
default: api.default.clone(),
routing: api.routing.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
}
}
}
impl Logger for LoggerImpl {
fn writer(&self, category: &str) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl { category: category.to_string(), strat: self.strat(category) })
}
fn strat(&self, category: &str) -> orchid_api::LogStrategy {
(self.routing.get(category).cloned().or(self.default.clone()))
.expect("Unrecognized log category with no default strategy")
}
}

View File

@@ -64,8 +64,7 @@ pub trait SystemCtor: Debug + Send + Sync + 'static {
type Instance: System;
const NAME: &'static str;
const VERSION: f64;
/// Create a system instance. When this function is called, a context object
/// isn't yet available
/// Create a system instance.
fn inst(&self, deps: <Self::Deps as DepDef>::Sat) -> Self::Instance;
}

View File

@@ -2,9 +2,15 @@ use std::rc::Rc;
use crate::entrypoint::ExtensionBuilder;
use crate::ext_port::ExtPort;
/// Run an extension inside a Tokio localset. Since the extension API does not
/// provide a forking mechanism, it can safely abort once the localset is
/// exhausted. If an extension absolutely needs a parallel thread, it can import
/// and call [tokio::task::spawn_local] which will keep alive the localset and
/// postpone the aggressive shutdown, and listen for the [Drop::drop] of the
/// value returned by [crate::system_ctor::SystemCtor::inst] to initiate
/// shutdown.
#[cfg(feature = "tokio")]
pub async fn tokio_main(builder: ExtensionBuilder) {
pub async fn tokio_main(builder: ExtensionBuilder) -> ! {
use tokio::io::{stderr, stdin, stdout};
use tokio::task::{LocalSet, spawn_local};
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
@@ -21,4 +27,5 @@ pub async fn tokio_main(builder: ExtensionBuilder) {
});
});
local_set.await;
std::process::exit(0)
}

View File

@@ -8,22 +8,28 @@ edition = "2024"
[dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
async-process = "2.4.0"
bound = "0.6.0"
derive_destructure = "1.0.0"
futures = { version = "0.3.31", features = ["std"], default-features = false }
futures-locks = "0.7.1"
hashbrown = "0.16.0"
hashbrown = "0.16.1"
itertools = "0.14.0"
lazy_static = "1.5.0"
libloading = { version = "0.9.0", optional = true }
memo-map = "0.3.3"
never = "0.1.0"
num-traits = "0.2.19"
orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
orchid-base = { version = "0.1.0", path = "../orchid-base" }
ordered-float = "5.0.0"
pastey = "0.1.1"
ordered-float = "5.1.0"
pastey = "0.2.1"
substack = "1.1.1"
test_executors = "0.3.5"
test_executors = "0.4.1"
tokio = { version = "1.49.0", features = ["process"], optional = true }
tokio-util = { version = "0.7.18", features = ["compat"], optional = true }
trait-set = "0.3.0"
unsync-pipe = { version = "0.2.0", path = "../unsync-pipe" }
[features]
tokio = ["dep:tokio", "dep:tokio-util", "dep:libloading"]

View File

@@ -6,10 +6,10 @@ use std::{fmt, ops};
use futures::future::LocalBoxFuture;
use futures_locks::RwLock;
use hashbrown::HashMap;
use orchid_base::logging::Logger;
use crate::api;
use crate::expr_store::ExprStore;
use crate::logger::LoggerImpl;
use crate::system::{System, WeakSystem};
use crate::tree::WeakRoot;
@@ -24,11 +24,11 @@ pub trait Spawner {
pub struct CtxData {
spawner: Rc<dyn Spawner>,
pub msg_logs: Logger,
pub systems: RwLock<HashMap<api::SysId, WeakSystem>>,
pub system_id: RefCell<NonZeroU16>,
pub exprs: ExprStore,
pub root: RwLock<WeakRoot>,
pub logger: LoggerImpl,
}
#[derive(Clone)]
pub struct Ctx(Rc<CtxData>);
@@ -46,14 +46,14 @@ impl WeakCtx {
}
impl Ctx {
#[must_use]
pub fn new(msg_logs: Logger, spawner: impl Spawner + 'static) -> Self {
pub fn new(spawner: impl Spawner + 'static, logger: LoggerImpl) -> Self {
Self(Rc::new(CtxData {
msg_logs,
spawner: Rc::new(spawner),
systems: RwLock::default(),
system_id: RefCell::new(NonZero::new(1).unwrap()),
exprs: ExprStore::default(),
root: RwLock::default(),
logger,
}))
}
/// Spawn a parallel future that you can join at any later time.

55
orchid-host/src/dylib.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use hashbrown::HashMap;
use libloading::Library;
use orchid_base::binary::vt_to_future;
use crate::api;
use crate::ctx::Ctx;
use crate::extension::ExtPort;
static DYNAMIC_LIBRARIES: Mutex<Option<HashMap<PathBuf, Arc<Library>>>> = Mutex::new(None);
fn load_dylib(path: &Path) -> Result<Arc<Library>, libloading::Error> {
let mut g = DYNAMIC_LIBRARIES.lock().unwrap();
let map = g.get_or_insert_default();
if let Some(lib) = map.get(path) {
Ok(lib.clone())
} else {
let lib = Arc::new(unsafe { Library::new(path) }?);
map.insert(path.to_owned(), lib.clone());
Ok(lib)
}
}
#[cfg(feature = "tokio")]
pub async fn ext_dylib(path: &Path, ctx: Ctx) -> Result<ExtPort, libloading::Error> {
use futures::io::BufReader;
use futures::{AsyncBufReadExt, StreamExt};
use libloading::Symbol;
use unsync_pipe::pipe;
let (write_input, input) = pipe(1024);
let (output, read_output) = pipe(1024);
let (log, read_log) = pipe(1024);
let log_path = path.to_string_lossy().to_string();
let _ = ctx.spawn(async move {
use orchid_base::logging::log;
let mut lines = BufReader::new(read_log).lines();
while let Some(line) = lines.next().await {
writeln!(log("stderr"), "{log_path} err> {}", line.expect("Readline implies this")).await;
}
});
let library = load_dylib(path)?;
let entrypoint: Symbol<unsafe extern "C" fn(api::binary::ExtensionContext)> =
unsafe { library.get("orchid_extension_main") }?;
let data = Box::into_raw(Box::new(ctx)) as *const ();
extern "C" fn drop(data: *const ()) { std::mem::drop(unsafe { Box::from_raw(data as *mut Ctx) }) }
extern "C" fn spawn(data: *const (), vt: api::binary::FutureVT) {
let _ = unsafe { (data as *mut Ctx).as_mut().unwrap().spawn(vt_to_future(vt)) };
}
let spawner = api::binary::Spawner { data, drop, spawn };
let cx = api::binary::ExtensionContext { input, output, log, spawner };
unsafe { (entrypoint)(cx) };
Ok(ExtPort { input: Box::pin(write_input), output: Box::pin(read_output) })
}

View File

@@ -6,7 +6,7 @@ use futures_locks::{RwLockWriteGuard, TryLockError};
use orchid_base::error::OrcErrv;
use orchid_base::format::fmt;
use orchid_base::location::Pos;
use orchid_base::logging::logger;
use orchid_base::logging::log;
use crate::expr::{Expr, ExprKind, PathSet, Step};
use crate::tree::Root;
@@ -86,7 +86,7 @@ impl ExecCtx {
while self.use_gas(1) {
let mut kind_swap = ExprKind::Missing;
mem::swap(&mut kind_swap, &mut self.cur);
writeln!(logger(), "Exxecute lvl{} {}", self.stack.len(), fmt(&kind_swap).await);
writeln!(log("debug"), "Exxecute lvl{} {}", self.stack.len(), fmt(&kind_swap).await).await;
let (kind, op) = match kind_swap {
ExprKind::Identity(target) => {
let inner = self.unpack_ident(&target).await;

View File

@@ -14,11 +14,10 @@ use futures::{AsyncRead, AsyncWrite, AsyncWriteExt, SinkExt, StreamExt};
use hashbrown::{HashMap, HashSet};
use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request};
use orchid_base::clone;
use orchid_base::format::{FmtCtxImpl, Format};
use orchid_base::interner::{IStr, IStrv, es, ev, is, iv};
use orchid_base::location::Pos;
use orchid_base::logging::logger;
use orchid_base::logging::log;
use orchid_base::name::Sym;
use orchid_base::reqnot::{Client, ClientExt, MsgReaderExt, ReqHandleExt, ReqReaderExt, io_comm};
use orchid_base::stash::{stash, with_stash};
@@ -70,199 +69,213 @@ impl Drop for ExtensionData {
pub struct Extension(Rc<ExtensionData>);
impl Extension {
pub async fn new(mut init: ExtPort, ctx: Ctx) -> io::Result<Self> {
api::HostHeader { log_strategy: logger().strat(), msg_logs: ctx.msg_logs.strat() }
.encode(init.input.as_mut())
.await
.unwrap();
api::HostHeader { logger: ctx.logger.to_api() }.encode(init.input.as_mut()).await.unwrap();
init.input.flush().await.unwrap();
let header = api::ExtensionHeader::decode(init.output.as_mut()).await.unwrap();
let header2 = header.clone();
Ok(Self(Rc::new_cyclic(|weak: &Weak<ExtensionData>| {
// context not needed because exit is extension-initiated
let (client, _, future) = io_comm(
Rc::new(Mutex::new(init.input)),
Mutex::new(init.output),
clone!(weak; async move |reader| {
with_stash(async {
let this = Extension(weak.upgrade().unwrap());
let notif = reader.read::<api::ExtHostNotif>().await.unwrap();
if !matches!(notif, api::ExtHostNotif::Log(_)) {
writeln!(logger(), "Host received notif {notif:?}");
}
match notif {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Acquire(acq)) => {
let target = this.0.ctx.exprs.get_expr(acq.1).expect("Invalid ticket");
this.0.ctx.exprs.give_expr(target)
}
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
if this.is_own_sys(rel.0).await {
this.0.ctx.exprs.take_expr(rel.1);
} else {
writeln!(this.0.ctx.msg_logs, "Not our system {:?}", rel.0)
let (client, _, comm) = io_comm(Rc::new(Mutex::new(init.input)), Mutex::new(init.output));
let weak2 = weak;
let weak = weak.clone();
let ctx2 = ctx.clone();
let join_ext = ctx.clone().spawn(async move {
comm
.listen(
async |reader| {
with_stash(async {
let this = Extension(weak.upgrade().unwrap());
let notif = reader.read::<api::ExtHostNotif>().await.unwrap();
// logging is never logged because its value will be logged anyway
if !matches!(notif, api::ExtHostNotif::Log(_)) {
writeln!(log("msg"), "Host received notif {notif:?}").await;
}
},
api::ExtHostNotif::Log(api::Log(str)) => logger().log(str),
api::ExtHostNotif::Sweeped(data) => {
for i in join_all(data.strings.into_iter().map(es)).await {
this.0.strings.borrow_mut().remove(&i);
}
for i in join_all(data.vecs.into_iter().map(ev)).await {
this.0.string_vecs.borrow_mut().remove(&i);
}
},
}
Ok(())
}).await
}),
{
clone!(weak, ctx);
async move |mut reader| {
with_stash(async {
let this = Self(weak.upgrade().unwrap());
let req = reader.read_req::<api::ExtHostReq>().await.unwrap();
let handle = reader.finish().await;
if !matches!(req, api::ExtHostReq::ExtAtomPrint(_)) {
writeln!(logger(), "Host received request {req:?}");
}
match req {
api::ExtHostReq::Ping(ping) => handle.reply(&ping, &()).await,
api::ExtHostReq::IntReq(intreq) => match intreq {
api::IntReq::InternStr(s) => {
let i = is(&s.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&s, &i.to_api()).await
match notif {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Acquire(acq)) => {
let target = this.0.ctx.exprs.get_expr(acq.1).expect("Invalid ticket");
this.0.ctx.exprs.give_expr(target)
},
api::IntReq::InternStrv(v) => {
let tokens = join_all(v.0.iter().map(|m| es(*m))).await;
this.0.strings.borrow_mut().extend(tokens.iter().cloned());
let i = iv(&tokens).await;
this.0.string_vecs.borrow_mut().insert(i.clone());
handle.reply(&v, &i.to_api()).await
},
api::IntReq::ExternStr(si) => {
let i = es(si.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&si, &i.to_string()).await
},
api::IntReq::ExternStrv(vi) => {
let i = ev(vi.0).await;
this.0.strings.borrow_mut().extend(i.iter().cloned());
this.0.string_vecs.borrow_mut().insert(i.clone());
let markerv = i.iter().map(|t| t.to_api()).collect_vec();
handle.reply(&vi, &markerv).await
},
},
api::ExtHostReq::Fwd(ref fw @ api::Fwd(ref atom, ref key, ref body)) => {
let sys = ctx.system_inst(atom.owner).await.expect("owner of live atom dropped");
let client = sys.client();
let reply =
client.request(api::Fwded(fw.0.clone(), *key, body.clone())).await.unwrap();
handle.reply(fw, &reply).await
},
api::ExtHostReq::SysFwd(ref fw @ api::SysFwd(id, ref body)) => {
let sys = ctx.system_inst(id).await.unwrap();
handle.reply(fw, &sys.request(body.clone()).await).await
},
api::ExtHostReq::SubLex(sl) => {
let (rep_in, mut rep_out) = channel(0);
{
let lex_g = this.0.lex_recur.lock().await;
let mut req_in =
lex_g.get(&sl.id).cloned().expect("Sublex for nonexistent lexid");
req_in.send(ReqPair(sl.clone(), rep_in)).await.unwrap();
}
handle.reply(&sl, &rep_out.next().await.unwrap()).await
},
api::ExtHostReq::ExprReq(expr_req) => match expr_req {
api::ExprReq::Inspect(ins @ api::Inspect { target }) => {
let expr = ctx.exprs.get_expr(target).expect("Invalid ticket");
handle
.reply(&ins, &api::Inspected {
refcount: expr.strong_count() as u32,
location: expr.pos().to_api(),
kind: expr.to_api().await,
})
.await
},
api::ExprReq::Create(ref cre @ api::Create(ref expr)) => {
let expr = Expr::from_api(expr, PathSetBuilder::new(), ctx.clone()).await;
let expr_id = expr.id();
ctx.exprs.give_expr(expr);
handle.reply(cre, &expr_id).await
},
},
api::ExtHostReq::LsModule(ref ls @ api::LsModule(_sys, path)) => {
let reply: <api::LsModule as Request>::Response = 'reply: {
let path = ev(path).await;
let root = (ctx.root.read().await.upgrade())
.expect("LSModule called when root isn't in context");
let root_data = &*root.0.read().await;
let mut walk_ctx = (ctx.clone(), &root_data.consts);
let module =
match walk(&root_data.root, false, path.iter().cloned(), &mut walk_ctx).await
{
Ok(module) => module,
Err(ChildError { kind, .. }) =>
break 'reply Err(match kind {
ChildErrorKind::Private => panic!("Access checking was disabled"),
ChildErrorKind::Constant => api::LsModuleError::IsConstant,
ChildErrorKind::Missing => api::LsModuleError::InvalidPath,
}),
};
let mut members = std::collections::HashMap::new();
for (k, v) in &module.members {
let kind = match v.kind(ctx.clone(), &root_data.consts).await {
MemberKind::Const => api::MemberInfoKind::Constant,
MemberKind::Module(_) => api::MemberInfoKind::Module,
};
members.insert(k.to_api(), api::MemberInfo { public: v.public, kind });
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
if this.is_own_sys(rel.0).await {
this.0.ctx.exprs.take_expr(rel.1);
} else {
writeln!(log("warn"), "Not our system {:?}", rel.0).await
}
Ok(api::ModuleInfo { members })
};
handle.reply(ls, &reply).await
},
api::ExtHostReq::ResolveNames(ref rn) => {
let api::ResolveNames { constid, names, sys } = rn;
let mut resolver = {
let systems = ctx.systems.read().await;
let weak_sys = systems.get(sys).expect("ResolveNames for invalid sys");
let sys = weak_sys.upgrade().expect("ResolveNames after sys drop");
sys.name_resolver(*constid).await
};
let responses = stream(async |mut cx| {
for name in names {
cx.emit(match resolver(&ev(*name).await[..]).await {
Ok(abs) => Ok(abs.to_sym().await.to_api()),
Err(e) => Err(e.to_api()),
})
.await
},
api::ExtHostNotif::Log(api::Log { category, message }) =>
write!(log(&es(category).await), "{message}").await,
api::ExtHostNotif::Sweeped(data) => {
for i in join_all(data.strings.into_iter().map(es)).await {
this.0.strings.borrow_mut().remove(&i);
}
})
.collect()
.await;
handle.reply(rn, &responses).await
},
api::ExtHostReq::ExtAtomPrint(ref eap @ api::ExtAtomPrint(ref atom)) => {
let atom = AtomHand::from_api(atom, Pos::None, &mut ctx.clone()).await;
let unit = atom.print(&FmtCtxImpl::default()).await;
handle.reply(eap, &unit.to_api()).await
},
}
})
.await
}
},
);
let join_ext = ctx.spawn(async {
future.await.unwrap();
// extension exited successfully
for i in join_all(data.vecs.into_iter().map(ev)).await {
this.0.string_vecs.borrow_mut().remove(&i);
}
},
}
Ok(())
})
.await
},
async |mut reader| {
with_stash(async {
let req = reader.read_req::<api::ExtHostReq>().await.unwrap();
let handle = reader.finish().await;
// Atom printing and interning is never reported because it generates too much
// noise
if !matches!(req, api::ExtHostReq::ExtAtomPrint(_))
|| matches!(req, api::ExtHostReq::IntReq(_))
{
writeln!(log("msg"), "Host received request {req:?}").await;
}
let this = Self(weak.upgrade().unwrap());
match req {
api::ExtHostReq::Ping(ping) => handle.reply(&ping, &()).await,
api::ExtHostReq::IntReq(intreq) => match intreq {
api::IntReq::InternStr(s) => {
let i = is(&s.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&s, &i.to_api()).await
},
api::IntReq::InternStrv(v) => {
let tokens = join_all(v.0.iter().map(|m| es(*m))).await;
this.0.strings.borrow_mut().extend(tokens.iter().cloned());
let i = iv(&tokens).await;
this.0.string_vecs.borrow_mut().insert(i.clone());
handle.reply(&v, &i.to_api()).await
},
api::IntReq::ExternStr(si) => {
let i = es(si.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&si, &i.to_string()).await
},
api::IntReq::ExternStrv(vi) => {
let i = ev(vi.0).await;
this.0.strings.borrow_mut().extend(i.iter().cloned());
this.0.string_vecs.borrow_mut().insert(i.clone());
let markerv = i.iter().map(|t| t.to_api()).collect_vec();
handle.reply(&vi, &markerv).await
},
},
api::ExtHostReq::Fwd(ref fw @ api::Fwd(ref atom, ref key, ref body)) => {
let sys =
ctx.system_inst(atom.owner).await.expect("owner of live atom dropped");
let client = sys.client();
let reply =
client.request(api::Fwded(fw.0.clone(), *key, body.clone())).await.unwrap();
handle.reply(fw, &reply).await
},
api::ExtHostReq::SysFwd(ref fw @ api::SysFwd(id, ref body)) => {
let sys = ctx.system_inst(id).await.unwrap();
handle.reply(fw, &sys.request(body.clone()).await).await
},
api::ExtHostReq::SubLex(sl) => {
let (rep_in, mut rep_out) = channel(0);
{
let lex_g = this.0.lex_recur.lock().await;
let mut req_in =
lex_g.get(&sl.id).cloned().expect("Sublex for nonexistent lexid");
req_in.send(ReqPair(sl.clone(), rep_in)).await.unwrap();
}
handle.reply(&sl, &rep_out.next().await.unwrap()).await
},
api::ExtHostReq::ExprReq(expr_req) => match expr_req {
api::ExprReq::Inspect(ins @ api::Inspect { target }) => {
let expr = ctx.exprs.get_expr(target).expect("Invalid ticket");
handle
.reply(&ins, &api::Inspected {
refcount: expr.strong_count() as u32,
location: expr.pos().to_api(),
kind: expr.to_api().await,
})
.await
},
api::ExprReq::Create(ref cre @ api::Create(ref expr)) => {
let expr = Expr::from_api(expr, PathSetBuilder::new(), ctx.clone()).await;
let expr_id = expr.id();
ctx.exprs.give_expr(expr);
handle.reply(cre, &expr_id).await
},
},
api::ExtHostReq::LsModule(ref ls @ api::LsModule(_sys, path)) => {
let reply: <api::LsModule as Request>::Response = 'reply: {
let path = ev(path).await;
let root = (ctx.root.read().await.upgrade())
.expect("LSModule called when root isn't in context");
let root_data = &*root.0.read().await;
let mut walk_ctx = (ctx.clone(), &root_data.consts);
let module =
match walk(&root_data.root, false, path.iter().cloned(), &mut walk_ctx)
.await
{
Ok(module) => module,
Err(ChildError { kind, .. }) =>
break 'reply Err(match kind {
ChildErrorKind::Private => panic!("Access checking was disabled"),
ChildErrorKind::Constant => api::LsModuleError::IsConstant,
ChildErrorKind::Missing => api::LsModuleError::InvalidPath,
}),
};
let mut members = std::collections::HashMap::new();
for (k, v) in &module.members {
let kind = match v.kind(ctx.clone(), &root_data.consts).await {
MemberKind::Const => api::MemberInfoKind::Constant,
MemberKind::Module(_) => api::MemberInfoKind::Module,
};
members.insert(k.to_api(), api::MemberInfo { public: v.public, kind });
}
Ok(api::ModuleInfo { members })
};
handle.reply(ls, &reply).await
},
api::ExtHostReq::ResolveNames(ref rn) => {
let api::ResolveNames { constid, names, sys } = rn;
let mut resolver = {
let systems = ctx.systems.read().await;
let weak_sys = systems.get(sys).expect("ResolveNames for invalid sys");
let sys = weak_sys.upgrade().expect("ResolveNames after sys drop");
sys.name_resolver(*constid).await
};
let responses = stream(async |mut cx| {
for name in names {
cx.emit(match resolver(&ev(*name).await[..]).await {
Ok(abs) => {
let sym = abs.to_sym().await;
this.0.string_vecs.borrow_mut().insert(sym.tok());
Ok(sym.to_api())
},
Err(e) => {
(this.0.strings.borrow_mut())
.extend(e.iter().map(|e| e.description.clone()));
Err(e.to_api())
},
})
.await
}
})
.collect()
.await;
handle.reply(rn, &responses).await
},
api::ExtHostReq::ExtAtomPrint(ref eap @ api::ExtAtomPrint(ref atom)) => {
let atom = AtomHand::from_api(atom, Pos::None, &mut ctx.clone()).await;
let unit = atom.print(&FmtCtxImpl::default()).await;
handle.reply(eap, &unit.to_api()).await
},
}
})
.await
},
)
.await
.unwrap();
});
ExtensionData {
name: header.name.clone(),
ctx: ctx.clone(),
name: header2.name.clone(),
ctx: ctx2,
systems: (header.systems.iter().cloned())
.map(|decl| SystemCtor { decl, ext: WeakExtension(weak.clone()) })
.map(|decl| SystemCtor { decl, ext: WeakExtension(weak2.clone()) })
.collect(),
join_ext: Some(join_ext),
next_pars: RefCell::new(NonZeroU64::new(1).unwrap()),
@@ -282,7 +295,7 @@ impl Extension {
#[must_use]
pub async fn is_own_sys(&self, id: api::SysId) -> bool {
let Some(sys) = self.ctx().system_inst(id).await else {
writeln!(logger(), "Invalid system ID {id:?}");
writeln!(log("warn"), "Invalid system ID {id:?}").await;
return false;
};
Rc::ptr_eq(&self.0, &sys.ext().0)

View File

@@ -3,11 +3,13 @@ use orchid_api as api;
pub mod atom;
pub mod ctx;
pub mod dealias;
pub mod dylib;
pub mod execute;
pub mod expr;
pub mod expr_store;
pub mod extension;
pub mod lex;
pub mod logger;
pub mod parse;
pub mod parsed;
pub mod subprocess;

84
orchid-host/src/logger.rs Normal file
View File

@@ -0,0 +1,84 @@
use std::fmt::Arguments;
use std::fs::File;
use std::io::{Write, stderr};
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_base::logging::{LogWriter, Logger};
use crate::api;
pub struct LogWriterImpl(api::LogStrategy);
impl LogWriter for LogWriterImpl {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
match &self.0 {
api::LogStrategy::Discard => (),
api::LogStrategy::Default => {
stderr().write_fmt(fmt).expect("Could not write to stderr!");
stderr().flush().expect("Could not flush stderr")
},
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
file.write_fmt(fmt).unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
})
}
}
#[derive(Clone, Default)]
pub struct LoggerImpl {
routing: HashMap<String, api::LogStrategy>,
default: Option<api::LogStrategy>,
}
impl LoggerImpl {
pub fn to_api(&self) -> api::Logger {
api::Logger {
default: self.default.clone(),
routing: self.routing.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
}
}
pub fn new(
default: Option<api::LogStrategy>,
strats: impl IntoIterator<Item = (String, api::LogStrategy)>,
) -> Self {
Self { routing: strats.into_iter().collect(), default }
}
pub fn set_default(&mut self, strat: api::LogStrategy) { self.default = Some(strat) }
pub fn clear_default(&mut self) { self.default = None }
pub fn set_category(&mut self, category: &str, strat: api::LogStrategy) {
self.routing.insert(category.to_string(), strat);
}
pub fn with_default(mut self, strat: api::LogStrategy) -> Self {
self.set_default(strat);
self
}
pub fn with_category(mut self, category: &str, strat: api::LogStrategy) -> Self {
self.set_category(category, strat);
self
}
pub async fn log(&self, category: &str, msg: impl AsRef<str>) {
writeln!(self.writer(category), "{}", msg.as_ref()).await
}
pub fn has_category(&self, category: &str) -> bool { self.routing.contains_key(category) }
pub async fn log_buf(&self, category: &str, event: impl AsRef<str>, buf: &[u8]) {
if std::env::var("ORCHID_LOG_BUFFERS").is_ok_and(|v| !v.is_empty()) {
let data = buf.iter().map(|b| format!("{b:02x}")).join(" ");
writeln!(self.writer(category), "{}: [{data}]", event.as_ref()).await
}
}
}
impl Logger for LoggerImpl {
fn writer(&self, category: &str) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl(self.strat(category).clone()))
}
fn strat(&self, category: &str) -> api::LogStrategy {
(self.routing.get(category).cloned().or(self.default.clone()))
.expect("Invalid category and catchall logger not set")
}
}

View File

@@ -1,32 +1,34 @@
use std::io;
use std::{io, process};
use async_process;
use futures::io::BufReader;
use futures::{self, AsyncBufReadExt};
use orchid_base::logging::logger;
use futures::{self, AsyncBufReadExt, StreamExt};
use orchid_base::logging::log;
#[cfg(feature = "tokio")]
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::ctx::Ctx;
use crate::extension::ExtPort;
#[cfg(feature = "tokio")]
pub async fn ext_command(cmd: std::process::Command, ctx: Ctx) -> io::Result<ExtPort> {
let mut child = async_process::Command::from(cmd)
.stdin(async_process::Stdio::piped())
.stdout(async_process::Stdio::piped())
.stderr(async_process::Stdio::piped())
let name = cmd.get_program().to_string_lossy().to_string();
let mut child = tokio::process::Command::from(cmd)
.stdin(process::Stdio::piped())
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.spawn()?;
std::thread::spawn(|| {});
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let mut child_stderr = child.stderr.take().unwrap();
let child_stderr = child.stderr.take().unwrap();
let _ = ctx.spawn(Box::pin(async move {
let _ = child;
let mut reader = BufReader::new(&mut child_stderr);
loop {
let mut buf = String::new();
if 0 == reader.read_line(&mut buf).await.unwrap() {
break;
}
logger().log(buf.strip_suffix('\n').expect("Readline implies this"));
let mut lines = BufReader::new(child_stderr.compat()).lines();
while let Some(line) = lines.next().await {
// route stderr with an empty category string. This is not the intended logging
// method
writeln!(log("stderr"), "{} err> {}", name, line.expect("Readline implies this")).await;
}
}));
Ok(ExtPort { input: Box::pin(stdin), output: Box::pin(stdout) })
Ok(ExtPort { input: Box::pin(stdin.compat_write()), output: Box::pin(stdout.compat()) })
}

View File

@@ -14,9 +14,10 @@ use orchid_base::error::{OrcRes, mk_errv_floating};
use orchid_base::format::{FmtCtx, FmtUnit, Format};
use orchid_base::interner::{IStr, es, is};
use orchid_base::iter_utils::IteratorPrint;
use orchid_base::logging::logger;
use orchid_base::logging::log;
use orchid_base::name::{NameLike, Sym, VName, VPath};
use orchid_base::reqnot::{Client, ClientExt};
use orchid_base::stash::stash;
use ordered_float::NotNan;
use substack::{Stackframe, Substack};
@@ -86,7 +87,8 @@ impl System {
#[must_use]
pub fn can_lex(&self, c: char) -> bool {
let ret = char_filter_match(&self.0.lex_filter, c);
writeln!(logger(), "{} can lex {c}: {}", self.ctor().name(), ret);
let ctor = self.ctor();
stash(async move { writeln!(log("debug"), "{} can lex {c}: {}", ctor.name(), ret).await });
ret
}
#[must_use]

View File

@@ -3,11 +3,19 @@ name = "orchid-std"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "orchid-std"
path = "src/main.rs"
[lib]
crate-type = ["cdylib", "lib"]
path = "src/lib.rs"
[dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
futures = { version = "0.3.31", features = ["std"], default-features = false }
hashbrown = "0.16.0"
hashbrown = "0.16.1"
itertools = "0.14.0"
never = "0.1.0"
once_cell = "1.21.3"
@@ -18,12 +26,12 @@ orchid-base = { version = "0.1.0", path = "../orchid-base" }
orchid-extension = { version = "0.1.0", path = "../orchid-extension", features = [
"tokio",
] }
ordered-float = "5.0.0"
pastey = "0.1.1"
rust_decimal = "1.38.0"
ordered-float = "5.1.0"
pastey = "0.2.1"
rust_decimal = "1.39.0"
subslice-offset = "0.1.1"
substack = "1.1.1"
tokio = { version = "1.47.1", features = ["full"] }
tokio = { version = "1.49.0", features = ["full"] }
[dev-dependencies]
test_executors = "0.3.5"
test_executors = "0.4.1"

View File

@@ -11,3 +11,12 @@ pub use std::tuple::{HomoTpl, Tpl, Tuple, UntypedTuple};
pub use macros::macro_system::MacroSystem;
pub use macros::mactree::{MacTok, MacTree};
use orchid_api as api;
use orchid_extension::binary::orchid_extension_main_body;
use orchid_extension::entrypoint::ExtensionBuilder;
pub extern "C" fn orchid_extension_main(cx: api::binary::ExtensionContext) {
orchid_extension_main_body(
cx,
ExtensionBuilder::new("orchid-std::main").system(StdSystem).system(MacroSystem),
);
}

View File

@@ -8,7 +8,7 @@ use orchid_base::error::mk_errv;
use orchid_base::format::fmt;
use orchid_base::interner::is;
use orchid_base::location::Pos;
use orchid_base::logging::logger;
use orchid_base::logging::log;
use orchid_base::name::{NameLike, Sym, VPath};
use orchid_base::tree::Paren;
use orchid_extension::atom::TAtom;
@@ -27,9 +27,7 @@ use crate::{MacTok, MacTree};
pub async fn resolve(val: MacTree) -> GExpr {
exec(async move |mut h| {
// if ctx.logger().is_active() {
writeln!(logger(), "Macro-resolving {}", fmt(&val).await);
// }
writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await).await;
let root = refl();
let mut macros = HashMap::new();
for n in val.glossary() {
@@ -67,7 +65,13 @@ pub async fn resolve(val: MacTree) -> GExpr {
}
let mut rctx = ResolveCtx { h, exclusive, priod };
let gex = resolve_one(&mut rctx, Substack::Bottom, &val).await;
writeln!(logger(), "Macro-resolution over {}\nreturned {}", fmt(&val).await, fmt(&gex).await);
writeln!(
log("debug"),
"Macro-resolution over {}\nreturned {}",
fmt(&val).await,
fmt(&gex).await
)
.await;
gex
})
.await

View File

@@ -7,13 +7,15 @@ edition = "2024"
[dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
camino = "1.1.9"
clap = { version = "4.5.24", features = ["derive", "env"] }
ctrlc = "3.4.5"
camino = "1.2.2"
clap = { version = "4.5.54", features = ["derive", "env"] }
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" }
orchid-host = { version = "0.1.0", path = "../orchid-host", features = [
"tokio",
] }
substack = "1.1.1"
tokio = { version = "1.43.0", features = ["full"] }
tokio = { version = "1.49.0", features = ["full"] }

View File

@@ -1,3 +1,5 @@
use orchid_base::logging::Logger;
use tokio::time::Instant;
pub mod parse_folder;
use std::cell::RefCell;
@@ -17,9 +19,10 @@ 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::logging::{log, with_logger};
use orchid_base::name::{NameLike, VPath};
use orchid_base::parse::{Import, Snippet};
use orchid_base::stash::with_stash;
use orchid_base::sym;
use orchid_base::tree::{Token, ttv_fmt};
use orchid_host::ctx::{Ctx, JoinHandle, Spawner};
@@ -27,6 +30,7 @@ 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::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule};
use orchid_host::subprocess::ext_command;
@@ -45,12 +49,12 @@ pub struct Args {
extension: Vec<Utf8PathBuf>,
#[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')]
system: Vec<String>,
#[arg(short, long)]
logs: bool,
#[arg(short, long)]
msg_logs: bool,
#[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)]
@@ -78,6 +82,20 @@ pub enum Commands {
},
}
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,
@@ -92,6 +110,38 @@ fn get_all_extensions<'a>(
})
}
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() }
@@ -109,258 +159,270 @@ impl Spawner for SpawnerImpl {
#[tokio::main]
async fn main() -> io::Result<ExitCode> {
eprintln!("orcx launched");
eprintln!("Orcx was made by Lawrence Bethlenfalvy");
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 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 });
let logger = get_logger(&args);
let logger2 = logger.clone();
unsafe { STARTUP = Some(Instant::now()) };
local_set.spawn_local(async move {
let ctx = &Ctx::new(msg_logger.clone(), SpawnerImpl);
let extensions = get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().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 ctx = &Ctx::new(SpawnerImpl, logger2);
with_stash(async {
let extensions =
get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap();
time_print(&args, "Extensions loaded");
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 add_imports = |items: &mut Vec<Item>, 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,
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 };
writeln!(
log("debug"),
"lexed: {}",
take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true)
)
.await;
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<Item>, 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 } => {
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) => {
writeln!(
log("debug"),
"lexed: {}",
fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")
)
.await;
lexemes
},
Err(e) => {
eprintln!("{e}");
*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));
};
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!(
"{const_name} = {}",
take_first(&val.print(&FmtCtxImpl::default()).await, false)
),
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!"),
}
}
},
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::<ParsTokTree>(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!"),
}
},
}
},
}
})
.await;
});
with_interner(local_interner(), with_logger(cx_logger, local_set)).await;
with_interner(local_interner(), with_logger(logger, local_set)).await;
let x = *exit_code.borrow();
Ok(x)
}

View File

@@ -12,7 +12,7 @@ futures = "0.3.31"
itertools = "0.14.0"
rand = "0.9.2"
rand_chacha = "0.9.0"
test_executors = "0.4.0"
test_executors = "0.4.1"
[dependencies]
futures-io = "0.3.31"

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.24", features = ["derive"] }
clap = { version = "4.5.54", features = ["derive"] }