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 } futures = { version = "0.3.31", features = ["std"], default-features = false }
[dev-dependencies] [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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
quote = "1.0.40" quote = "1.0.42"
syn = { version = "2.0.106" } syn = { version = "2.0.112" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" } 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" itertools = "0.14.0"

View File

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

View File

@@ -1,32 +1,46 @@
//! # Binary extension definition //! # Binary extension definition
//! //!
//! A binary extension is a DLL / shared object / dylib with a symbol called //! A binary extension is a DLL / shared object / dylib with a symbol called
//! `orchid_extension_main` which accepts a single argument of type [ExtCtx]. //! `orchid_extension_main` which accepts a single argument of type
//! Once that is received, communication continuees through the channel with the //! [ExtensionContext]. Once that is received, communication continuees through
//! same protocol outlined in [crate::proto] //! the channel with the same protocol outlined in [crate::proto]
use unsync_pipe::{Reader, Writer}; use unsync_pipe::{Reader, Writer};
/// !Send !Sync owned waker /// !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)] #[repr(C)]
pub struct OwnedWakerVT { pub struct OwnedWakerVT {
data: *const (), pub data: *const (),
/// `self` /// `self`
drop: extern "C" fn(*const ()), pub drop: extern "C" fn(*const ()),
/// `self`
pub wake: extern "C" fn(*const ()),
/// `&self` /// `&self`
wake: extern "C" fn(*const ()), pub wake_ref: extern "C" fn(*const ()),
} }
/// !Send !Sync, equivalent to `&mut Context<'a>`, hence no `drop`. /// !Send !Sync, equivalent to `&mut Context<'a>`, hence no `drop`.
/// When received in [FutureVT::poll], it must not outlive the call. /// 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)] #[repr(C)]
pub struct FutureContextVT { pub struct FutureContextVT {
data: *const (), pub data: *const (),
/// `&self` /// `&self`
waker: extern "C" fn(*const ()) -> OwnedWakerVT, pub waker: extern "C" fn(*const ()) -> OwnedWakerVT,
} }
/// ABI-stable `Poll<()>` /// ABI-stable `Poll<()>`
#[derive(Clone, Copy)]
#[repr(C)] #[repr(C)]
pub enum UnitPoll { pub enum UnitPoll {
Pending, Pending,
@@ -34,34 +48,43 @@ pub enum UnitPoll {
} }
/// ABI-stable `Pin<Box<dyn Future<Output = ()>>>` /// 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)] #[repr(C)]
pub struct FutureVT { pub struct FutureVT {
data: *const (), pub data: *const (),
/// `self` /// `self`
drop: extern "C" fn(*const ()), pub drop: extern "C" fn(*const ()),
/// `&mut self` Equivalent to [Future::poll] /// `&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)] #[repr(C)]
pub struct ExtensionContext { pub struct ExtensionContext {
data: *const (), /// Spawns tasks associated with this extension
/// `self` pub spawner: Spawner,
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),
/// serialized [crate::HostExtChannel] /// serialized [crate::HostExtChannel]
input: Reader, pub input: Reader,
/// serialized [crate::ExtHostChannel] /// serialized [crate::ExtHostChannel]
output: Writer, pub output: Writer,
/// UTF-8 log stream directly to log service. /// 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 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)] #[derive(Clone, Debug, Coding, PartialEq, Eq, Hash)]
pub enum LogStrategy { pub enum LogStrategy {
StdErr, /// Context-dependent default stream, often stderr
File(String), Default,
/// A file on the local filesystem
File { path: String, append: bool },
/// Discard any log output
Discard, Discard,
} }
#[derive(Clone, Debug, Coding)]
pub struct Logger {
pub routing: HashMap<String, LogStrategy>,
pub default: Option<LogStrategy>,
}
#[derive(Clone, Debug, Coding, Hierarchy)] #[derive(Clone, Debug, Coding, Hierarchy)]
#[extends(ExtHostNotif)] #[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"; static HOST_INTRO: &[u8] = b"Orchid host, binary API v0\n";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HostHeader { pub struct HostHeader {
pub log_strategy: logging::LogStrategy, pub logger: logging::Logger,
pub msg_logs: logging::LogStrategy,
} }
impl Decode for HostHeader { impl Decode for HostHeader {
async fn decode<R: AsyncRead + ?Sized>(mut read: Pin<&mut R>) -> io::Result<Self> { async fn decode<R: AsyncRead + ?Sized>(mut read: Pin<&mut R>) -> io::Result<Self> {
read_exact(read.as_mut(), HOST_INTRO).await?; read_exact(read.as_mut(), HOST_INTRO).await?;
Ok(Self { Ok(Self { logger: logging::Logger::decode(read).await? })
log_strategy: logging::LogStrategy::decode(read.as_mut()).await?,
msg_logs: logging::LogStrategy::decode(read.as_mut()).await?,
})
} }
} }
impl Encode for HostHeader { impl Encode for HostHeader {
async fn encode<W: AsyncWrite + ?Sized>(&self, mut write: Pin<&mut W>) -> io::Result<()> { async fn encode<W: AsyncWrite + ?Sized>(&self, mut write: Pin<&mut W>) -> io::Result<()> {
write.write_all(HOST_INTRO).await?; write.write_all(HOST_INTRO).await?;
self.log_strategy.encode(write.as_mut()).await?; self.logger.encode(write.as_mut()).await
self.msg_logs.encode(write.as_mut()).await
} }
} }
@@ -159,19 +154,19 @@ impl MsgSet for HostMsgSet {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap;
use orchid_api_traits::enc_vec; use orchid_api_traits::enc_vec;
use ordered_float::NotNan; use ordered_float::NotNan;
use test_executors::spin_on; use test_executors::spin_on;
use super::*; use super::*;
use crate::Logger;
#[test] #[test]
fn host_header_enc() { fn host_header_enc() {
spin_on(async { spin_on(async {
let hh = HostHeader { let hh = HostHeader { logger: Logger { routing: HashMap::new(), default: None } };
log_strategy: logging::LogStrategy::File("SomeFile".to_string()),
msg_logs: logging::LogStrategy::File("SomeFile".to_string()),
};
let mut enc = &enc_vec(&hh)[..]; let mut enc = &enc_vec(&hh)[..];
eprintln!("Encoded to {enc:?}"); eprintln!("Encoded to {enc:?}");
HostHeader::decode(Pin::new(&mut enc)).await.unwrap(); HostHeader::decode(Pin::new(&mut enc)).await.unwrap();

View File

@@ -13,7 +13,7 @@ bound = "0.6.0"
derive_destructure = "1.0.0" derive_destructure = "1.0.0"
dyn-clone = "1.0.20" dyn-clone = "1.0.20"
futures = { version = "0.3.31", features = ["std"], default-features = false } futures = { version = "0.3.31", features = ["std"], default-features = false }
hashbrown = "0.16.0" hashbrown = "0.16.1"
itertools = "0.14.0" itertools = "0.14.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
never = "0.1.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 = { version = "0.1.0", path = "../orchid-api" }
orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" } orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" } orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
ordered-float = "5.0.0" ordered-float = "5.1.0"
regex = "1.11.2" regex = "1.12.2"
rust-embed = "8.7.2" rust-embed = "8.9.0"
substack = "1.1.1" substack = "1.1.1"
trait-set = "0.3.0" trait-set = "0.3.0"
task-local = "0.1.0" task-local = "0.1.0"
[dev-dependencies] [dev-dependencies]
futures = "0.3.31" 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 { 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) 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 { impl From<OrcErr> for OrcErrv {
fn from(value: OrcErr) -> Self { Self(vec![value]) } fn from(value: OrcErr) -> Self { Self(vec![value]) }

View File

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

View File

@@ -1,47 +1,74 @@
use std::any::Any;
use std::cell::RefCell;
use std::fmt::Arguments; use std::fmt::Arguments;
use std::fs::File; use std::io::Write;
use std::io::{Write, stderr}; use std::rc::Rc;
pub use api::LogStrategy; use futures::future::LocalBoxFuture;
use itertools::Itertools;
use task_local::task_local; use task_local::task_local;
use crate::api; use crate::api;
#[derive(Clone)] task_local! {
pub struct Logger(api::LogStrategy); static DEFAULT_WRITER: RefCell<Box<dyn Write>>
impl Logger { }
pub fn new(strat: api::LogStrategy) -> Self { Self(strat) }
pub fn log(&self, msg: impl AsRef<str>) { writeln!(self, "{}", msg.as_ref()) } /// Set the stream used for [api::LogStrategy::Default]. If not set,
pub fn strat(&self) -> api::LogStrategy { self.0.clone() } /// [std::io::stderr] will be used.
pub fn is_active(&self) -> bool { !matches!(self.0, api::LogStrategy::Discard) } pub async fn with_default_stream<F: Future>(stderr: impl Write + 'static, fut: F) -> F::Output {
pub fn log_buf(&self, event: impl AsRef<str>, buf: &[u8]) { DEFAULT_WRITER.scope(RefCell::new(Box::new(stderr)), fut).await
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 trait LogWriter {
} fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()>;
pub fn write_fmt(&self, fmt: Arguments) { }
match &self.0 {
api::LogStrategy::Discard => (), pub trait Logger: Any {
api::LogStrategy::StdErr => { fn writer(&self, category: &str) -> Rc<dyn LogWriter>;
stderr().write_fmt(fmt).expect("Could not write to stderr!"); fn strat(&self, category: &str) -> api::LogStrategy;
stderr().flush().expect("Could not flush stderr") fn is_active(&self, category: &str) -> bool {
}, !matches!(self.strat(category), api::LogStrategy::Discard)
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! { task_local! {
static LOGGER: Logger; static LOGGER: Rc<dyn Logger>;
} }
pub async fn with_logger<F: Future>(logger: Logger, fut: F) -> F::Output { pub async fn with_logger<F: Future>(logger: impl Logger + 'static, fut: F) -> F::Output {
LOGGER.scope(logger, fut).await 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::cell::RefCell;
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::{Pin, pin}; use std::pin::{Pin, pin};
use std::rc::Rc; use std::rc::Rc;
@@ -316,13 +315,25 @@ impl CommCtx {
pub fn io_comm( pub fn io_comm(
o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>, o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>,
i: Mutex<Pin<Box<dyn AsyncRead>>>, i: Mutex<Pin<Box<dyn AsyncRead>>>,
notif: impl for<'a> AsyncFn(Box<dyn MsgReader<'a> + 'a>) -> io::Result<()>, ) -> (impl Client + 'static, CommCtx, IoCommServer) {
req: impl for<'a> AsyncFn(Box<dyn ReqReader<'a> + 'a>) -> io::Result<Receipt<'a>>,
) -> (impl Client + 'static, CommCtx, impl Future<Output = io::Result<()>>) {
let i = Rc::new(i); let i = Rc::new(i);
let (onsub, client) = IoClient::new(o.clone()); let (onsub, client) = IoClient::new(o.clone());
let (exit, onexit) = channel(1); 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 { enum Event {
Input(u64, IoGuard<dyn AsyncRead>), Input(u64, IoGuard<dyn AsyncRead>),
Sub(ReplySub), Sub(ReplySub),
@@ -363,6 +374,9 @@ pub fn io_comm(
Err(e) => break 'body Err(e), Err(e) => break 'body Err(e),
Ok(Event::Exit) => { Ok(Event::Exit) => {
*exiting.borrow_mut() = true; *exiting.borrow_mut() = true;
let mut out = o.lock().await;
out.as_mut().flush().await?;
out.as_mut().close().await?;
break; break;
}, },
Ok(Event::Sub(ReplySub { id, ack, cb })) => { Ok(Event::Sub(ReplySub { id, ack, cb })) => {
@@ -399,11 +413,12 @@ pub fn io_comm(
next? next?
} }
Ok(()) Ok(())
}) }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use futures::channel::mpsc; use futures::channel::mpsc;
@@ -414,6 +429,8 @@ mod test {
use test_executors::spin_on; use test_executors::spin_on;
use unsync_pipe::pipe; use unsync_pipe::pipe;
use crate::logging::test::TestLogger;
use crate::logging::with_logger;
use crate::reqnot::{ClientExt, MsgReaderExt, ReqReaderExt, io_comm}; use crate::reqnot::{ClientExt, MsgReaderExt, ReqReaderExt, io_comm};
#[derive(Clone, Debug, PartialEq, Coding, Hierarchy)] #[derive(Clone, Debug, PartialEq, Coding, Hierarchy)]
@@ -422,33 +439,36 @@ mod test {
#[test] #[test]
fn notification() { 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 (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024); let (in2, out1) = pipe(1024);
let (received, mut on_receive) = mpsc::channel(2); let (received, mut on_receive) = mpsc::channel(2);
let (_, recv_ctx, run_recv) = io_comm( let (_, recv_ctx, recv_srv) =
Rc::new(Mutex::new(Box::pin(in2))), io_comm(Rc::new(Mutex::new(Box::pin(in2))), Mutex::new(Box::pin(out2)));
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| { async |notif| {
received.clone().send(notif.read::<TestNotif>().await?).await.unwrap(); received.clone().send(notif.read::<TestNotif>().await?).await.unwrap();
Ok(()) Ok(())
}, },
async |_| panic!("Should receive notif, not request"), async |_| panic!("Should receive notif, not request"),
); )
let (sender, ..) = io_comm( .await
Rc::new(Mutex::new(Box::pin(in1))), .unwrap()
Mutex::new(Box::pin(out1)), },
async |_| panic!("Should not receive notif"), async {
async |_| panic!("Should not receive request"),
);
join!(async { run_recv.await.unwrap() }, async {
sender.notify(TestNotif(3)).await.unwrap(); sender.notify(TestNotif(3)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(3))); assert_eq!(on_receive.next().await, Some(TestNotif(3)));
sender.notify(TestNotif(4)).await.unwrap(); sender.notify(TestNotif(4)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(4))); assert_eq!(on_receive.next().await, Some(TestNotif(4)));
recv_ctx.exit().await; recv_ctx.exit().await;
}); }
}) );
}))
} }
#[derive(Clone, Debug, Coding, Hierarchy)] #[derive(Clone, Debug, Coding, Hierarchy)]
@@ -460,30 +480,94 @@ mod test {
#[test] #[test]
fn request() { 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 (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024); let (in2, out1) = pipe(1024);
let (_, srv_ctx, run_srv) = io_comm( let (_, srv_ctx, srv) =
Rc::new(Mutex::new(Box::pin(in2))), io_comm(Rc::new(Mutex::new(Box::pin(in2))), Mutex::new(Box::pin(out2)));
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 |_| panic!("No notifs expected"),
async |mut req| { async |mut req| {
let val = req.read_req::<DummyRequest>().await?; let val = req.read_req::<DummyRequest>().await?;
req.reply(&val, &(val.0 + 1)).await req.reply(&val, &(val.0 + 1)).await
}, },
); )
let (client, client_ctx, run_client) = io_comm( .await
Rc::new(Mutex::new(Box::pin(in1))), .unwrap()
Mutex::new(Box::pin(out1)), },
async {
client_srv
.listen(
async |_| panic!("Not expecting ingress notif"), async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"), async |_| panic!("Not expecting ingress req"),
); )
join!(async { run_srv.await.unwrap() }, async { run_client.await.unwrap() }, async { .await
.unwrap()
},
async {
let response = client.request(DummyRequest(5)).await.unwrap(); let response = client.request(DummyRequest(5)).await.unwrap();
assert_eq!(response, 6); assert_eq!(response, 6);
srv_ctx.exit().await; srv_ctx.exit().await;
client_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" bound = "0.6.0"
derive_destructure = "1.0.0" derive_destructure = "1.0.0"
dyn-clone = "1.0.20" dyn-clone = "1.0.20"
futures = { version = "0.3.31", features = [ futures = { version = "0.3.31", default-features = false, features = [
"std", "std",
"async-await", "async-await",
], default-features = false } ] }
futures-locks = "0.7.1" futures-locks = "0.7.1"
hashbrown = "0.16.0" hashbrown = "0.16.1"
include_dir = { version = "0.7.4", optional = true } include_dir = { version = "0.7.4", optional = true }
itertools = "0.14.0" itertools = "0.14.0"
konst = "0.4.2" konst = "0.4.3"
lazy_static = "1.5.0" lazy_static = "1.5.0"
memo-map = "0.3.3" memo-map = "0.3.3"
never = "0.1.0" 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-derive = { version = "0.1.0", path = "../orchid-api-derive" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" } orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
orchid-base = { version = "0.1.0", path = "../orchid-base" } orchid-base = { version = "0.1.0", path = "../orchid-base" }
ordered-float = "5.0.0" ordered-float = "5.1.0"
pastey = "0.1.1" pastey = "0.2.1"
substack = "1.1.1" substack = "1.1.1"
task-local = "0.1.0" task-local = "0.1.0"
tokio = { version = "1.47.1", optional = true, features = [] } tokio = { version = "1.49.0", optional = true, features = [] }
tokio-util = { version = "0.7.16", optional = true, features = ["compat"] } tokio-util = { version = "0.7.17", optional = true, features = ["compat"] }
trait-set = "0.3.0" trait-set = "0.3.0"

View File

@@ -19,6 +19,7 @@ use never::Never;
use orchid_api_traits::{Decode, Encode, enc_vec}; use orchid_api_traits::{Decode, Encode, enc_vec};
use orchid_base::error::OrcRes; use orchid_base::error::OrcRes;
use orchid_base::format::{FmtCtx, FmtCtxImpl, FmtUnit, take_first}; use orchid_base::format::{FmtCtx, FmtCtxImpl, FmtUnit, take_first};
use orchid_base::logging::log;
use orchid_base::name::Sym; use orchid_base::name::Sym;
use task_local::task_local; 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)); 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_api_traits::{Coding, enc_vec};
use orchid_base::error::OrcRes; use orchid_base::error::OrcRes;
use orchid_base::format::FmtUnit; use orchid_base::format::FmtUnit;
use orchid_base::logging::logger; use orchid_base::logging::log;
use orchid_base::name::Sym; use orchid_base::name::Sym;
use crate::api; 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, ()> { fn drop<'a>(&'a self, AtomCtx(buf, _): AtomCtx<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
let string_self = T::decode_slice(&mut &buf[..]).print().await; 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::char_filter::{char_filter_match, char_filter_union, mk_char_filter};
use orchid_base::error::try_with_reporter; use orchid_base::error::try_with_reporter;
use orchid_base::interner::{es, is, with_interner}; 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::name::Sym;
use orchid_base::parse::{Comment, Snippet}; use orchid_base::parse::{Comment, Snippet};
use orchid_base::reqnot::{ use orchid_base::reqnot::{
@@ -35,6 +35,7 @@ use crate::ext_port::ExtPort;
use crate::func_atom::with_funs_ctx; use crate::func_atom::with_funs_ctx;
use crate::interner::new_interner; use crate::interner::new_interner;
use crate::lexer::{LexContext, ekey_cascade, ekey_not_applicable}; 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::parser::{PTokTree, ParsCtx, get_const, linev_into_api, with_parsed_const_ctx};
use crate::reflection::with_refl_roots; use crate::reflection::with_refl_roots;
use crate::system::{SysCtx, atom_by_idx, cted, with_sys}; 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 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] /// Send a request through the global client's [ClientExt::request]
pub async fn request<T: Request + UnderRoot<Root = ExtHostReq>>(t: T) -> T::Response { 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] /// 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 { async fn with_sys_record<F: Future>(id: api::SysId, fut: F) -> F::Output {
let cted = SYSTEM_TABLE.with(|tbl| { let cted = SYSTEM_TABLE.with(|tbl| tbl.borrow().get(&id).expect("Invalid sys ID").cted.clone());
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
});
with_sys(SysCtx(id, cted), fut).await with_sys(SysCtx(id, cted), fut).await
} }
@@ -126,8 +127,7 @@ impl ExtensionBuilder {
self.add_context(with_lazy_member_store); self.add_context(with_lazy_member_store);
self.add_context(with_refl_roots); self.add_context(with_refl_roots);
(ctx.spawn)(Box::pin(async move { (ctx.spawn)(Box::pin(async move {
let api::HostHeader { log_strategy, msg_logs } = let host_header = api::HostHeader::decode(ctx.input.as_mut()).await.unwrap();
api::HostHeader::decode(ctx.input.as_mut()).await.unwrap();
let decls = (self.systems.iter().enumerate()) let decls = (self.systems.iter().enumerate())
.map(|(id, sys)| (u16::try_from(id).expect("more than u16max system ctors"), sys)) .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()))) .map(|(id, sys)| sys.decl(api::SysDeclId(NonZero::new(id + 1).unwrap())))
@@ -137,24 +137,26 @@ impl ExtensionBuilder {
.await .await
.unwrap(); .unwrap();
ctx.output.as_mut().flush().await.unwrap(); ctx.output.as_mut().flush().await.unwrap();
let logger = Logger::new(log_strategy); let logger1 = LoggerImpl::from_api(&host_header.logger);
let logger2 = logger.clone(); let logger2 = logger1.clone();
let msg_logger = Logger::new(msg_logs); let (client, comm_ctx, extension_srv) =
let (client, ctx, run_extension) = io_comm( io_comm(Rc::new(Mutex::new(ctx.output)), Mutex::new(ctx.input));
Rc::new(Mutex::new(ctx.output)), let extension_fut = extension_srv.listen(
Mutex::new(ctx.input), async |n: Box<dyn MsgReader<'_>>| {
async move |n: Box<dyn MsgReader<'_>>| { let notif = n.read().await.unwrap();
match n.read().await.unwrap() { match notif {
api::HostExtNotif::Exit => exit().await, api::HostExtNotif::Exit => exit().await,
} }
Ok(()) Ok(())
}, },
async move |mut reader| { async |mut reader| {
with_stash(async { with_stash(async {
let req = reader.read_req().await.unwrap(); let req = reader.read_req().await.unwrap();
let handle = reader.finish().await; 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(_))) { 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 { match req {
api::HostExtReq::SystemDrop(sys_drop) => { api::HostExtReq::SystemDrop(sys_drop) => {
@@ -266,11 +268,6 @@ impl ExtensionBuilder {
let ekey_na = ekey_not_applicable().await; let ekey_na = ekey_not_applicable().await;
let ekey_cascade = ekey_cascade().await; let ekey_cascade = ekey_cascade().await;
let lexers = cted().inst().dyn_lexers(); let lexers = cted().inst().dyn_lexers();
writeln!(
logger,
"sys={sys:?}, tc={trigger_char}, lexers={}",
lexers.iter().map(|l| format!("{l:?}")).join(",")
);
for lx in for lx in
lexers.iter().filter(|l| char_filter_match(l.char_filter(), trigger_char)) 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; expr_store.dispose().await;
handle.reply(&lex, &None).await handle.reply(&lex, &None).await
}) })
@@ -423,16 +420,16 @@ impl ExtensionBuilder {
logger2, logger2,
with_comm( with_comm(
Rc::new(client), Rc::new(client),
ctx, comm_ctx,
(self.context.into_iter()).fold( (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), |fut, cx| cx.apply(fut),
), ),
), ),
), ),
), ),
) )
.await .await;
}) as Pin<Box<_>>); }) 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 orchid_base::interner::{IStr, IStrv, InternerSrv};
use crate::api; use crate::api;
use crate::entrypoint::request; use crate::entrypoint::{MUTE_REPLY, request};
#[derive(Default)] #[derive(Default)]
struct ExtInterner { struct ExtInterner {
@@ -17,7 +17,9 @@ impl InternerSrv for ExtInterner {
fn is<'a>(&'a self, v: &'a str) -> LocalBoxFuture<'a, IStr> { fn is<'a>(&'a self, v: &'a str) -> LocalBoxFuture<'a, IStr> {
match self.str.i(v) { match self.str.i(v) {
Ok(i) => Box::pin(ready(i)), 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> { fn es(&self, t: api::TStr) -> LocalBoxFuture<'_, IStr> {

View File

@@ -12,6 +12,7 @@ pub mod func_atom;
pub mod gen_expr; pub mod gen_expr;
pub mod interner; pub mod interner;
pub mod lexer; pub mod lexer;
pub mod logger;
pub mod other_system; pub mod other_system;
pub mod parser; pub mod parser;
pub mod reflection; pub mod reflection;
@@ -19,3 +20,4 @@ pub mod system;
pub mod system_ctor; pub mod system_ctor;
pub mod tokio; pub mod tokio;
pub mod tree; 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; type Instance: System;
const NAME: &'static str; const NAME: &'static str;
const VERSION: f64; const VERSION: f64;
/// Create a system instance. When this function is called, a context object /// Create a system instance.
/// isn't yet available
fn inst(&self, deps: <Self::Deps as DepDef>::Sat) -> Self::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::entrypoint::ExtensionBuilder;
use crate::ext_port::ExtPort; 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")] #[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::io::{stderr, stdin, stdout};
use tokio::task::{LocalSet, spawn_local}; use tokio::task::{LocalSet, spawn_local};
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
@@ -21,4 +27,5 @@ pub async fn tokio_main(builder: ExtensionBuilder) {
}); });
}); });
local_set.await; local_set.await;
std::process::exit(0)
} }

View File

@@ -8,22 +8,28 @@ edition = "2024"
[dependencies] [dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" } async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4" async-once-cell = "0.5.4"
async-process = "2.4.0"
bound = "0.6.0" bound = "0.6.0"
derive_destructure = "1.0.0" derive_destructure = "1.0.0"
futures = { version = "0.3.31", features = ["std"], default-features = false } futures = { version = "0.3.31", features = ["std"], default-features = false }
futures-locks = "0.7.1" futures-locks = "0.7.1"
hashbrown = "0.16.0" hashbrown = "0.16.1"
itertools = "0.14.0" itertools = "0.14.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
libloading = { version = "0.9.0", optional = true }
memo-map = "0.3.3" memo-map = "0.3.3"
never = "0.1.0" never = "0.1.0"
num-traits = "0.2.19" num-traits = "0.2.19"
orchid-api = { version = "0.1.0", path = "../orchid-api" } orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" } orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
orchid-base = { version = "0.1.0", path = "../orchid-base" } orchid-base = { version = "0.1.0", path = "../orchid-base" }
ordered-float = "5.0.0" ordered-float = "5.1.0"
pastey = "0.1.1" pastey = "0.2.1"
substack = "1.1.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" 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::future::LocalBoxFuture;
use futures_locks::RwLock; use futures_locks::RwLock;
use hashbrown::HashMap; use hashbrown::HashMap;
use orchid_base::logging::Logger;
use crate::api; use crate::api;
use crate::expr_store::ExprStore; use crate::expr_store::ExprStore;
use crate::logger::LoggerImpl;
use crate::system::{System, WeakSystem}; use crate::system::{System, WeakSystem};
use crate::tree::WeakRoot; use crate::tree::WeakRoot;
@@ -24,11 +24,11 @@ pub trait Spawner {
pub struct CtxData { pub struct CtxData {
spawner: Rc<dyn Spawner>, spawner: Rc<dyn Spawner>,
pub msg_logs: Logger,
pub systems: RwLock<HashMap<api::SysId, WeakSystem>>, pub systems: RwLock<HashMap<api::SysId, WeakSystem>>,
pub system_id: RefCell<NonZeroU16>, pub system_id: RefCell<NonZeroU16>,
pub exprs: ExprStore, pub exprs: ExprStore,
pub root: RwLock<WeakRoot>, pub root: RwLock<WeakRoot>,
pub logger: LoggerImpl,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Ctx(Rc<CtxData>); pub struct Ctx(Rc<CtxData>);
@@ -46,14 +46,14 @@ impl WeakCtx {
} }
impl Ctx { impl Ctx {
#[must_use] #[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 { Self(Rc::new(CtxData {
msg_logs,
spawner: Rc::new(spawner), spawner: Rc::new(spawner),
systems: RwLock::default(), systems: RwLock::default(),
system_id: RefCell::new(NonZero::new(1).unwrap()), system_id: RefCell::new(NonZero::new(1).unwrap()),
exprs: ExprStore::default(), exprs: ExprStore::default(),
root: RwLock::default(), root: RwLock::default(),
logger,
})) }))
} }
/// Spawn a parallel future that you can join at any later time. /// 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::error::OrcErrv;
use orchid_base::format::fmt; use orchid_base::format::fmt;
use orchid_base::location::Pos; use orchid_base::location::Pos;
use orchid_base::logging::logger; use orchid_base::logging::log;
use crate::expr::{Expr, ExprKind, PathSet, Step}; use crate::expr::{Expr, ExprKind, PathSet, Step};
use crate::tree::Root; use crate::tree::Root;
@@ -86,7 +86,7 @@ impl ExecCtx {
while self.use_gas(1) { while self.use_gas(1) {
let mut kind_swap = ExprKind::Missing; let mut kind_swap = ExprKind::Missing;
mem::swap(&mut kind_swap, &mut self.cur); 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 { let (kind, op) = match kind_swap {
ExprKind::Identity(target) => { ExprKind::Identity(target) => {
let inner = self.unpack_ident(&target).await; 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 hashbrown::{HashMap, HashSet};
use itertools::Itertools; use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request}; use orchid_api_traits::{Decode, Encode, Request};
use orchid_base::clone;
use orchid_base::format::{FmtCtxImpl, Format}; use orchid_base::format::{FmtCtxImpl, Format};
use orchid_base::interner::{IStr, IStrv, es, ev, is, iv}; use orchid_base::interner::{IStr, IStrv, es, ev, is, iv};
use orchid_base::location::Pos; use orchid_base::location::Pos;
use orchid_base::logging::logger; use orchid_base::logging::log;
use orchid_base::name::Sym; use orchid_base::name::Sym;
use orchid_base::reqnot::{Client, ClientExt, MsgReaderExt, ReqHandleExt, ReqReaderExt, io_comm}; use orchid_base::reqnot::{Client, ClientExt, MsgReaderExt, ReqHandleExt, ReqReaderExt, io_comm};
use orchid_base::stash::{stash, with_stash}; use orchid_base::stash::{stash, with_stash};
@@ -70,38 +69,42 @@ impl Drop for ExtensionData {
pub struct Extension(Rc<ExtensionData>); pub struct Extension(Rc<ExtensionData>);
impl Extension { impl Extension {
pub async fn new(mut init: ExtPort, ctx: Ctx) -> io::Result<Self> { pub async fn new(mut init: ExtPort, ctx: Ctx) -> io::Result<Self> {
api::HostHeader { log_strategy: logger().strat(), msg_logs: ctx.msg_logs.strat() } api::HostHeader { logger: ctx.logger.to_api() }.encode(init.input.as_mut()).await.unwrap();
.encode(init.input.as_mut())
.await
.unwrap();
init.input.flush().await.unwrap(); init.input.flush().await.unwrap();
let header = api::ExtensionHeader::decode(init.output.as_mut()).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>| { Ok(Self(Rc::new_cyclic(|weak: &Weak<ExtensionData>| {
// context not needed because exit is extension-initiated // context not needed because exit is extension-initiated
let (client, _, future) = io_comm( let (client, _, comm) = io_comm(Rc::new(Mutex::new(init.input)), Mutex::new(init.output));
Rc::new(Mutex::new(init.input)), let weak2 = weak;
Mutex::new(init.output), let weak = weak.clone();
clone!(weak; async move |reader| { let ctx2 = ctx.clone();
let join_ext = ctx.clone().spawn(async move {
comm
.listen(
async |reader| {
with_stash(async { with_stash(async {
let this = Extension(weak.upgrade().unwrap()); let this = Extension(weak.upgrade().unwrap());
let notif = reader.read::<api::ExtHostNotif>().await.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(_)) { if !matches!(notif, api::ExtHostNotif::Log(_)) {
writeln!(logger(), "Host received notif {notif:?}"); writeln!(log("msg"), "Host received notif {notif:?}").await;
} }
match notif { match notif {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Acquire(acq)) => { api::ExtHostNotif::ExprNotif(api::ExprNotif::Acquire(acq)) => {
let target = this.0.ctx.exprs.get_expr(acq.1).expect("Invalid ticket"); let target = this.0.ctx.exprs.get_expr(acq.1).expect("Invalid ticket");
this.0.ctx.exprs.give_expr(target) this.0.ctx.exprs.give_expr(target)
} },
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => { api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
if this.is_own_sys(rel.0).await { if this.is_own_sys(rel.0).await {
this.0.ctx.exprs.take_expr(rel.1); this.0.ctx.exprs.take_expr(rel.1);
} else { } else {
writeln!(this.0.ctx.msg_logs, "Not our system {:?}", rel.0) writeln!(log("warn"), "Not our system {:?}", rel.0).await
} }
}, },
api::ExtHostNotif::Log(api::Log(str)) => logger().log(str), api::ExtHostNotif::Log(api::Log { category, message }) =>
write!(log(&es(category).await), "{message}").await,
api::ExtHostNotif::Sweeped(data) => { api::ExtHostNotif::Sweeped(data) => {
for i in join_all(data.strings.into_iter().map(es)).await { for i in join_all(data.strings.into_iter().map(es)).await {
this.0.strings.borrow_mut().remove(&i); this.0.strings.borrow_mut().remove(&i);
@@ -112,18 +115,21 @@ impl Extension {
}, },
} }
Ok(()) Ok(())
}).await })
}), .await
{ },
clone!(weak, ctx); async |mut reader| {
async move |mut reader| {
with_stash(async { with_stash(async {
let this = Self(weak.upgrade().unwrap());
let req = reader.read_req::<api::ExtHostReq>().await.unwrap(); let req = reader.read_req::<api::ExtHostReq>().await.unwrap();
let handle = reader.finish().await; let handle = reader.finish().await;
if !matches!(req, api::ExtHostReq::ExtAtomPrint(_)) { // Atom printing and interning is never reported because it generates too much
writeln!(logger(), "Host received request {req:?}"); // 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 { match req {
api::ExtHostReq::Ping(ping) => handle.reply(&ping, &()).await, api::ExtHostReq::Ping(ping) => handle.reply(&ping, &()).await,
api::ExtHostReq::IntReq(intreq) => match intreq { api::ExtHostReq::IntReq(intreq) => match intreq {
@@ -153,7 +159,8 @@ impl Extension {
}, },
}, },
api::ExtHostReq::Fwd(ref fw @ api::Fwd(ref atom, ref key, ref body)) => { 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 sys =
ctx.system_inst(atom.owner).await.expect("owner of live atom dropped");
let client = sys.client(); let client = sys.client();
let reply = let reply =
client.request(api::Fwded(fw.0.clone(), *key, body.clone())).await.unwrap(); client.request(api::Fwded(fw.0.clone(), *key, body.clone())).await.unwrap();
@@ -199,7 +206,8 @@ impl Extension {
let root_data = &*root.0.read().await; let root_data = &*root.0.read().await;
let mut walk_ctx = (ctx.clone(), &root_data.consts); let mut walk_ctx = (ctx.clone(), &root_data.consts);
let module = let module =
match walk(&root_data.root, false, path.iter().cloned(), &mut walk_ctx).await match walk(&root_data.root, false, path.iter().cloned(), &mut walk_ctx)
.await
{ {
Ok(module) => module, Ok(module) => module,
Err(ChildError { kind, .. }) => Err(ChildError { kind, .. }) =>
@@ -232,8 +240,16 @@ impl Extension {
let responses = stream(async |mut cx| { let responses = stream(async |mut cx| {
for name in names { for name in names {
cx.emit(match resolver(&ev(*name).await[..]).await { cx.emit(match resolver(&ev(*name).await[..]).await {
Ok(abs) => Ok(abs.to_sym().await.to_api()), Ok(abs) => {
Err(e) => Err(e.to_api()), 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 .await
} }
@@ -250,19 +266,16 @@ impl Extension {
} }
}) })
.await .await
}
}, },
); )
.await
let join_ext = ctx.spawn(async { .unwrap();
future.await.unwrap();
// extension exited successfully
}); });
ExtensionData { ExtensionData {
name: header.name.clone(), name: header2.name.clone(),
ctx: ctx.clone(), ctx: ctx2,
systems: (header.systems.iter().cloned()) systems: (header.systems.iter().cloned())
.map(|decl| SystemCtor { decl, ext: WeakExtension(weak.clone()) }) .map(|decl| SystemCtor { decl, ext: WeakExtension(weak2.clone()) })
.collect(), .collect(),
join_ext: Some(join_ext), join_ext: Some(join_ext),
next_pars: RefCell::new(NonZeroU64::new(1).unwrap()), next_pars: RefCell::new(NonZeroU64::new(1).unwrap()),
@@ -282,7 +295,7 @@ impl Extension {
#[must_use] #[must_use]
pub async fn is_own_sys(&self, id: api::SysId) -> bool { pub async fn is_own_sys(&self, id: api::SysId) -> bool {
let Some(sys) = self.ctx().system_inst(id).await else { 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; return false;
}; };
Rc::ptr_eq(&self.0, &sys.ext().0) Rc::ptr_eq(&self.0, &sys.ext().0)

View File

@@ -3,11 +3,13 @@ use orchid_api as api;
pub mod atom; pub mod atom;
pub mod ctx; pub mod ctx;
pub mod dealias; pub mod dealias;
pub mod dylib;
pub mod execute; pub mod execute;
pub mod expr; pub mod expr;
pub mod expr_store; pub mod expr_store;
pub mod extension; pub mod extension;
pub mod lex; pub mod lex;
pub mod logger;
pub mod parse; pub mod parse;
pub mod parsed; pub mod parsed;
pub mod subprocess; 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::io::BufReader;
use futures::{self, AsyncBufReadExt}; use futures::{self, AsyncBufReadExt, StreamExt};
use orchid_base::logging::logger; use orchid_base::logging::log;
#[cfg(feature = "tokio")]
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::ctx::Ctx; use crate::ctx::Ctx;
use crate::extension::ExtPort; use crate::extension::ExtPort;
#[cfg(feature = "tokio")]
pub async fn ext_command(cmd: std::process::Command, ctx: Ctx) -> io::Result<ExtPort> { pub async fn ext_command(cmd: std::process::Command, ctx: Ctx) -> io::Result<ExtPort> {
let mut child = async_process::Command::from(cmd) let name = cmd.get_program().to_string_lossy().to_string();
.stdin(async_process::Stdio::piped()) let mut child = tokio::process::Command::from(cmd)
.stdout(async_process::Stdio::piped()) .stdin(process::Stdio::piped())
.stderr(async_process::Stdio::piped()) .stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.spawn()?; .spawn()?;
std::thread::spawn(|| {});
let stdin = child.stdin.take().unwrap(); let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.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 _ = ctx.spawn(Box::pin(async move {
let _ = child; let _ = child;
let mut reader = BufReader::new(&mut child_stderr); let mut lines = BufReader::new(child_stderr.compat()).lines();
loop { while let Some(line) = lines.next().await {
let mut buf = String::new(); // route stderr with an empty category string. This is not the intended logging
if 0 == reader.read_line(&mut buf).await.unwrap() { // method
break; writeln!(log("stderr"), "{} err> {}", name, line.expect("Readline implies this")).await;
}
logger().log(buf.strip_suffix('\n').expect("Readline implies this"));
} }
})); }));
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::format::{FmtCtx, FmtUnit, Format};
use orchid_base::interner::{IStr, es, is}; use orchid_base::interner::{IStr, es, is};
use orchid_base::iter_utils::IteratorPrint; 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::name::{NameLike, Sym, VName, VPath};
use orchid_base::reqnot::{Client, ClientExt}; use orchid_base::reqnot::{Client, ClientExt};
use orchid_base::stash::stash;
use ordered_float::NotNan; use ordered_float::NotNan;
use substack::{Stackframe, Substack}; use substack::{Stackframe, Substack};
@@ -86,7 +87,8 @@ impl System {
#[must_use] #[must_use]
pub fn can_lex(&self, c: char) -> bool { pub fn can_lex(&self, c: char) -> bool {
let ret = char_filter_match(&self.0.lex_filter, c); 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 ret
} }
#[must_use] #[must_use]

View File

@@ -3,11 +3,19 @@ name = "orchid-std"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "orchid-std"
path = "src/main.rs"
[lib]
crate-type = ["cdylib", "lib"]
path = "src/lib.rs"
[dependencies] [dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" } async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4" async-once-cell = "0.5.4"
futures = { version = "0.3.31", features = ["std"], default-features = false } futures = { version = "0.3.31", features = ["std"], default-features = false }
hashbrown = "0.16.0" hashbrown = "0.16.1"
itertools = "0.14.0" itertools = "0.14.0"
never = "0.1.0" never = "0.1.0"
once_cell = "1.21.3" 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 = [ orchid-extension = { version = "0.1.0", path = "../orchid-extension", features = [
"tokio", "tokio",
] } ] }
ordered-float = "5.0.0" ordered-float = "5.1.0"
pastey = "0.1.1" pastey = "0.2.1"
rust_decimal = "1.38.0" rust_decimal = "1.39.0"
subslice-offset = "0.1.1" subslice-offset = "0.1.1"
substack = "1.1.1" substack = "1.1.1"
tokio = { version = "1.47.1", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
[dev-dependencies] [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::macro_system::MacroSystem;
pub use macros::mactree::{MacTok, MacTree}; pub use macros::mactree::{MacTok, MacTree};
use orchid_api as api; 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::format::fmt;
use orchid_base::interner::is; use orchid_base::interner::is;
use orchid_base::location::Pos; 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::name::{NameLike, Sym, VPath};
use orchid_base::tree::Paren; use orchid_base::tree::Paren;
use orchid_extension::atom::TAtom; use orchid_extension::atom::TAtom;
@@ -27,9 +27,7 @@ use crate::{MacTok, MacTree};
pub async fn resolve(val: MacTree) -> GExpr { pub async fn resolve(val: MacTree) -> GExpr {
exec(async move |mut h| { exec(async move |mut h| {
// if ctx.logger().is_active() { writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await).await;
writeln!(logger(), "Macro-resolving {}", fmt(&val).await);
// }
let root = refl(); let root = refl();
let mut macros = HashMap::new(); let mut macros = HashMap::new();
for n in val.glossary() { for n in val.glossary() {
@@ -67,7 +65,13 @@ pub async fn resolve(val: MacTree) -> GExpr {
} }
let mut rctx = ResolveCtx { h, exclusive, priod }; let mut rctx = ResolveCtx { h, exclusive, priod };
let gex = resolve_one(&mut rctx, Substack::Bottom, &val).await; 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 gex
}) })
.await .await

View File

@@ -7,13 +7,15 @@ edition = "2024"
[dependencies] [dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" } async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
camino = "1.1.9" camino = "1.2.2"
clap = { version = "4.5.24", features = ["derive", "env"] } clap = { version = "4.5.54", features = ["derive", "env"] }
ctrlc = "3.4.5" ctrlc = "3.5.1"
futures = "0.3.31" futures = "0.3.31"
itertools = "0.14.0" itertools = "0.14.0"
orchid-api = { version = "0.1.0", path = "../orchid-api" } orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-base = { version = "0.1.0", path = "../orchid-base" } 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" 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; pub mod parse_folder;
use std::cell::RefCell; 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::local_interner::local_interner;
use orchid_base::interner::{is, with_interner}; use orchid_base::interner::{is, with_interner};
use orchid_base::location::SrcRange; 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::name::{NameLike, VPath};
use orchid_base::parse::{Import, Snippet}; use orchid_base::parse::{Import, Snippet};
use orchid_base::stash::with_stash;
use orchid_base::sym; use orchid_base::sym;
use orchid_base::tree::{Token, ttv_fmt}; use orchid_base::tree::{Token, ttv_fmt};
use orchid_host::ctx::{Ctx, JoinHandle, Spawner}; 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::expr::ExprKind;
use orchid_host::extension::Extension; use orchid_host::extension::Extension;
use orchid_host::lex::lex; use orchid_host::lex::lex;
use orchid_host::logger::LoggerImpl;
use orchid_host::parse::{HostParseCtxImpl, parse_item, parse_items}; use orchid_host::parse::{HostParseCtxImpl, parse_item, parse_items};
use orchid_host::parsed::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule}; use orchid_host::parsed::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule};
use orchid_host::subprocess::ext_command; use orchid_host::subprocess::ext_command;
@@ -45,12 +49,12 @@ pub struct Args {
extension: Vec<Utf8PathBuf>, extension: Vec<Utf8PathBuf>,
#[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')] #[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')]
system: Vec<String>, system: Vec<String>,
#[arg(short, long)] #[arg(short, long, default_value = "off", default_missing_value = "stderr")]
logs: bool, logs: Vec<String>,
#[arg(short, long)]
msg_logs: bool,
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
#[arg(long, action)]
time: bool,
} }
#[derive(Subcommand, Debug)] #[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>( fn get_all_extensions<'a>(
args: &'a Args, args: &'a Args,
ctx: &'a Ctx, 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<()>); struct JoinHandleImpl(tokio::task::JoinHandle<()>);
impl JoinHandle for JoinHandleImpl { impl JoinHandle for JoinHandleImpl {
fn abort(&self) { self.0.abort() } fn abort(&self) { self.0.abort() }
@@ -109,18 +159,20 @@ impl Spawner for SpawnerImpl {
#[tokio::main] #[tokio::main]
async fn main() -> io::Result<ExitCode> { 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 exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS));
let local_set = LocalSet::new(); let local_set = LocalSet::new();
let exit_code1 = exit_code.clone(); let exit_code1 = exit_code.clone();
let args = Args::parse(); let logger = get_logger(&args);
let logger = Logger::new(if args.logs { LogStrategy::StdErr } else { LogStrategy::Discard }); let logger2 = logger.clone();
let cx_logger = logger.clone(); unsafe { STARTUP = Some(Instant::now()) };
let msg_logger =
Logger::new(if args.msg_logs { LogStrategy::StdErr } else { LogStrategy::Discard });
local_set.spawn_local(async move { local_set.spawn_local(async move {
let ctx = &Ctx::new(msg_logger.clone(), SpawnerImpl); let ctx = &Ctx::new(SpawnerImpl, logger2);
let extensions = get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap(); with_stash(async {
let extensions =
get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap();
time_print(&args, "Extensions loaded");
match args.command { match args.command {
Commands::Lex { file } => { Commands::Lex { file } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap(); let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
@@ -174,18 +226,19 @@ async fn main() -> io::Result<ExitCode> {
let mut lexemes = let mut lexemes =
lex(is(prompt.trim()).await, path.clone(), &systems, ctx).await.unwrap(); lex(is(prompt.trim()).await, path.clone(), &systems, ctx).await.unwrap();
let Some(discr) = lexemes.first() else { continue }; let Some(discr) = lexemes.first() else { continue };
if args.logs { writeln!(
println!( log("debug"),
"lexed: {}", "lexed: {}",
take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true) take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true)
); )
} .await;
let prefix_sr = SrcRange::zw(path.clone(), 0); let prefix_sr = SrcRange::zw(path.clone(), 0);
let process_lexemes = async |lexemes: &[ParsTokTree]| { let process_lexemes = async |lexemes: &[ParsTokTree]| {
let snippet = Snippet::new(&lexemes[0], lexemes); let snippet = Snippet::new(&lexemes[0], lexemes);
let parse_ctx = let parse_ctx =
HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] }; HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] };
match try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet)).await match try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
.await
{ {
Ok(items) => Some(items), Ok(items) => Some(items),
Err(e) => { Err(e) => {
@@ -195,7 +248,8 @@ async fn main() -> io::Result<ExitCode> {
} }
}; };
let add_imports = |items: &mut Vec<Item>, imports: &[Import]| { let add_imports = |items: &mut Vec<Item>, imports: &[Import]| {
items.extend(imports.iter().map(|import| Item::new(import.sr.clone(), import.clone()))); items
.extend(imports.iter().map(|import| Item::new(import.sr.clone(), import.clone())));
}; };
if discr.is_kw(is("import").await) { if discr.is_kw(is("import").await) {
let Some(import_lines) = process_lexemes(&lexemes).await else { continue }; let Some(import_lines) = process_lexemes(&lexemes).await else { continue };
@@ -217,7 +271,11 @@ async fn main() -> io::Result<ExitCode> {
_ => panic!("Expected exactly one constant declaration from let"), _ => panic!("Expected exactly one constant declaration from let"),
}; };
add_imports(&mut new_lines, &imports); add_imports(&mut new_lines, &imports);
imports.push(Import::new(input_sr.clone(), VPath::new(path.segs()), const_name.clone())); imports.push(Import::new(
input_sr.clone(),
VPath::new(path.segs()),
const_name.clone(),
));
let new_module = ParsedModule::new(true, new_lines); let new_module = ParsedModule::new(true, new_lines);
match with_reporter(root.add_parsed(&new_module, path.clone())).await { match with_reporter(root.add_parsed(&new_module, path.clone())).await {
Ok(new) => root = new, Ok(new) => root = new,
@@ -291,7 +349,6 @@ async fn main() -> io::Result<ExitCode> {
} }
}, },
Commands::Exec { proj, code } => { Commands::Exec { proj, code } => {
eprintln!("exec branch");
let path = sym!(usercode); let path = sym!(usercode);
let prefix_sr = SrcRange::zw(path.clone(), 0); let prefix_sr = SrcRange::zw(path.clone(), 0);
let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap(); let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap();
@@ -308,9 +365,12 @@ async fn main() -> io::Result<ExitCode> {
} }
let mut lexemes = match lex(is(code.trim()).await, path.clone(), &systems, ctx).await { let mut lexemes = match lex(is(code.trim()).await, path.clone(), &systems, ctx).await {
Ok(lexemes) => { Ok(lexemes) => {
if args.logs { writeln!(
println!("lexed: {}", fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")); log("debug"),
} "lexed: {}",
fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")
)
.await;
lexemes lexemes
}, },
Err(e) => { Err(e) => {
@@ -359,8 +419,10 @@ async fn main() -> io::Result<ExitCode> {
} }
}, },
} }
})
.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(); let x = *exit_code.borrow();
Ok(x) Ok(x)
} }

View File

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

View File

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