Traditional route appears to work
Beginnings of dylib extensions, entirely untestted
This commit is contained in:
770
Cargo.lock
generated
770
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
118
orchid-base/src/binary.rs
Normal 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 } }
|
||||
@@ -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]) }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
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"),
|
||||
);
|
||||
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 {
|
||||
)
|
||||
.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)),
|
||||
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
|
||||
},
|
||||
);
|
||||
let (client, client_ctx, run_client) = io_comm(
|
||||
Rc::new(Mutex::new(Box::pin(in1))),
|
||||
Mutex::new(Box::pin(out1)),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
async {
|
||||
client_srv
|
||||
.listen(
|
||||
async |_| panic!("Not expecting ingress notif"),
|
||||
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();
|
||||
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();
|
||||
}
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
30
orchid-extension/src/binary.rs
Normal file
30
orchid-extension/src/binary.rs
Normal 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)),
|
||||
});
|
||||
}
|
||||
@@ -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<_>>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
57
orchid-extension/src/logger.rs
Normal file
57
orchid-extension/src/logger.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
55
orchid-host/src/dylib.rs
Normal 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) })
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,38 +69,42 @@ 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| {
|
||||
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!(logger(), "Host received notif {notif:?}");
|
||||
writeln!(log("msg"), "Host received notif {notif:?}").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::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)
|
||||
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) => {
|
||||
for i in join_all(data.strings.into_iter().map(es)).await {
|
||||
this.0.strings.borrow_mut().remove(&i);
|
||||
@@ -112,18 +115,21 @@ impl Extension {
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}).await
|
||||
}),
|
||||
{
|
||||
clone!(weak, ctx);
|
||||
async move |mut reader| {
|
||||
})
|
||||
.await
|
||||
},
|
||||
async |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:?}");
|
||||
// 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 {
|
||||
@@ -153,7 +159,8 @@ impl Extension {
|
||||
},
|
||||
},
|
||||
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 reply =
|
||||
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 mut walk_ctx = (ctx.clone(), &root_data.consts);
|
||||
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,
|
||||
Err(ChildError { kind, .. }) =>
|
||||
@@ -232,8 +240,16 @@ impl Extension {
|
||||
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()),
|
||||
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
|
||||
}
|
||||
@@ -250,19 +266,16 @@ impl Extension {
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let join_ext = ctx.spawn(async {
|
||||
future.await.unwrap();
|
||||
// extension exited successfully
|
||||
)
|
||||
.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)
|
||||
|
||||
@@ -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
84
orchid-host/src/logger.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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()) })
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
112
orcx/src/main.rs
112
orcx/src/main.rs
@@ -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,18 +159,20 @@ 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();
|
||||
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();
|
||||
@@ -174,18 +226,19 @@ async fn main() -> io::Result<ExitCode> {
|
||||
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!(
|
||||
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
|
||||
match try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
|
||||
.await
|
||||
{
|
||||
Ok(items) => Some(items),
|
||||
Err(e) => {
|
||||
@@ -195,7 +248,8 @@ async fn main() -> io::Result<ExitCode> {
|
||||
}
|
||||
};
|
||||
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) {
|
||||
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"),
|
||||
};
|
||||
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);
|
||||
match with_reporter(root.add_parsed(&new_module, path.clone())).await {
|
||||
Ok(new) => root = new,
|
||||
@@ -291,7 +349,6 @@ async fn main() -> io::Result<ExitCode> {
|
||||
}
|
||||
},
|
||||
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();
|
||||
@@ -308,9 +365,12 @@ async fn main() -> io::Result<ExitCode> {
|
||||
}
|
||||
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(" "));
|
||||
}
|
||||
writeln!(
|
||||
log("debug"),
|
||||
"lexed: {}",
|
||||
fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")
|
||||
)
|
||||
.await;
|
||||
lexemes
|
||||
},
|
||||
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();
|
||||
Ok(x)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
Reference in New Issue
Block a user