Compare commits

7 Commits

102 changed files with 3037 additions and 2209 deletions

View File

@@ -13,3 +13,12 @@ RUST_BACKTRACE = "1"
[build]
# rustflags = ["-Znext-solver"]
[profile.dev]
opt-level = 0
debug = 2
strip = 'none'
debug-assertions = true
overflow-checks = true
lto = false
panic = 'abort'

View File

@@ -1,29 +0,0 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install rust toolchain
run: curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
- name: Build
run: $HOME/.cargo/bin/cargo build --verbose
- name: Run tests
run: $HOME/.cargo/bin/cargo test --verbose
- name: Clippy
run: $HOME/.cargo/bin/cargo clippy
- name: Formatting
run: $HOME/.cargo/bin/cargo +nightly fmt --check
- name: No unqualified imports from orchid_api
run: $HOME/.cargo/bin/cargo xtask check-api-refs

51
Cargo.lock generated
View File

@@ -92,6 +92,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "ar_archive_writer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
dependencies = [
"object",
]
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -297,9 +306,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.43"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -994,6 +1003,15 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -1087,6 +1105,7 @@ dependencies = [
"async-event",
"async-fn-stream",
"async-once-cell",
"chrono",
"derive_destructure",
"dyn-clone",
"futures",
@@ -1122,6 +1141,7 @@ dependencies = [
"async-fn-stream",
"async-once-cell",
"bound",
"chrono",
"derive_destructure",
"futures",
"futures-locks",
@@ -1184,11 +1204,15 @@ dependencies = [
"ctrlc",
"futures",
"itertools",
"never",
"orchid-api",
"orchid-base",
"orchid-extension",
"orchid-host",
"stacker",
"substack",
"tokio",
"tokio-util",
]
[[package]]
@@ -1293,6 +1317,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
dependencies = [
"ar_archive_writer",
"cc",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
@@ -1731,6 +1765,19 @@ dependencies = [
"web-time",
]
[[package]]
name = "stacker"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"windows-sys 0.59.0",
]
[[package]]
name = "stdio-perftest"
version = "0.1.0"

View File

@@ -2,3 +2,14 @@ let user = r[ "foo" 1, "bar" t[3, 4] ]
let _main = user.bar.1
let main = "foo" + string::slice "hello" 1 3 + "bar"
let io_main = (
stdio::get_stdout \stdout
std::stream::write_str stdout "Hello, World!"
(std::stream::flush
(std::stream::close
orchid::cmd::exit
\e e)
\e e)
\e e
)

View File

@@ -0,0 +1,2 @@
[profile.dev]
panic = 'unwind'

View File

@@ -376,3 +376,13 @@ impl Encode for chrono::TimeDelta {
self.subsec_nanos().encode(write).await
}
}
impl Decode for chrono::DateTime<chrono::Utc> {
async fn decode<R: AsyncRead + ?Sized>(read: Pin<&mut R>) -> io::Result<Self> {
Ok(Self::from_timestamp_micros(i64::decode(read).await?).unwrap())
}
}
impl Encode for chrono::DateTime<chrono::Utc> {
async fn encode<W: AsyncWrite + ?Sized>(&self, write: Pin<&mut W>) -> io::Result<()> {
self.timestamp_micros().encode(write).await
}
}

View File

@@ -95,14 +95,18 @@ impl Request for DeserAtom {
/// A request blindly routed to the system that provides an atom.
#[derive(Clone, Debug, Coding, Hierarchy)]
#[extends(AtomReq, HostExtReq)]
pub struct FinalFwded(pub Atom, pub TStrv, pub Vec<u8>);
impl Request for FinalFwded {
pub struct Fwded(pub Atom, pub TStrv, pub Vec<u8>);
impl Request for Fwded {
type Response = Option<Vec<u8>>;
}
#[derive(Clone, Debug, Coding, Hierarchy)]
#[extends(ExtHostReq)]
pub struct Fwd(pub Atom, pub TStrv, pub Vec<u8>);
pub struct Fwd {
pub target: Atom,
pub method: TStrv,
pub body: Vec<u8>,
}
impl Request for Fwd {
type Response = Option<Vec<u8>>;
}
@@ -138,8 +142,7 @@ impl Request for ExtAtomPrint {
pub enum AtomReq {
CallRef(CallRef),
FinalCall(FinalCall),
FwdedRef(FinalFwded),
FinalFwded(FinalFwded),
Fwded(Fwded),
AtomPrint(AtomPrint),
SerializeAtom(SerializeAtom),
}
@@ -150,8 +153,7 @@ impl AtomReq {
match self {
Self::CallRef(CallRef(a, ..))
| Self::FinalCall(FinalCall(a, ..))
| Self::FwdedRef(FinalFwded(a, ..))
| Self::FinalFwded(FinalFwded(a, ..))
| Self::Fwded(Fwded(a, ..))
| Self::AtomPrint(AtomPrint(a))
| Self::SerializeAtom(SerializeAtom(a)) => a,
}

View File

@@ -6,7 +6,6 @@
//! 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

View File

@@ -4,7 +4,9 @@ use std::num::NonZeroU64;
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
use crate::{Atom, ExtHostNotif, ExtHostReq, LocalAtom, Location, OrcError, SysId, TStrv};
use crate::{
Atom, ExtHostNotif, ExtHostReq, FormattingUnit, LocalAtom, Location, OrcError, SysId, TStrv,
};
/// An arbitrary ID associated with an expression on the host side. Incoming
/// tickets always come with some lifetime guarantee, which can be extended with
@@ -104,11 +106,21 @@ impl Request for Inspect {
type Response = Inspected;
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Coding, Hierarchy)]
#[extends(ExprReq, ExtHostReq)]
pub struct ExprPrint {
pub target: ExprTicket,
}
impl Request for ExprPrint {
type Response = FormattingUnit;
}
#[derive(Clone, Debug, Coding, Hierarchy)]
#[extends(ExtHostReq)]
#[extendable]
pub enum ExprReq {
Inspect(Inspect),
ExprPrint(ExprPrint),
Create(Create),
}

View File

@@ -0,0 +1,87 @@
use std::pin::Pin;
use std::task::{Context, Poll};
/// Future returned by [cancel_cleanup]
pub struct CancelCleanup<Fut: Future + Unpin, Fun: FnOnce(Fut)> {
/// Set to None when Ready
fut: Option<Fut>,
/// Only set to None in Drop
on_drop: Option<Fun>,
}
impl<Fut: Future + Unpin, Fun: FnOnce(Fut)> Future for CancelCleanup<Fut, Fun> {
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let Self { fut, .. } = unsafe { self.get_unchecked_mut() };
if let Some(future) = fut {
let future = unsafe { Pin::new_unchecked(future) };
let poll = future.poll(cx);
if poll.is_ready() {
*fut = None;
}
poll
} else {
Poll::Pending
}
}
}
impl<Fut: Future + Unpin, Fun: FnOnce(Fut)> Drop for CancelCleanup<Fut, Fun> {
fn drop(&mut self) {
if let Some(fut) = self.fut.take() {
(self.on_drop.take().unwrap())(fut)
}
}
}
/// Handle a Future's Drop. The callback is only called if the future has not
/// yet returned and would be cancelled, and it receives the future as an
/// argument
pub fn cancel_cleanup<Fut: Future + Unpin, Fun: FnOnce(Fut)>(
fut: Fut,
on_drop: Fun,
) -> CancelCleanup<Fut, Fun> {
CancelCleanup { fut: Some(fut), on_drop: Some(on_drop) }
}
#[cfg(test)]
mod test {
use std::pin::pin;
use futures::channel::mpsc;
use futures::future::join;
use futures::{SinkExt, StreamExt};
use super::*;
use crate::debug::spin_on;
#[test]
fn called_on_drop() {
let mut called = false;
cancel_cleanup(pin!(async {}), |_| called = true);
assert!(called, "cleanup was called when the future was dropped");
}
#[test]
fn not_called_if_finished() {
spin_on(false, async {
let (mut req_in, mut req_out) = mpsc::channel(0);
let (mut rep_in, mut rep_out) = mpsc::channel(0);
join(
async {
req_out.next().await.unwrap();
rep_in.send(()).await.unwrap();
},
async {
cancel_cleanup(
pin!(async {
req_in.send(()).await.unwrap();
rep_out.next().await.unwrap();
}),
|_| panic!("Callback called on drop even though the future was finished"),
)
.await
},
)
.await
});
}
}

View File

@@ -143,9 +143,17 @@ pub fn eprint_stream_events<'a, S: Stream + 'a>(
)
}
struct SpinWaker(AtomicBool);
struct SpinWaker {
repeat: AtomicBool,
loud: bool,
}
impl Wake for SpinWaker {
fn wake(self: Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
fn wake(self: Arc<Self>) {
self.repeat.store(true, Ordering::SeqCst);
if self.loud {
eprintln!("Triggered repeat for spin_on")
}
}
}
/// A dumb executor that keeps synchronously re-running the future as long as it
@@ -155,15 +163,15 @@ impl Wake for SpinWaker {
/// # Panics
///
/// If the future doesn't wake itself and doesn't settle.
pub fn spin_on<Fut: Future>(f: Fut) -> Fut::Output {
let repeat = Arc::new(SpinWaker(AtomicBool::new(false)));
pub fn spin_on<Fut: Future>(loud: bool, f: Fut) -> Fut::Output {
let spin_waker = Arc::new(SpinWaker { repeat: AtomicBool::new(false), loud });
let mut f = pin!(f);
let waker = repeat.clone().into();
let waker = spin_waker.clone().into();
let mut cx = Context::from_waker(&waker);
loop {
match f.as_mut().poll(&mut cx) {
Poll::Ready(t) => break t,
Poll::Pending if repeat.0.swap(false, Ordering::Relaxed) => (),
Poll::Pending if spin_waker.repeat.swap(false, Ordering::SeqCst) => (),
Poll::Pending => panic!("The future did not exit and did not call its waker."),
}
}

View File

@@ -1,5 +1,7 @@
pub mod debug;
mod cancel_cleanup;
pub use cancel_cleanup::*;
mod localset;
pub use localset::*;
mod task_future;
pub use task_future::*;
pub use task_future::*;

View File

@@ -1,21 +1,35 @@
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::Poll;
use futures::StreamExt;
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
use futures::channel::mpsc::{SendError, UnboundedReceiver, UnboundedSender, unbounded};
use futures::future::LocalBoxFuture;
use futures::stream::FuturesUnordered;
use futures::{SinkExt, StreamExt};
pub struct LocalSet<'a, E> {
receiver: UnboundedReceiver<LocalBoxFuture<'a, Result<(), E>>>,
pending: VecDeque<LocalBoxFuture<'a, Result<(), E>>>,
pub struct LocalSetController<'a, E> {
sender: UnboundedSender<LocalBoxFuture<'a, Result<(), E>>>,
}
impl<'a, E> LocalSet<'a, E> {
pub fn new() -> (UnboundedSender<LocalBoxFuture<'a, Result<(), E>>>, Self) {
let (sender, receiver) = unbounded();
(sender, Self { receiver, pending: VecDeque::new() })
impl<'a, E> LocalSetController<'a, E> {
pub async fn spawn<F: Future<Output = Result<(), E>> + 'a>(
&mut self,
fut: F,
) -> Result<(), SendError> {
self.sender.send(Box::pin(fut)).await
}
}
pub fn local_set<'a, E: 'a>()
-> (LocalSetController<'a, E>, impl Future<Output = Result<(), E>> + 'a) {
let (sender, receiver) = unbounded();
let controller = LocalSetController { sender };
let set = LocalSet { receiver, pending: FuturesUnordered::new() };
(controller, set)
}
struct LocalSet<'a, E> {
receiver: UnboundedReceiver<LocalBoxFuture<'a, Result<(), E>>>,
pending: FuturesUnordered<LocalBoxFuture<'a, Result<(), E>>>,
}
impl<E> Future for LocalSet<'_, E> {
type Output = Result<(), E>;
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
@@ -23,7 +37,7 @@ impl<E> Future for LocalSet<'_, E> {
let mut any_pending = false;
loop {
match this.receiver.poll_next_unpin(cx) {
Poll::Ready(Some(fut)) => this.pending.push_back(fut),
Poll::Ready(Some(fut)) => this.pending.push(fut),
Poll::Ready(None) => break,
Poll::Pending => {
any_pending = true;
@@ -31,15 +45,14 @@ impl<E> Future for LocalSet<'_, E> {
},
}
}
let count = this.pending.len();
for _ in 0..count {
let mut req = this.pending.pop_front().unwrap();
match req.as_mut().poll(cx) {
Poll::Ready(Ok(())) => (),
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
loop {
match this.pending.poll_next_unpin(cx) {
Poll::Ready(Some(Err(e))) => return Poll::Ready(Err(e)),
Poll::Ready(Some(Ok(()))) => continue,
Poll::Ready(None) => break,
Poll::Pending => {
any_pending = true;
this.pending.push_back(req)
break;
},
}
}

View File

@@ -1,10 +1,11 @@
use std::any::Any;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::pin::Pin;
use std::pin::{Pin, pin};
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use futures::FutureExt;
use futures::channel::oneshot::{self, Canceled};
use futures::future::{FusedFuture, LocalBoxFuture};
struct State {
@@ -43,50 +44,56 @@ impl Future for Pollable {
}
}
pub struct JoinError;
/// An object that can be used to inspect the state of the task
pub struct Handle<T: 'static>(Rc<RefCell<State>>, PhantomData<T>);
pub struct Handle<T: 'static> {
send_abort: RefCell<Option<oneshot::Sender<()>>>,
ready: Rc<RefCell<bool>>,
recv_output: RefCell<oneshot::Receiver<T>>,
}
impl<T: 'static> Handle<T> {
/// Immediately stop working on this task, and return the result if it has
/// already finished
pub fn abort(&self) -> Option<T> {
let mut g = self.0.borrow_mut();
g.work.take();
match g.result.take() {
Some(val) => Some(*val.downcast().expect("Mismatch between type of future and handle")),
None => {
g.waker.wake_by_ref();
None
},
if let Some(abort) = self.send_abort.take() {
let _ = abort.send(());
}
self.recv_output.borrow_mut().try_recv().ok().flatten()
}
/// Determine if there's any more work to do on this task
pub fn is_finished(&self) -> bool {
let g = self.0.borrow();
g.result.is_some() || g.work.is_none()
}
pub fn is_finished(&self) -> bool { *self.ready.borrow() }
/// "finish" the freestanding task, and return the future instead
pub async fn join(self) -> T {
let work = {
let mut g = self.0.borrow_mut();
if let Some(val) = g.result.take() {
return *val.downcast().expect("Mistmatch between type of future and handle");
}
g.waker.wake_by_ref();
g.work.take().expect("Attempted to join task that was already aborted")
};
*work.await.downcast().expect("Mismatch between type of future and handle")
pub async fn join(self) -> Result<T, JoinError> {
self.recv_output.into_inner().await.map_err(|Canceled| JoinError)
}
}
/// Split a future into an object that can be polled and one that returns
/// information on its progress and its result. The first one can be passed to
/// an executor or localset, the second can be used to manage it
pub fn to_task<F: Future<Output: 'static> + 'static>(f: F) -> (Pollable, Handle<F::Output>) {
let dyn_future = Box::pin(async { Box::new(f.await) as Box<dyn Any> });
let state = Rc::new(RefCell::new(State {
result: None,
work: Some(dyn_future),
waker: Waker::noop().clone(),
}));
(Pollable(state.clone()), Handle(state, PhantomData))
pub fn to_task<'a, F: Future<Output: 'a> + 'a>(
f: F,
) -> (impl Future<Output = ()> + 'a, Handle<F::Output>) {
let (send_abort, mut on_abort) = oneshot::channel();
let (send_output, on_output) = oneshot::channel();
let ready = Rc::new(RefCell::new(false));
let ready2 = ready.clone();
let fut = async move {
let mut fut = pin!(f.fuse());
let output = futures::select_biased! {
res = on_abort => match res {
Ok(()) => return,
Err(_) => fut.await,
},
output = fut => output,
};
ready2.replace(true);
let _: Result<_, _> = send_output.send(output);
};
(fut, Handle {
ready,
recv_output: RefCell::new(on_output),
send_abort: RefCell::new(Some(send_abort)),
})
}

View File

@@ -1,4 +1,4 @@
use std::cell::RefCell;
use std::cell::{BorrowMutError, RefCell};
use std::marker::PhantomData;
use std::pin::{Pin, pin};
use std::rc::Rc;
@@ -16,56 +16,95 @@ use futures::{
};
use hashbrown::HashMap;
use orchid_api_traits::{Decode, Encode, Request, UnderRoot};
use orchid_async_utils::LocalSet;
use orchid_async_utils::debug::{PanicOnDrop, assert_no_drop};
use orchid_async_utils::{cancel_cleanup, local_set, to_task};
use crate::{clone, finish_or_stash, stash, with_stash};
// TODO: revise error handling; error recovery is never partial, it always
// requires dropping the server, client, and all requests
/// A token indicating that a reply to a request has been sent. Returned from
/// [RepWriter::finish] which is the raw reply channel, or [ReqHandleExt::reply]
/// or [ReqReaderExt::reply] which are type checked
#[must_use = "Receipts indicate that a required action has been performed within a function. \
Most likely this should be returned somewhere."]
pub struct Receipt<'a>(PhantomData<&'a mut ()>);
impl Receipt<'_> {
/// Only call this function from a custom implementation of [RepWriter]
pub fn _new() -> Self { Self(PhantomData) }
pub struct Receipt;
impl Receipt {
/// Only ever call this function from a custom implementation of
/// [RepWriter::finish]
pub fn _new() -> Self { Self }
}
/// Return data while waiting for the response to a request. [Self::future] must
/// be awaited in order to ensure that progress is being made
pub struct ReqWait {
/// Future representing waiting for a request. This must be steadily polled.
pub future: LocalBoxFuture<'static, io::Result<Box<dyn RepReader>>>,
/// Since the [Self::future] must be awaited which exclusively borrows it,
/// this separate handle can be used for cancellation.
pub canceller: Box<dyn CancelNotifier>,
}
/// Write guard to outbound for the purpose of serializing a request. Only one
/// can exist at a time. Dropping this object should panic.
pub trait ReqWriter<'a> {
pub trait ReqWriter {
/// Access to the underlying channel. This may be buffered.
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite>;
/// Finalize the request, release the outbound channel, then queue for the
/// reply on the inbound channel.
fn send(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Box<dyn RepReader<'a> + 'a>>>;
fn send(self: Box<Self>) -> ReqWait;
}
/// Write guard to inbound for the purpose of deserializing a reply. While held,
/// no inbound requests or other replies can be processed.
///
/// Dropping this object should panic even if [RepReader::finish] returns
/// synchronously, because the API isn't cancellation safe in general so it is a
/// programmer error in all cases to drop an object related to it without proper
/// cleanup.
pub trait RepReader<'a> {
/// # Cancellation
///
/// If the request has been cancelled and the server has accepted the
/// cancellation instead of writing a reply (which is never guaranteed), then
/// this object is inert and should be dropped.
///
/// Dropping this object if [Self::reader] returns [Some] should panic even if
/// [RepReader::finish] returns synchronously, because the API isn't
/// cancellation safe in general so it is a programmer error to drop an object
/// related to it without proper cleanup.
pub trait RepReader {
/// Access to the underlying channel. The length of the message is inferred
/// from the number of bytes read so this must not be buffered.
fn reader(&mut self) -> Pin<&mut dyn AsyncRead>;
/// from the number of bytes read so this must not be buffered and a full
/// reply must always be read from it if available
///
/// This returns None if the request has successfully been cancelled, in which
/// case this object can be dropped without calling [Self::finish]
fn reader(&mut self) -> Option<Pin<&mut dyn AsyncRead>>;
/// Finish reading the request
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, ()>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, ()>;
}
/// A handle for cancelling in-flight requests without a reference to
/// the wait future (which would be mutably borrowed by an await at this point)
pub trait CancelNotifier {
/// Upon cancellation the future may resolve to a stub version of [RepReader]
/// with no reader access, but since the cancellation is not synchronized
/// with the server, a full reply may still be received, and if it is, the
/// original reply must still be read from it.
fn cancel(self: Box<Self>) -> LocalBoxFuture<'static, ()>;
}
/// Write guard to outbound for the purpose of serializing a notification.
///
/// Dropping this object should panic for the same reason [RepReader] panics
pub trait MsgWriter<'a> {
pub trait MsgWriter {
/// Access to the underlying channel. This may be buffered.
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite>;
/// Send the notification
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<()>>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<()>>;
}
/// For initiating outbound requests and notifications
pub trait Client {
fn start_request(&self) -> LocalBoxFuture<'_, io::Result<Box<dyn ReqWriter<'_> + '_>>>;
fn start_notif(&self) -> LocalBoxFuture<'_, io::Result<Box<dyn MsgWriter<'_> + '_>>>;
fn start_request(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn ReqWriter>>>;
fn start_notif(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn MsgWriter>>>;
}
impl<T: Client + ?Sized> ClientExt for T {}
@@ -73,62 +112,146 @@ impl<T: Client + ?Sized> ClientExt for T {}
/// notif lifecycle and typing
#[allow(async_fn_in_trait)]
pub trait ClientExt: Client {
#[allow(
clippy::await_holding_refcell_ref,
reason = "Must bypass a future return point by sharing the common path"
)]
async fn request<T: Request + UnderRoot<Root: Encode>>(&self, t: T) -> io::Result<T::Response> {
let mut req = self.start_request().await?;
t.into_root().encode(req.writer().as_mut()).await?;
let mut rep = req.send().await?;
let response = T::Response::decode(rep.reader()).await;
rep.finish().await;
response
let start_req = self.start_request();
// This section must finish if it has started, and the returned writer's `send`
// must be called as well.
let common = Rc::new(RefCell::new(Some(Box::pin(async move {
let mut writer = start_req.await?;
t.into_root().encode(writer.writer().as_mut()).await?;
io::Result::Ok(writer)
}))));
// Initialized in the cancelable section if common returns here. If set, the
// future inside must be finished on stash after the notification is sent
// to ensure that the server acknowledges the cancellation, or to decode the
// result if the cancellation was in fact too late.
let req_wait_rc = Rc::new(RefCell::new(None));
// If both this and common are None, that means the cancelable section is
// already past its last interruptible point, and must be finished on stash
cancel_cleanup(
clone!(req_wait_rc, common; Box::pin(async move {
let req_wait;
{
let mut common_g = common.try_borrow_mut().expect("cancel will drop us before locking");
let common = (common_g.as_mut())
.expect("Only unset by us below or by cancel after dropping us");
// cancel handler may take over here
req_wait = common.await?.send();
common_g.take();
}
let mut rep;
{
let mut req_wait_g = (req_wait_rc.try_borrow_mut())
.expect("We are the first ones to access this");
*req_wait_g = Some(req_wait);
let req_wait = req_wait_g.as_mut().expect("Initialized right above");
// cancel handler may take over here
rep = req_wait.future.as_mut().await?;
req_wait_g.take();
};
// cancel handler will not interrupt if we've gotten this far
let reader = rep.reader().expect("Not been cancelled thus far");
let result = T::Response::decode(reader).await;
rep.finish().await;
result
})),
|fut| {
stash(async move {
// TODO: strategy for IO errors on stash
let req_wait = if common.try_borrow_mut().is_ok_and(|r| r.is_none()) {
// fut was already past common
match req_wait_rc.try_borrow_mut() {
Ok(mut opt) => {
let Some(req_wait) = opt.take() else {
// fut was already reading, finish that read and exit
fut.await.expect("IO error on stash");
return;
};
req_wait
},
Err(BorrowMutError { .. }) => {
// fut was in waiting, take over and do our own thing
std::mem::drop(fut);
req_wait_rc.take().expect("If it was borrowed then it was still set")
},
}
} else {
// fut was still in common, take over and finish common
std::mem::drop(fut);
let common =
(common.take()).expect("If it was still borrowed in fut, it was not yet unset");
common.await.expect("IO error on stash").send()
};
req_wait.canceller.cancel().await;
let mut rep = req_wait.future.await.expect("IO error on stash");
let Some(reader) = rep.reader() else { return };
T::Response::decode(reader).await.expect("IO error on stash");
rep.finish().await;
})
},
)
.await
}
async fn notify<T: UnderRoot<Root: Encode>>(&self, t: T) -> io::Result<()> {
let mut notif = self.start_notif().await?;
t.into_root().encode(notif.writer().as_mut()).await?;
notif.finish().await?;
Ok(())
async fn notify<T: UnderRoot<Root: Encode> + 'static>(&self, t: T) -> io::Result<()> {
let start_notif = self.start_notif();
finish_or_stash(Box::pin(async {
let mut notif = start_notif.await?;
t.into_root().encode(notif.writer().as_mut()).await?;
notif.finish().await?;
Ok(())
}))
.await
}
}
pub trait ReqReader<'a> {
pub trait ReqReader {
fn reader(&mut self) -> Pin<&mut dyn AsyncRead>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, Box<dyn ReqHandle<'a> + 'a>>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, Box<dyn ReqHandle>>;
}
impl<'a, T: ReqReader<'a> + ?Sized> ReqReaderExt<'a> for T {}
impl<T: ReqReader + ?Sized> ReqReaderExt for T {}
#[allow(async_fn_in_trait)]
pub trait ReqReaderExt<'a>: ReqReader<'a> {
pub trait ReqReaderExt: ReqReader {
async fn read_req<R: Decode>(&mut self) -> io::Result<R> { R::decode(self.reader()).await }
async fn reply<R: Request>(
self: Box<Self>,
req: impl Evidence<R>,
rep: &R::Response,
) -> io::Result<Receipt<'a>> {
rep: R::Response,
) -> io::Result<Receipt> {
self.finish().await.reply(req, rep).await
}
async fn start_reply(self: Box<Self>) -> io::Result<Box<dyn RepWriter<'a> + 'a>> {
async fn start_reply(self: Box<Self>) -> io::Result<Box<dyn RepWriter>> {
self.finish().await.start_reply().await
}
}
pub trait ReqHandle<'a> {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Box<dyn RepWriter<'a> + 'a>>>;
pub trait ReqHandle {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Box<dyn RepWriter>>>;
}
impl<'a, T: ReqHandle<'a> + ?Sized> ReqHandleExt<'a> for T {}
impl<T: ReqHandle + ?Sized> ReqHandleExt for T {}
#[allow(async_fn_in_trait)]
pub trait ReqHandleExt<'a>: ReqHandle<'a> {
pub trait ReqHandleExt: ReqHandle {
async fn reply<Req: Request>(
self: Box<Self>,
_: impl Evidence<Req>,
rep: &Req::Response,
) -> io::Result<Receipt<'a>> {
let mut reply = self.start_reply().await?;
rep.encode(reply.writer()).await?;
reply.finish().await
rep: Req::Response,
) -> io::Result<Receipt> {
let start_reply = self.start_reply();
finish_or_stash(Box::pin(async move {
let mut reply = start_reply.await?;
rep.encode(reply.writer()).await?;
reply.finish().await
}))
.await
}
}
pub trait RepWriter<'a> {
pub trait RepWriter {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Receipt<'a>>>;
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Receipt>>;
}
pub trait MsgReader<'a> {
@@ -166,41 +289,43 @@ type IoLock<T> = Rc<Mutex<Pin<Box<T>>>>;
type IoGuard<T> = Bound<MutexGuard<'static, Pin<Box<T>>>, IoLock<T>>;
/// An incoming request. This holds a lock on the ingress channel.
pub struct IoReqReader<'a> {
prefix: &'a [u8],
pub struct IoReqReader {
prefix: u64,
read: IoGuard<dyn AsyncRead>,
write: &'a Mutex<IoRef<dyn AsyncWrite>>,
o: Rc<Mutex<IoRef<dyn AsyncWrite>>>,
}
impl<'a> ReqReader<'a> for IoReqReader<'a> {
impl ReqReader for IoReqReader {
fn reader(&mut self) -> Pin<&mut dyn AsyncRead> { self.read.as_mut() }
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, Box<dyn ReqHandle<'a> + 'a>> {
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, Box<dyn ReqHandle>> {
Box::pin(async {
Box::new(IoReqHandle { prefix: self.prefix, write: self.write }) as Box<dyn ReqHandle<'a>>
Box::new(IoReqHandle { prefix: self.prefix, write: self.o }) as Box<dyn ReqHandle>
})
}
}
pub struct IoReqHandle<'a> {
prefix: &'a [u8],
write: &'a Mutex<IoRef<dyn AsyncWrite>>,
pub struct IoReqHandle {
prefix: u64,
write: IoLock<dyn AsyncWrite>,
}
impl<'a> ReqHandle<'a> for IoReqHandle<'a> {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Box<dyn RepWriter<'a> + 'a>>> {
impl ReqHandle for IoReqHandle {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Box<dyn RepWriter>>> {
let write = self.write.clone();
Box::pin(async move {
let mut write = self.write.lock().await;
write.as_mut().write_all(self.prefix).await?;
Ok(Box::new(IoRepWriter { write }) as Box<dyn RepWriter<'a>>)
let mut write = Bound::async_new(write, |l| l.lock()).await;
self.prefix.encode(write.as_mut()).await?;
Ok(Box::new(IoRepWriter { write }) as Box<dyn RepWriter>)
})
}
}
pub struct IoRepWriter<'a> {
write: MutexGuard<'a, IoRef<dyn AsyncWrite>>,
pub struct IoRepWriter {
write: IoGuard<dyn AsyncWrite>,
}
impl<'a> RepWriter<'a> for IoRepWriter<'a> {
impl RepWriter for IoRepWriter {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite> { self.write.as_mut() }
fn finish(mut self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Receipt<'a>>> {
fn finish(mut self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Receipt>> {
Box::pin(async move {
self.writer().flush().await?;
Ok(Receipt(PhantomData))
Ok(Receipt)
})
}
}
@@ -214,11 +339,16 @@ impl<'a> MsgReader<'a> for IoMsgReader<'a> {
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, ()> { Box::pin(async {}) }
}
pub enum ReplyRecord {
Cancelled,
Ready(IoGuard<dyn AsyncRead>),
}
#[derive(Debug)]
struct ReplySub {
id: u64,
ack: oneshot::Sender<()>,
cb: oneshot::Sender<IoGuard<dyn AsyncRead>>,
cb: oneshot::Sender<ReplyRecord>,
}
struct IoClient {
@@ -231,37 +361,42 @@ impl IoClient {
let (req, rep) = mpsc::channel(0);
(rep, Self { output, id: Rc::new(RefCell::new(0)), subscribe: Rc::new(req) })
}
async fn lock_out(&self) -> IoGuard<dyn AsyncWrite> {
Bound::async_new(self.output.clone(), async |o| o.lock().await).await
}
}
impl Client for IoClient {
fn start_notif(&self) -> LocalBoxFuture<'_, io::Result<Box<dyn MsgWriter<'_> + '_>>> {
fn start_notif(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn MsgWriter>>> {
let output = self.output.clone();
Box::pin(async {
let drop_g = assert_no_drop("Notif future dropped");
let mut o = self.lock_out().await;
let mut o = Bound::async_new(output, |o| o.lock()).await;
0u64.encode(o.as_mut()).await?;
drop_g.defuse();
Ok(Box::new(IoNotifWriter { o, drop_g: assert_no_drop("Notif writer dropped") })
as Box<dyn MsgWriter>)
})
}
fn start_request(&self) -> LocalBoxFuture<'_, io::Result<Box<dyn ReqWriter<'_> + '_>>> {
Box::pin(async {
let id = {
let mut id_g = self.id.borrow_mut();
*id_g += 1;
*id_g
};
let (cb, reply) = oneshot::channel();
let (ack, got_ack) = oneshot::channel();
let drop_g = assert_no_drop("Request future dropped");
self.subscribe.as_ref().clone().send(ReplySub { id, ack, cb }).await.unwrap();
fn start_request(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn ReqWriter>>> {
let output = self.output.clone();
let id = {
let mut id_g = self.id.borrow_mut();
*id_g += 1;
*id_g
};
let (cb, reply) = oneshot::channel();
let (ack, got_ack) = oneshot::channel();
let mut subscribe = self.subscribe.as_ref().clone();
let start_req_drop_g = assert_no_drop("Request future dropped");
Box::pin(async move {
subscribe.send(ReplySub { id, ack, cb }).await.unwrap();
got_ack.await.unwrap();
let mut w = self.lock_out().await;
id.encode(w.as_mut()).await?;
drop_g.defuse();
let mut xfer_bytes = id.to_be_bytes();
xfer_bytes[0] = 0x00;
let req_prefix = u64::from_be_bytes(xfer_bytes);
let mut w = Bound::async_new(output.clone(), |o| o.lock()).await;
req_prefix.encode(w.as_mut()).await?;
start_req_drop_g.defuse();
Ok(Box::new(IoReqWriter {
id,
output,
reply,
w,
drop_g: assert_no_drop("Request reader dropped without reply"),
@@ -270,34 +405,62 @@ impl Client for IoClient {
}
}
struct IoReqWriter {
reply: oneshot::Receiver<IoGuard<dyn AsyncRead>>,
w: IoGuard<dyn AsyncWrite>,
drop_g: PanicOnDrop,
struct IoReqCanceller {
id: u64,
output: IoLock<dyn AsyncWrite>,
}
impl<'a> ReqWriter<'a> for IoReqWriter {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite> { self.w.as_mut() }
fn send(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Box<dyn RepReader<'a> + 'a>>> {
Box::pin(async {
let Self { reply, mut w, drop_g } = *self;
w.flush().await?;
mem::drop(w);
let i = reply.await.expect("Client dropped before reply received");
drop_g.defuse();
Ok(Box::new(IoRepReader {
i,
drop_g: assert_no_drop("Reply reader dropped without finishing"),
}) as Box<dyn RepReader>)
impl CancelNotifier for IoReqCanceller {
fn cancel(self: Box<Self>) -> LocalBoxFuture<'static, ()> {
let mut xfer_bytes = self.id.to_be_bytes();
xfer_bytes[0] = 0x02;
let cancel_id = u64::from_be_bytes(xfer_bytes);
let cancel_signal_drop_g = assert_no_drop("Cannot cancel the sending of a cancellation");
let o = self.output.clone();
Box::pin(async move {
let mut o = o.lock().await;
let _ = cancel_id.encode(o.as_mut()).await;
cancel_signal_drop_g.defuse();
})
}
}
struct IoRepReader {
i: IoGuard<dyn AsyncRead>,
struct IoReqWriter {
id: u64,
reply: oneshot::Receiver<ReplyRecord>,
output: IoLock<dyn AsyncWrite>,
w: IoGuard<dyn AsyncWrite>,
drop_g: PanicOnDrop,
}
impl<'a> RepReader<'a> for IoRepReader {
fn reader(&mut self) -> Pin<&mut dyn AsyncRead> { self.i.as_mut() }
impl ReqWriter for IoReqWriter {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite> { self.w.as_mut() }
fn send(self: Box<Self>) -> ReqWait {
let Self { id, output, reply, mut w, drop_g } = *self;
let canceller = IoReqCanceller { id, output };
let future = async {
w.flush().await?;
mem::drop(w);
let reply_record = reply.await.expect("Client dropped before reply received");
drop_g.defuse();
Ok(Box::new(IoRepReader {
reply_record,
drop_g: assert_no_drop("Reply reader dropped without finishing"),
}) as Box<dyn RepReader>)
};
ReqWait { future: Box::pin(future), canceller: Box::new(canceller) }
}
}
struct IoRepReader {
reply_record: ReplyRecord,
drop_g: PanicOnDrop,
}
impl RepReader for IoRepReader {
fn reader(&mut self) -> Option<Pin<&mut dyn AsyncRead>> {
match &mut self.reply_record {
ReplyRecord::Cancelled => None,
ReplyRecord::Ready(guard) => Some(guard.as_mut()),
}
}
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, ()> {
Box::pin(async { self.drop_g.defuse() })
}
@@ -308,13 +471,13 @@ struct IoNotifWriter {
o: IoGuard<dyn AsyncWrite>,
drop_g: PanicOnDrop,
}
impl<'a> MsgWriter<'a> for IoNotifWriter {
impl MsgWriter for IoNotifWriter {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite> { self.o.as_mut() }
fn finish(mut self: Box<Self>) -> LocalBoxFuture<'static, io::Result<()>> {
Box::pin(async move {
self.o.flush().await?;
let ret = self.o.flush().await;
self.drop_g.defuse();
Ok(())
ret
})
}
}
@@ -333,10 +496,7 @@ impl CommCtx {
/// Establish bidirectional request-notification communication over a duplex
/// channel. The returned [IoClient] can be used for notifications immediately,
/// but requests can only be received while the future is running. The future
/// will only resolve when [CommCtx::quit] is called. The generic type
/// parameters are associated with the client and serve to ensure with a runtime
/// check that the correct message families are sent in the correct directions
/// across the channel.
/// will only resolve when [CommCtx::exit] is called.
pub fn io_comm(
o: Pin<Box<dyn AsyncWrite>>,
i: Pin<Box<dyn AsyncRead>>,
@@ -356,8 +516,8 @@ pub struct IoCommServer {
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>>,
notif: impl AsyncFn(Box<dyn MsgReader>) -> io::Result<()>,
req: impl AsyncFn(Box<dyn ReqReader>) -> io::Result<Receipt>,
) -> io::Result<()> {
let Self { o, i, onexit, onsub } = self;
enum Event {
@@ -379,7 +539,9 @@ impl IoCommServer {
}
}
});
let (mut add_pending_req, fork_future) = LocalSet::new();
let running_requests = RefCell::new(HashMap::new());
let (mut task_pool, fork_future) = local_set();
let mut fork_stream = pin!(fork_future.into_stream());
let mut pending_replies = HashMap::new();
'body: {
@@ -400,32 +562,73 @@ impl IoCommServer {
// this is detected and logged on client
let _ = ack.send(());
},
// ID 0 is reserved for single-fire notifications
Ok(Event::Input(0, read)) => {
let notif = &notif;
let notif_job =
async move { notif(Box::new(IoMsgReader { _pd: PhantomData, read })).await };
add_pending_req.send(Box::pin(notif_job)).await.unwrap();
},
// MSB == 0 is a request, !id where MSB == 1 is the corresponding response
Ok(Event::Input(id, read)) if (id & (1 << (u64::BITS - 1))) == 0 => {
let (o, req) = (o.clone(), &req);
let req_job = async move {
let mut prefix = Vec::new();
(!id).encode_vec(&mut prefix);
let _ = req(Box::new(IoReqReader { prefix: &pin!(prefix), read, write: &o })).await;
Ok(())
};
add_pending_req.send(Box::pin(req_job)).await.unwrap();
task_pool.spawn(notif(Box::new(IoMsgReader { _pd: PhantomData, read }))).await.unwrap();
},
// non-zero IDs are associated with requests
Ok(Event::Input(id, read)) => {
let cb = pending_replies.remove(&!id).expect("Reply to unrecognized request");
cb.send(read).unwrap_or_else(|_| panic!("Failed to send reply"));
// the MSb decides what kind of message this is
let mut id_bytes = id.to_be_bytes();
let discr = std::mem::replace(&mut id_bytes[0], 0x00);
let id = u64::from_be_bytes(id_bytes);
match discr {
// request
0x00 => {
let (o, req, reqs) = (o.clone(), &req, &running_requests);
task_pool
.spawn(async move {
id_bytes[0] = 0x01;
let prefix = u64::from_be_bytes(id_bytes);
let reader = Box::new(IoReqReader { prefix, read, o });
let (fut, handle) = to_task(async { req(reader).await.map(|Receipt| ()) });
reqs.borrow_mut().insert(id, handle);
with_stash(fut).await;
// during this await the read guard is released and thus we may receive a
// cancel notification from below
Ok(())
})
.await
.unwrap();
},
// response
0x01 => {
let cb = pending_replies.remove(&id).expect("Reply to unrecognized request");
cb.send(ReplyRecord::Ready(read))
.unwrap_or_else(|_| panic!("Failed to send reply"));
},
// cancellation
0x02 => {
match running_requests.borrow().get(&id) {
Some(handle) => handle.abort(),
// assuming that the client is correct, if there is no record
// then the reply was already sent
None => continue,
};
// if the request starts writing back before our abort arrives, we only
// get this mutex once it's done
let mut write = o.lock().await;
// if the request is still in the store, the write didn't begin
let Some(_) = running_requests.borrow_mut().remove(&id) else { continue };
id_bytes[0] = 0x03;
let cancel_code = u64::from_be_bytes(id_bytes);
cancel_code.encode(write.as_mut()).await?;
},
// stub reply for cancelled request
0x03 => {
let cb = pending_replies.remove(&id).expect("Cancelling unrecognized request");
cb.send(ReplyRecord::Cancelled)
.unwrap_or_else(|_| panic!("Failed to send reply cancellation"))
},
n => panic!("Unrecognized message type code {n}"),
}
},
}
}
Ok(())
}?;
mem::drop(add_pending_req);
mem::drop(task_pool);
while let Some(next) = fork_stream.next().await {
next?
}
@@ -441,13 +644,15 @@ mod test {
use std::cell::RefCell;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt, join};
use futures::{FutureExt, SinkExt, StreamExt, join, select};
use never::Never;
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
use orchid_async_utils::debug::spin_on;
use unsync_pipe::pipe;
use crate::comm::{ClientExt, MsgReaderExt, ReqReaderExt, io_comm};
use crate::with_stash;
#[derive(Clone, Debug, PartialEq, Coding, Hierarchy)]
#[extendable]
@@ -455,7 +660,7 @@ mod test {
#[test]
fn notification() {
spin_on(async {
spin_on(false, async {
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (received, mut on_receive) = mpsc::channel(2);
@@ -494,7 +699,7 @@ mod test {
#[test]
fn request() {
spin_on(async {
spin_on(false, async {
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (_, srv_ctx, srv) = io_comm(Box::pin(in2), Box::pin(out2));
@@ -506,7 +711,7 @@ mod test {
async |_| panic!("No notifs expected"),
async |mut req| {
let val = req.read_req::<DummyRequest>().await?;
req.reply(&val, &(val.0 + 1)).await
req.reply(&val, val.0 + 1).await
},
)
.await
@@ -533,7 +738,7 @@ mod test {
#[test]
fn exit() {
spin_on(async {
spin_on(false, async {
let (input1, output1) = pipe(1024);
let (input2, output2) = pipe(1024);
let (reply_client, reply_context, reply_server) =
@@ -553,7 +758,7 @@ mod test {
},
async |mut hand| {
let req = hand.read_req::<DummyRequest>().await?;
hand.reply(&req, &(req.0 + 1)).await
hand.reply(&req, req.0 + 1).await
},
)
.await
@@ -579,4 +784,49 @@ mod test {
)
});
}
#[test]
fn timely_cancel() {
spin_on(false, async {
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (wait_in, mut wait_out) = mpsc::channel(0);
let (_, srv_ctx, srv) = io_comm(Box::pin(in2), Box::pin(out2));
let (client, client_ctx, client_srv) = io_comm(Box::pin(in1), Box::pin(out1));
join!(
async {
srv
.listen(
async |_| panic!("No notifs expected"),
async |mut req| {
let _ = req.read_req::<DummyRequest>().await?;
wait_in.clone().send(()).await.unwrap();
// TODO: verify cancellation
futures::future::pending::<Never>().await;
unreachable!("request should be cancelled before resume is triggered")
},
)
.await
.unwrap()
},
async {
client_srv
.listen(
async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"),
)
.await
.unwrap()
},
with_stash(async {
select! {
_ = client.request(DummyRequest(5)).fuse() => panic!("This one should not run"),
rep = wait_out.next() => rep.expect("something?"),
};
srv_ctx.exit().await.unwrap();
client_ctx.exit().await.unwrap();
})
);
})
}
}

View File

@@ -142,7 +142,7 @@ impl OrcErrv {
/// If there is exactly one error, return it. Mostly used for simplified
/// printing
#[must_use]
pub fn one(&self) -> Option<&OrcErr> { (self.0.len() == 1).then(|| &self.0[9]) }
pub fn one(&self) -> Option<&OrcErr> { self.0.iter().exactly_one().ok() }
/// Iterate over all positions of all errors
pub fn pos_iter(&self) -> impl Iterator<Item = ErrPos> + '_ {
self.0.iter().flat_map(|e| e.positions.iter().cloned())

View File

@@ -113,7 +113,6 @@ pub struct Comment {
pub sr: SrcRange,
}
impl Comment {
// XXX: which of these four are actually used?
pub async fn from_api(c: &api::Comment, src: Sym) -> Self {
Self { text: es(c.text).await, sr: SrcRange::new(c.range.clone(), &src) }
}
@@ -123,9 +122,6 @@ impl Comment {
_ => None,
}
}
pub fn to_tk<R: ExprRepr, X: ExtraTok>(&self) -> TokTree<R, X> {
TokTree { tok: Token::Comment(self.text.clone()), sr: self.sr.clone() }
}
pub fn to_api(&self) -> api::Comment {
api::Comment { range: self.sr.range(), text: self.text.to_api() }
}

View File

@@ -1,44 +1,188 @@
//! A pattern for running async code from sync destructors and other
//! unfortunately sync callbacks
//! unfortunately sync callbacks, and for ensuring that these futures finish in
//! a timely fashion
//!
//! We create a task_local vecdeque which is moved into a thread_local whenever
//! the task is being polled. A call to [stash] pushes the future onto this
//! deque. Before [with_stash] returns, it pops everything from the deque
//! individually and awaits each of them, pushing any additionally stashed
//! futures onto the back of the same deque.
//! deque. Before [with_stash] returns, it awaits everything stashed up to that
//! point or inside the stashed futures.
use std::cell::RefCell;
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
use task_local::task_local;
use futures::StreamExt;
use futures::future::LocalBoxFuture;
use futures::stream::FuturesUnordered;
use orchid_async_utils::cancel_cleanup;
#[derive(Default)]
struct StashedFutures {
queue: RefCell<VecDeque<Pin<Box<dyn Future<Output = ()>>>>>,
}
task_local! {
static STASHED_FUTURES: StashedFutures;
thread_local! {
/// # Invariant
///
/// Any function that changes the value of this thread_local must restore it before returning
static CURRENT_STASH: RefCell<Option<Vec<LocalBoxFuture<'static, ()>>>> = RefCell::default();
}
/// Complete the argument future, and any futures spawned from it via [stash].
/// This is useful mostly to guarantee that messaging destructors have run.
pub async fn with_stash<F: Future>(fut: F) -> F::Output {
STASHED_FUTURES
.scope(StashedFutures::default(), async {
let val = fut.await;
while let Some(fut) = STASHED_FUTURES.with(|sf| sf.queue.borrow_mut().pop_front()) {
fut.await;
}
val
})
.await
///
/// # Cancellation
///
/// To ensure that stashed futures run, the returned future re-stashes them a
/// layer above when dropped. Therefore cancelling `with_stash` is only safe
/// within an enclosing `with_stash` outside of a panic.
pub fn with_stash<F: Future>(fut: F) -> impl Future<Output = F::Output> {
WithStash { stash: FuturesUnordered::new(), state: WithStashState::Main(fut) }
}
/// Schedule a future to be run before the next [with_stash] guard ends. This is
/// most useful for sending messages from destructors.
///
/// # Panics
///
/// If no enclosing stash is found, this function panics, unless we are already
/// panicking. The assumption is that a panic is a vis-major where proper
/// cleanup is secondary to avoiding an abort.
pub fn stash<F: Future<Output = ()> + 'static>(fut: F) {
(STASHED_FUTURES.try_with(|sf| sf.queue.borrow_mut().push_back(Box::pin(fut))))
.expect("No stash! Timely completion cannot be guaranteed")
CURRENT_STASH.with(|stash| {
let mut g = stash.borrow_mut();
let Some(stash) = g.as_mut() else {
if !std::thread::panicking() {
panic!("No stash! Timely completion cannot be guaranteed");
}
return;
};
stash.push(Box::pin(fut))
})
}
pub fn finish_or_stash<F: Future + Unpin + 'static>(
fut: F,
) -> impl Future<Output = F::Output> + Unpin + 'static {
cancel_cleanup(fut, |fut| {
stash(async {
fut.await;
})
})
}
enum WithStashState<F: Future> {
Main(F),
Stash {
/// Optional to simplify state management, but only ever null on a very
/// short stretch
output: Option<F::Output>,
},
}
struct WithStash<F: Future> {
stash: FuturesUnordered<LocalBoxFuture<'static, ()>>,
state: WithStashState<F>,
}
impl<F: Future> Future for WithStash<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// SAFETY: the only non-Unpin item is Main#main, and it's pinned right back
let Self { state, stash } = unsafe { Pin::get_unchecked_mut(self) };
if let WithStashState::Main(main) = state {
// SAFETY: this comes from the pin we break on the line above
let main = unsafe { Pin::new_unchecked(main) };
let prev = CURRENT_STASH.with_borrow_mut(|key| key.replace(Vec::new()));
let poll = main.poll(cx);
let stash_init = CURRENT_STASH
.with_borrow_mut(|key| std::mem::replace(key, prev))
.expect("We put a Some() in here and CURRENT_STASH demands restoration");
stash.extend(stash_init);
if let Poll::Ready(o) = poll {
// skip this branch from this point onwards
*state = WithStashState::Stash { output: Some(o) };
}
}
match state {
WithStashState::Main(_) | WithStashState::Stash { output: None, .. } => Poll::Pending,
WithStashState::Stash { output: output @ Some(_) } => loop {
// if the queue has new elements, poll_next_unpin has to be called in the next
// loop to ensure that wake-ups are triggered for them too, and if
// poll_next_unpin is called, the queue may get yet more elements synchronously,
// hence the loop
let prev = CURRENT_STASH.with_borrow_mut(|key| key.replace(Vec::new()));
let poll = stash.poll_next_unpin(cx);
let stash_new = CURRENT_STASH
.with_borrow_mut(|key| std::mem::replace(key, prev))
.expect("We put a Some() in here and CURRENT_STASH demands restoration");
stash.extend(stash_new);
match poll {
Poll::Ready(None) if stash.is_empty() => {
let output = output.take().expect("Checked in branching");
break Poll::Ready(output);
},
Poll::Pending => {
break Poll::Pending;
},
Poll::Ready(_) => continue,
}
},
}
}
}
impl<F: Future> Drop for WithStash<F> {
fn drop(&mut self) {
if std::thread::panicking() {
eprintln!("Panicking through with_stash may silently drop stashed cleanup work")
}
for future in std::mem::take(&mut self.stash) {
stash(future);
}
}
}
#[cfg(test)]
mod test {
use futures::SinkExt;
use futures::channel::mpsc;
use futures::future::join;
use orchid_async_utils::debug::spin_on;
use super::*;
#[test]
fn run_stashed_future() {
let (mut send, recv) = mpsc::channel(0);
spin_on(
false,
join(
with_stash(async {
let mut send1 = send.clone();
stash(async move {
send1.send(1).await.unwrap();
});
let mut send1 = send.clone();
stash(async move {
let mut send2 = send1.clone();
stash(async move {
send2.send(2).await.unwrap();
});
send1.send(3).await.unwrap();
stash(async move {
send1.send(4).await.unwrap();
})
});
let mut send1 = send.clone();
stash(async move {
send1.send(5).await.unwrap();
});
send.send(6).await.unwrap();
}),
async {
let mut results = recv.take(6).collect::<Vec<_>>().await;
results.sort();
assert_eq!(
&results,
&[1, 2, 3, 4, 5, 6],
"all variations completed in unspecified order"
);
},
),
);
}
}

View File

@@ -332,5 +332,12 @@ pub async fn ttv_fmt<'a: 'b, 'b>(
FmtUnit::sequence("", " ", "", true, join_all(ttv.into_iter().map(|t| t.print(c))).await)
}
pub struct FmtTTV<'a, H: ExprRepr, X: ExtraTok>(pub &'a [TokTree<H, X>]);
impl<'b, H: ExprRepr, X: ExtraTok> Format for FmtTTV<'b, H, X> {
async fn print<'a>(&'a self, c: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
ttv_fmt(self.0, c).await
}
}
/// Indent a string by two spaces
pub fn indent(s: &str) -> String { s.replace("\n", "\n ") }

View File

@@ -9,6 +9,7 @@ edition = "2024"
async-event = "0.2.1"
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
chrono = "0.4.44"
derive_destructure = "1.0.0"
dyn-clone = "1.0.20"
futures = { version = "0.3.31", default-features = false, features = [

View File

@@ -1,5 +1,4 @@
use std::any::{Any, TypeId, type_name};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{self, Debug};
use std::future::Future;
@@ -19,7 +18,6 @@ use orchid_base::{
FmtCtx, FmtUnit, Format, IStr, OrcErrv, Pos, Receipt, ReqHandle, ReqReader, ReqReaderExt, Sym,
fmt, is, mk_errv, mk_errv_floating, take_first,
};
use task_local::task_local;
use trait_set::trait_set;
use crate::gen_expr::GExpr;
@@ -99,11 +97,11 @@ impl ForeignAtom {
/// Call an IPC method. If the type does not support the given method type,
/// this function returns [None]
pub async fn call<R: Request + UnderRoot<Root: AtomMethod>>(&self, r: R) -> Option<R::Response> {
let rep = (request(api::Fwd(
self.atom.clone(),
Sym::parse(<R as UnderRoot>::Root::NAME).await.unwrap().tok().to_api(),
enc_vec(&r.into_root()),
)))
let rep = (request(api::Fwd {
target: self.atom.clone(),
method: Sym::parse(<R as UnderRoot>::Root::NAME).await.unwrap().tok().to_api(),
body: enc_vec(&r.into_root()),
}))
.await?;
Some(R::Response::decode_slice(&mut &rep[..]))
}
@@ -111,26 +109,22 @@ impl ForeignAtom {
pub fn downcast<A: Atomic>(self) -> Result<TAtom<A>, NotTypAtom> {
let mut data = &self.atom.data.0[..];
let value = AtomTypeId::decode_slice(&mut data);
if cfg!(debug_assertions) {
let cted = dyn_cted();
let own_inst = cted.inst();
let owner_id = self.atom.owner;
let typ = type_name::<A>();
let owner = if sys_id() == owner_id {
own_inst.card()
} else {
(cted.deps().find(|s| s.id() == self.atom.owner))
.ok_or_else(|| NotTypAtom { expr: self.clone().ex(), pos: self.pos(), typ })?
.get_card()
};
let Some(ops) = owner.ops_by_atid(value) else {
panic!("{value:?} does not refer to an atom in {owner_id:?} when downcasting {typ}");
};
if ops.tid() != TypeId::of::<A>() {
panic!(
"{value:?} of {owner_id:?} refers to a type other than {typ}. System version mismatch?"
)
}
let cted = dyn_cted();
let own_inst = cted.inst();
let owner_id = self.atom.owner;
let typ = type_name::<A>();
let owner = if sys_id() == owner_id {
own_inst.card()
} else {
(cted.deps().find(|s| s.id() == self.atom.owner))
.ok_or_else(|| NotTypAtom { expr: self.clone().ex(), pos: self.pos(), typ })?
.get_card()
};
let Some(ops) = owner.ops_by_atid(value) else {
panic!("{value:?} does not refer to an atom in {owner_id:?} when downcasting {typ}");
};
if ops.tid() != TypeId::of::<A>() {
return Err(NotTypAtom { pos: self.pos.clone(), expr: self.ex(), typ });
}
let value = A::Data::decode_slice(&mut data);
Ok(TAtom { value, untyped: self })
@@ -187,67 +181,23 @@ pub trait AtomMethod: Coding + InHierarchy {
const NAME: &str;
}
task_local! {
pub(crate) static ATOM_WITHOUT_HANDLE_FINAL_IMPL: Rc<RefCell<Option<Box<dyn Any>>>>;
}
/// A handler for an [AtomMethod] on an [Atomic]. The [AtomMethod] must also be
/// registered in [Atomic::reg_methods]
pub trait Supports<M: AtomMethod>: Atomic {
fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: M,
) -> impl Future<Output = io::Result<Receipt<'a>>>;
fn handle_final<'a>(
self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: M,
) -> impl Future<Output = io::Result<Receipt<'a>>> {
async move {
let rcpt = self.handle(hand, req).await;
let _ = ATOM_WITHOUT_HANDLE_FINAL_IMPL.try_with(|cell| cell.replace(Some(Box::new(self))));
rcpt
}
}
// TODO: default-implement the above somehow while calling OwnedAtom::free if
// necessary
fn handle(&self, hand: Box<dyn ReqHandle>, req: M) -> impl Future<Output = io::Result<Receipt>>;
}
trait HandleAtomMethod<A> {
fn handle<'a, 'b: 'a>(
&'a self,
atom: &'a A,
reader: Box<dyn ReqReader<'b> + 'a>,
) -> LocalBoxFuture<'a, ()>;
fn handle_final<'a, 'b: 'a>(
&'a self,
atom: A,
reader: Box<dyn ReqReader<'b> + 'a>,
) -> LocalBoxFuture<'a, ()>;
fn handle<'a>(&'a self, atom: &'a A, reader: Box<dyn ReqReader>) -> LocalBoxFuture<'a, ()>;
}
struct AtomMethodHandler<M, A>(PhantomData<M>, PhantomData<A>);
impl<M: AtomMethod, A: Supports<M>> HandleAtomMethod<A> for AtomMethodHandler<M, A> {
fn handle<'a, 'b: 'a>(
&'a self,
atom: &'a A,
mut reader: Box<dyn ReqReader<'b> + 'a>,
) -> LocalBoxFuture<'a, ()> {
fn handle<'a>(&'a self, atom: &'a A, mut reader: Box<dyn ReqReader>) -> LocalBoxFuture<'a, ()> {
Box::pin(async {
let req = reader.read_req::<M>().await.unwrap();
let _ = Supports::<M>::handle(atom, reader.finish().await, req).await.unwrap();
})
}
fn handle_final<'a, 'b: 'a>(
&'a self,
atom: A,
mut reader: Box<dyn ReqReader<'b> + 'a>,
) -> LocalBoxFuture<'a, ()> {
Box::pin(async {
let req = reader.read_req::<M>().await.unwrap();
let _ = Supports::<M>::handle_final(atom, reader.finish().await, req).await.unwrap();
})
}
}
/// A collection of [Supports] impls for an [Atomic]. If a [Supports]
@@ -282,26 +232,7 @@ pub(crate) struct MethodSet<A: Atomic> {
handlers: HashMap<Sym, Rc<dyn HandleAtomMethod<A>>>,
}
impl<A: Atomic> MethodSet<A> {
pub(crate) async fn final_dispatch<'a>(
&self,
atom: A,
key: Sym,
req: Box<dyn ReqReader<'a> + 'a>,
) -> bool {
match self.handlers.get(&key) {
None => false,
Some(handler) => {
handler.handle_final(atom, req).await;
true
},
}
}
pub(crate) async fn dispatch<'a>(
&self,
atom: &'_ A,
key: Sym,
req: Box<dyn ReqReader<'a> + 'a>,
) -> bool {
pub(crate) async fn dispatch(&self, atom: &A, key: Sym, req: Box<dyn ReqReader>) -> bool {
match self.handlers.get(&key) {
None => false,
Some(handler) => {
@@ -353,11 +284,11 @@ impl<A: Atomic> TAtom<A> {
pub async fn call<R: Request + UnderRoot<Root: AtomMethod>>(&self, req: R) -> R::Response
where A: Supports<<R as UnderRoot>::Root> {
R::Response::decode_slice(
&mut &(request(api::Fwd(
self.untyped.atom.clone(),
Sym::parse(<R as UnderRoot>::Root::NAME).await.unwrap().tok().to_api(),
enc_vec(&req.into_root()),
)))
&mut &(request(api::Fwd {
target: self.untyped.atom.clone(),
method: Sym::parse(<R as UnderRoot>::Root::NAME).await.unwrap().tok().to_api(),
body: enc_vec(&req.into_root()),
}))
.await
.unwrap()[..],
)
@@ -389,17 +320,11 @@ pub trait AtomOps: 'static {
fn call<'a>(&'a self, ctx: AtomCtx<'a>, arg: Expr) -> LocalBoxFuture<'a, GExpr>;
fn call_ref<'a>(&'a self, ctx: AtomCtx<'a>, arg: Expr) -> LocalBoxFuture<'a, GExpr>;
fn print<'a>(&'a self, ctx: AtomCtx<'a>) -> LocalBoxFuture<'a, FmtUnit>;
fn handle_req<'a>(
&'a self,
ctx: AtomCtx<'a>,
key: Sym,
req: Box<dyn ReqReader<'a> + 'a>,
) -> LocalBoxFuture<'a, bool>;
fn handle_req_ref<'a>(
&'a self,
ctx: AtomCtx<'a>,
key: Sym,
req: Box<dyn ReqReader<'a> + 'a>,
req: Box<dyn ReqReader>,
) -> LocalBoxFuture<'a, bool>;
fn serialize<'a, 'b: 'a>(
&'a self,
@@ -417,25 +342,25 @@ pub trait AtomOps: 'static {
trait_set! {
pub trait AtomFactoryFn = FnOnce() -> LocalBoxFuture<'static, api::LocalAtom> + DynClone;
}
pub(crate) struct AtomFactory(Box<dyn AtomFactoryFn>);
pub(crate) struct AtomFactory(Box<dyn AtomFactoryFn>, String);
impl AtomFactory {
pub fn new(f: impl AsyncFnOnce() -> api::LocalAtom + Clone + 'static) -> Self {
Self(Box::new(|| f().boxed_local()))
pub fn new(name: String, f: impl AsyncFnOnce() -> api::LocalAtom + Clone + 'static) -> Self {
Self(Box::new(|| f().boxed_local()), name)
}
pub async fn build(self) -> api::LocalAtom { (self.0)().await }
}
impl Clone for AtomFactory {
fn clone(&self) -> Self { AtomFactory(clone_box(&*self.0)) }
fn clone(&self) -> Self { AtomFactory(clone_box(&*self.0), self.1.clone()) }
}
impl fmt::Debug for AtomFactory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "AtomFactory") }
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "AtomFactory<{}>", self.1) }
}
impl fmt::Display for AtomFactory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "AtomFactory") }
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") }
}
impl Format for AtomFactory {
async fn print<'a>(&'a self, _c: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
"AtomFactory".to_string().into()
self.to_string().into()
}
}

View File

@@ -22,9 +22,8 @@ use task_local::task_local;
use crate::gen_expr::{GExpr, bot};
use crate::{
ATOM_WITHOUT_HANDLE_FINAL_IMPL, AtomCtx, AtomFactory, AtomOps, Atomic, AtomicFeaturesImpl,
AtomicVariant, DynSystemCardExt, Expr, MethodSet, MethodSetBuilder, ToExpr, api, dyn_cted,
err_not_callable,
AtomCtx, AtomFactory, AtomOps, Atomic, AtomicFeaturesImpl, AtomicVariant, DynSystemCardExt, Expr,
MethodSet, MethodSetBuilder, ToExpr, api, dyn_cted, err_not_callable,
};
/// Value of [Atomic::Variant] for a type that implements [OwnedAtom]
@@ -32,7 +31,7 @@ pub struct OwnedVariant;
impl AtomicVariant for OwnedVariant {}
impl<A: OwnedAtom + Atomic<Variant = OwnedVariant>> AtomicFeaturesImpl<OwnedVariant> for A {
fn _factory(self) -> AtomFactory {
AtomFactory::new(async move || {
AtomFactory::new(type_name::<A>().to_string(), async move || {
let obj_store = get_obj_store();
let atom_id = {
let mut id = obj_store.next_id.borrow_mut();
@@ -73,7 +72,10 @@ impl Deref for AtomReadGuard<'_> {
/// Remove an atom from the store
pub(crate) async fn take_atom(id: api::AtomId) -> Box<dyn DynOwnedAtom> {
let mut g = get_obj_store().objects.write().await;
g.remove(&id).unwrap_or_else(|| panic!("Received invalid atom ID: {}", id.0))
g.remove(&id).unwrap_or_else(|| {
let name = dyn_cted().inst().card().name();
panic!("{name} received invalid atom ID: {}", id.0)
})
}
pub(crate) struct OwnedAtomOps<T: OwnedAtom> {
@@ -113,30 +115,11 @@ impl<A: OwnedAtom> AtomOps for OwnedAtomOps<A> {
fn print(&self, AtomCtx(_, id): AtomCtx<'_>) -> LocalBoxFuture<'_, FmtUnit> {
Box::pin(async move { AtomReadGuard::new(id.unwrap()).await.dyn_print().await })
}
fn handle_req<'a>(
&'a self,
AtomCtx(_, id): AtomCtx<'a>,
key: Sym,
req: Box<dyn orchid_base::ReqReader<'a> + 'a>,
) -> LocalBoxFuture<'a, bool> {
Box::pin(async move {
let a = take_atom(id.unwrap()).await;
let ms = self.ms.get_or_init(self.msbuild.pack()).await;
let cell = Rc::new(RefCell::new(None));
let matched = ATOM_WITHOUT_HANDLE_FINAL_IMPL
.scope(cell.clone(), ms.final_dispatch(*a.as_any().downcast().unwrap(), key, req))
.await;
if let Some(val) = cell.take() {
val.downcast::<A>().unwrap().free().await
}
matched
})
}
fn handle_req_ref<'a>(
&'a self,
AtomCtx(_, id): AtomCtx<'a>,
key: Sym,
req: Box<dyn orchid_base::ReqReader<'a> + 'a>,
req: Box<dyn orchid_base::ReqReader>,
) -> LocalBoxFuture<'a, bool> {
Box::pin(async move {
let a = AtomReadGuard::new(id.unwrap()).await;
@@ -303,7 +286,6 @@ fn assert_serializable<T: OwnedAtom>() {
pub(crate) trait DynOwnedAtom: DynClone + 'static {
fn as_any_ref(&self) -> &dyn Any;
fn as_any(self: Box<Self>) -> Box<dyn Any>;
fn encode<'a>(&'a self, buffer: Pin<&'a mut dyn AsyncWrite>) -> LocalBoxFuture<'a, ()>;
fn dyn_call_ref(&self, arg: Expr) -> LocalBoxFuture<'_, GExpr>;
fn dyn_call(self: Box<Self>, arg: Expr) -> LocalBoxFuture<'static, GExpr>;
@@ -316,7 +298,6 @@ pub(crate) trait DynOwnedAtom: DynClone + 'static {
}
impl<T: OwnedAtom> DynOwnedAtom for T {
fn as_any_ref(&self) -> &dyn Any { self }
fn as_any(self: Box<Self>) -> Box<dyn Any> { self }
fn encode<'a>(&'a self, buffer: Pin<&'a mut dyn AsyncWrite>) -> LocalBoxFuture<'a, ()> {
async { self.val().await.as_ref().encode(buffer).await.unwrap() }.boxed_local()
}

View File

@@ -19,7 +19,7 @@ pub struct ThinVariant;
impl AtomicVariant for ThinVariant {}
impl<A: ThinAtom + Atomic<Variant = ThinVariant>> AtomicFeaturesImpl<ThinVariant> for A {
fn _factory(self) -> AtomFactory {
AtomFactory::new(async move || {
AtomFactory::new(type_name::<A>().to_string(), async move || {
let (id, _) = dyn_cted().inst().card().ops::<A>();
let mut buf = enc_vec(&id);
self.encode_vec(&mut buf);
@@ -49,25 +49,17 @@ impl<T: ThinAtom> AtomOps for ThinAtomOps<T> {
fn call_ref<'a>(&'a self, AtomCtx(buf, ..): AtomCtx<'a>, arg: Expr) -> LocalBoxFuture<'a, GExpr> {
Box::pin(async move { T::decode_slice(&mut &buf[..]).call(arg).await })
}
fn handle_req<'a>(
fn handle_req_ref<'a>(
&'a self,
AtomCtx(buf, ..): AtomCtx<'a>,
key: Sym,
req: Box<dyn orchid_base::ReqReader<'a> + 'a>,
req: Box<dyn orchid_base::ReqReader>,
) -> LocalBoxFuture<'a, bool> {
Box::pin(async move {
let ms = self.ms.get_or_init(self.msbuild.pack()).await;
ms.dispatch(&T::decode_slice(&mut &buf[..]), key, req).await
})
}
fn handle_req_ref<'a>(
&'a self,
ctx: AtomCtx<'a>,
key: Sym,
req: Box<dyn orchid_base::ReqReader<'a> + 'a>,
) -> LocalBoxFuture<'a, bool> {
self.handle_req(ctx, key, req)
}
fn serialize<'a, 'b: 'a>(
&'a self,
ctx: AtomCtx<'a>,

View File

@@ -1,45 +1,34 @@
use std::borrow::Cow;
use std::rc::Rc;
use dyn_clone::DynClone;
use futures::future::LocalBoxFuture;
use never::Never;
use orchid_base::{Receipt, ReqHandle, ReqHandleExt};
use trait_set::trait_set;
use crate::gen_expr::{GExpr, new_atom};
use crate::std_reqs::RunCommand;
use crate::gen_expr::{GExpr, new_atom, serialize};
use crate::std_reqs::StartCommand;
use crate::{Atomic, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, ToExpr};
trait_set! {
pub trait ClonableAsyncFnOnceDyn = FnOnce() -> LocalBoxFuture<'static, Option<GExpr>> + DynClone;
pub trait AsyncFnDyn {
fn call<'a>(&'a self) -> LocalBoxFuture<'a, Option<GExpr>>;
}
impl<T: AsyncFn() -> Option<GExpr>> AsyncFnDyn for T {
fn call<'a>(&'a self) -> LocalBoxFuture<'a, Option<GExpr>> { Box::pin(async { (self)().await }) }
}
pub struct CmdAtom(Box<dyn ClonableAsyncFnOnceDyn>);
impl Clone for CmdAtom {
fn clone(&self) -> Self { Self(dyn_clone::clone_box(&*self.0)) }
}
#[derive(Clone)]
pub struct CmdAtom(Rc<dyn AsyncFnDyn>);
impl Atomic for CmdAtom {
type Data = ();
type Variant = OwnedVariant;
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<RunCommand>() }
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<StartCommand>() }
}
impl Supports<RunCommand> for CmdAtom {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: RunCommand,
) -> std::io::Result<Receipt<'a>> {
Self(dyn_clone::clone_box(&*self.0)).handle_final(hand, req).await
}
async fn handle_final<'a>(
self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: RunCommand,
) -> std::io::Result<Receipt<'a>> {
let reply = (self.0)().await;
impl Supports<StartCommand> for CmdAtom {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: StartCommand) -> std::io::Result<Receipt> {
let reply = self.0.call().await;
match reply {
None => hand.reply(&req, &None).await,
Some(next) => hand.reply(&req, &Some(next.serialize().await)).await,
None => hand.reply(&req, None).await,
Some(next) => hand.reply(&req, Some(serialize(next).await)).await,
}
}
}
@@ -48,13 +37,9 @@ impl OwnedAtom for CmdAtom {
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
pub fn cmd<R: ToExpr>(f: impl AsyncFnOnce() -> Option<R> + Clone + 'static) -> GExpr {
new_atom(CmdAtom(Box::new(|| {
Box::pin(async {
match f().await {
None => None,
Some(r) => Some(r.to_gen().await),
}
})
pub fn cmd<R: ToExpr>(f: impl AsyncFn() -> Option<R> + Clone + 'static) -> GExpr {
new_atom(CmdAtom(Rc::new(async move || match f().await {
None => None,
Some(r) => Some(r.to_gen().await),
})))
}

View File

@@ -71,6 +71,14 @@ pub trait ToExpr {
where Self: Sized {
async { self.to_gen().await.create().await }
}
fn boxed<'a>(self) -> Box<dyn ToExprDyn + 'a>
where Self: Sized + 'a {
Box::new(self)
}
fn clonable_boxed<'a>(self) -> Box<dyn ClonableToExprDyn + 'a>
where Self: Clone + Sized + 'a {
Box::new(self)
}
}
/// A wrapper for a future that implements [ToExpr]

View File

@@ -1,3 +1,4 @@
use std::any::type_name;
use std::borrow::Cow;
use std::marker::PhantomData;
use std::rc::Rc;
@@ -7,9 +8,9 @@ use futures::lock::Mutex;
use futures::stream::{self, LocalBoxStream};
use futures::{FutureExt, SinkExt, StreamExt};
use never::Never;
use orchid_base::OrcRes;
use orchid_base::{FmtCtx, FmtUnit, OrcRes};
use crate::gen_expr::{GExpr, arg, call, lam, new_atom, seq};
use crate::gen_expr::{GExpr, call, lam, new_atom, seq};
use crate::{Atomic, Expr, OwnedAtom, OwnedVariant, ToExpr, TryFromExpr};
enum Command {
@@ -18,6 +19,7 @@ enum Command {
}
struct BuilderCoroutineData {
name: &'static str,
receiver: Mutex<LocalBoxStream<'static, Command>>,
}
@@ -30,7 +32,7 @@ impl BuilderCoroutine {
None => panic!("Exec handle dropped and coroutine blocked instead of returning"),
Some(Command::Halt(expr)) => expr,
Some(Command::Execute(expr, reply)) =>
call(lam::<0>(seq(arg(0), call(new_atom(Replier { reply, builder: self }), arg(0)))), expr)
call(lam(async |x| seq(x, call(new_atom(Replier { reply, builder: self }), x)).await), expr)
.await,
}
}
@@ -53,6 +55,9 @@ impl OwnedAtom for Replier {
std::mem::drop(self.reply);
self.builder.run().await
}
async fn print_atom<'a>(&'a self, _c: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
format!("Replier<{}>", self.builder.0.name).into()
}
}
/// A long-lived async context that can yield to the executor. The expression
@@ -62,6 +67,7 @@ pub async fn exec<R: ToExpr>(f: impl for<'a> AsyncFnOnce(ExecHandle<'a>) -> R +
let halt =
async { Command::Halt(f(ExecHandle(cmd_snd, PhantomData)).await.to_gen().await) }.into_stream();
let coro = BuilderCoroutine(Rc::new(BuilderCoroutineData {
name: type_name::<R>(),
receiver: Mutex::new(stream::select(halt, cmd_recv).boxed_local()),
}));
coro.run().await

View File

@@ -8,12 +8,12 @@ use std::pin::Pin;
use std::rc::Rc;
use std::time::Duration;
use futures::future::{LocalBoxFuture, join_all};
use futures::{AsyncWriteExt, StreamExt, stream};
use futures::future::{LocalBoxFuture, join_all, join3};
use futures::{AsyncReadExt, AsyncWriteExt, StreamExt, stream};
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request, UnderRoot, enc_vec};
use orchid_async_utils::{Handle, to_task};
use orchid_async_utils::{Handle, JoinError, to_task};
use orchid_base::{
Client, ClientExt, CommCtx, Comment, MsgReader, MsgReaderExt, ReqHandleExt, ReqReaderExt,
Snippet, Sym, TokenVariant, Witness, char_filter_match, char_filter_union, es, io_comm, is, log,
@@ -21,7 +21,9 @@ use orchid_base::{
};
use substack::Substack;
use task_local::task_local;
use unsync_pipe::pipe;
use crate::gen_expr::serialize;
use crate::interner::new_interner;
use crate::logger::LoggerImpl;
use crate::tree::{TreeIntoApiCtxImpl, get_lazy, with_lazy_member_store};
@@ -63,15 +65,17 @@ pub async fn mute_reply<F: Future>(f: F) -> F::Output { MUTE_REPLY.scope((), f).
/// Send a request through the global client's [ClientExt::request]
pub async fn request<T: Request + UnderRoot<Root = api::ExtHostReq>>(t: T) -> T::Response {
let req_str = if MUTE_REPLY.try_with(|b| *b).is_err() { format!("{t:?}") } else { String::new() };
let response = get_client().request(t).await.unwrap();
if MUTE_REPLY.try_with(|b| *b).is_err() {
writeln!(log("msg"), "Got response {response:?}").await;
let ext = dyn_cted().inst().card().name();
writeln!(log("msg"), "{ext} {req_str} got response {response:?}").await;
}
response
}
/// Send a notification through the global client's [ClientExt::notify]
pub async fn notify<T: UnderRoot<Root = api::ExtHostNotif>>(t: T) {
pub async fn notify<T: UnderRoot<Root = api::ExtHostNotif> + 'static>(t: T) {
get_client().notify(t).await.unwrap()
}
@@ -103,7 +107,7 @@ impl<F: AsyncFnOnce(LocalBoxFuture<'_, ()>) + 'static> ContextModifier for F {
pub(crate) trait DynTaskHandle: 'static {
fn abort(self: Box<Self>);
fn join(self: Box<Self>) -> LocalBoxFuture<'static, Box<dyn Any>>;
fn join(self: Box<Self>) -> LocalBoxFuture<'static, Result<Box<dyn Any>, JoinError>>;
}
task_local! {
@@ -121,7 +125,7 @@ impl<T: 'static> TaskHandle<T> {
/// Stop working on the task and return the nested future. The distinction
/// between this and waiting until the task is complete without reparenting it
/// is significant for the purpose of [task_local] context
pub async fn join(self) -> T { *self.0.join().await.downcast().unwrap() }
pub async fn join(self) -> Result<T, JoinError> { Ok(*self.0.join().await?.downcast().unwrap()) }
}
/// Spawn a future that is not associated with a pending request or a past
@@ -135,7 +139,9 @@ pub fn spawn<F: Future<Output: 'static> + 'static>(delay: Duration, f: F) -> Tas
impl DynTaskHandle for Handle<Box<dyn Any>> {
fn abort(self: Box<Self>) { Self::abort(&self); }
fn join(self: Box<Self>) -> LocalBoxFuture<'static, Box<dyn Any>> { Box::pin(Self::join(*self)) }
fn join(self: Box<Self>) -> LocalBoxFuture<'static, Result<Box<dyn Any>, JoinError>> {
Box::pin(Self::join(*self))
}
}
/// A new Orchid extension as specified in loaders. An extension is a unit of
@@ -210,15 +216,15 @@ impl ExtensionBuilder {
match req {
api::HostExtReq::SystemDrop(sys_drop) => {
SYSTEM_TABLE.with(|l| l.borrow_mut().remove(&sys_drop.0));
handle.reply(&sys_drop, &()).await
handle.reply(&sys_drop, ()).await
},
api::HostExtReq::AtomDrop(atom_drop @ api::AtomDrop(sys_id, atom)) =>
with_sys_record(sys_id, async {
take_atom(atom).await.dyn_free().await;
handle.reply(&atom_drop, &()).await
handle.reply(&atom_drop, ()).await
})
.await,
api::HostExtReq::Ping(ping @ api::Ping) => handle.reply(&ping, &()).await,
api::HostExtReq::Ping(ping @ api::Ping) => handle.reply(&ping, ()).await,
api::HostExtReq::Sweep(api::Sweep) => todo!(),
api::HostExtReq::SysReq(api::SysReq::NewSystem(new_sys)) => {
let (ctor_idx, _) = (decls.iter().enumerate().find(|(_, s)| s.id == new_sys.system))
@@ -254,7 +260,7 @@ impl ExtensionBuilder {
.await;
let response =
api::NewSystemResponse { lex_filter, const_root, line_types, prelude };
handle.reply(&new_sys, &response).await
handle.reply(&new_sys, response).await
})
.await
},
@@ -263,17 +269,23 @@ impl ExtensionBuilder {
let (path, tree) = get_lazy(tree_id).await;
let mut tia_ctx =
TreeIntoApiCtxImpl { path: Substack::Bottom, basepath: &path[..] };
handle.reply(&get_tree, &tree.into_api(&mut tia_ctx).await).await
handle.reply(&get_tree, tree.into_api(&mut tia_ctx).await).await
})
.await,
api::HostExtReq::SysReq(api::SysReq::SysFwded(fwd)) => {
let fwd_tok = Witness::of(&fwd);
let api::SysFwded(sys_id, payload) = fwd;
with_sys_record(sys_id, async {
let (mut req_in, req) = pipe(1024);
let (rep, mut rep_out) = pipe(1024);
let mut reply = Vec::new();
let req = TrivialReqCycle { req: &payload, rep: &mut reply };
let _ = dyn_cted().inst().dyn_request(Box::new(req)).await;
handle.reply(fwd_tok, &reply).await
let (..) = join3(
async { req_in.write_all(&payload).await.expect("Ingress failed") },
async { rep_out.read_to_end(&mut reply).await.expect("Egress failed") },
dyn_cted().inst().dyn_request(Box::new(TrivialReqCycle { req, rep })),
)
.await;
handle.reply(fwd_tok, reply).await
})
.await
},
@@ -295,7 +307,7 @@ impl ExtensionBuilder {
Err(e) => {
let eopt = e.keep_only(|e| *e != ekey_cascade).map(|e| Err(e.to_api()));
expr_store.dispose().await;
return handle.reply(&lex, &eopt).await;
return handle.reply(&lex, eopt).await;
},
Ok((s, expr)) => {
let expr = join_all(
@@ -305,13 +317,13 @@ impl ExtensionBuilder {
.await;
let pos = (text.len() - s.len()) as u32;
expr_store.dispose().await;
return handle.reply(&lex, &Some(Ok(api::LexedExpr { pos, expr }))).await;
return handle.reply(&lex, Some(Ok(api::LexedExpr { pos, expr }))).await;
},
}
}
writeln!(log("warn"), "Got notified about n/a character '{trigger_char}'").await;
expr_store.dispose().await;
handle.reply(&lex, &None).await
handle.reply(&lex, None).await
})
.await,
api::HostExtReq::ParseLine(pline) => {
@@ -335,14 +347,14 @@ impl ExtensionBuilder {
};
mem::drop(line);
expr_store.dispose().await;
handle.reply(req, &o_line).await
handle.reply(req, o_line).await
})
.await
},
api::HostExtReq::FetchParsedConst(ref fpc @ api::FetchParsedConst(sys, id)) =>
with_sys_record(sys, async {
let cnst = get_const(id).await;
handle.reply(fpc, &cnst.serialize().await).await
handle.reply(fpc, serialize(cnst).await).await
})
.await,
api::HostExtReq::AtomReq(atom_req) => {
@@ -354,50 +366,48 @@ impl ExtensionBuilder {
api::AtomReq::SerializeAtom(ser) => {
let mut buf = enc_vec(&id);
match nfo.serialize(actx, Pin::<&mut Vec<_>>::new(&mut buf)).await {
None => handle.reply(ser, &None).await,
None => handle.reply(ser, None).await,
Some(refs) => {
let refs =
join_all(refs.into_iter().map(async |ex| ex.into_api(&mut ()).await))
.await;
handle.reply(ser, &Some((buf, refs))).await
handle.reply(ser, Some((buf, refs))).await
},
}
},
api::AtomReq::AtomPrint(print @ api::AtomPrint(_)) =>
handle.reply(print, &nfo.print(actx).await.to_api()).await,
api::AtomReq::FinalFwded(fwded) => {
let api::FinalFwded(_, key, payload) = &fwded;
handle.reply(print, nfo.print(actx).await.to_api()).await,
api::AtomReq::Fwded(fwded) => {
let api::Fwded(_, key, payload) = &fwded;
let (mut req_in, req) = pipe(1024);
let (rep, mut rep_out) = pipe(1024);
let mut reply = Vec::new();
let key = Sym::from_api(*key).await;
let req = TrivialReqCycle { req: payload, rep: &mut reply };
let some = nfo.handle_req(actx, key, Box::new(req)).await;
handle.reply(fwded, &some.then_some(reply)).await
},
api::AtomReq::FwdedRef(fwded) => {
let api::FinalFwded(_, key, payload) = &fwded;
let mut reply = Vec::new();
let key = Sym::from_api(*key).await;
let req = TrivialReqCycle { req: payload, rep: &mut reply };
let some = nfo.handle_req_ref(actx, key, Box::new(req)).await;
handle.reply(fwded, &some.then_some(reply)).await
let (.., some) = join3(
async { req_in.write_all(payload).await.expect("Ingress failed") },
async { rep_out.read_to_end(&mut reply).await.expect("Egress failed") },
nfo.handle_req_ref(actx, key, Box::new(TrivialReqCycle { req, rep })),
)
.await;
handle.reply(fwded, some.then_some(reply)).await
},
api::AtomReq::CallRef(call @ api::CallRef(_, arg)) => {
let expr_store = BorrowedExprStore::new();
let expr_handle = ExprHandle::borrowed(*arg, &expr_store);
let ret = nfo.call_ref(actx, Expr::from_handle(expr_handle.clone())).await;
let api_expr = ret.serialize().await;
let api_expr = serialize(ret).await;
mem::drop(expr_handle);
expr_store.dispose().await;
handle.reply(call, &api_expr).await
handle.reply(call, api_expr).await
},
api::AtomReq::FinalCall(call @ api::FinalCall(_, arg)) => {
let expr_store = BorrowedExprStore::new();
let expr_handle = ExprHandle::borrowed(*arg, &expr_store);
let ret = nfo.call(actx, Expr::from_handle(expr_handle.clone())).await;
let api_expr = ret.serialize().await;
let api_expr = serialize(ret).await;
mem::drop(expr_handle);
expr_store.dispose().await;
handle.reply(call, &api_expr).await
handle.reply(call, api_expr).await
},
}
})
@@ -414,7 +424,7 @@ impl ExtensionBuilder {
let id = AtomTypeId::decode_slice(read);
let nfo = (dyn_cted().inst().card().ops_by_atid(id))
.expect("Deserializing atom with invalid ID");
handle.reply(&deser, &nfo.deserialize(read, &refs).await).await
handle.reply(&deser, nfo.deserialize(read, &refs).await).await
})
.await
},

View File

@@ -10,7 +10,7 @@ use futures::future::join_all;
use hashbrown::HashSet;
use orchid_base::{FmtCtx, FmtUnit, Format, OrcErrv, Pos, stash};
use crate::gen_expr::{GExpr, GExprKind};
use crate::gen_expr::{GExpr, slot};
use crate::{ForeignAtom, api, notify, request, sys_id};
/// Handle for a lifetime associated with an [ExprHandle], such as a function
@@ -158,9 +158,7 @@ impl Expr {
pub fn handle(&self) -> Rc<ExprHandle> { self.handle.clone() }
/// Wrap this expression in a [GExpr] synchronously as an escape hatch.
/// Otherwise identical to this type's [crate::ToExpr] impl
pub fn slot(&self) -> GExpr {
GExpr { pos: Pos::SlotTarget, kind: GExprKind::Slot(self.clone()) }
}
pub fn slot(&self) -> GExpr { slot(self.clone()) }
/// Increments the refcount to ensure that the ticket remains valid even if
/// the handle is freed. To avoid a leak, [Expr::deserialize] must eventually
/// be called.
@@ -168,11 +166,7 @@ impl Expr {
}
impl Format for Expr {
async fn print<'a>(&'a self, _c: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
match &self.data().await.kind {
ExprKind::Opaque => "OPAQUE".to_string().into(),
ExprKind::Bottom(b) => format!("Bottom({b})").into(),
ExprKind::Atom(a) => FmtUnit::from_api(&request(api::ExtAtomPrint(a.atom.clone())).await),
}
FmtUnit::from_api(&request(api::ExprPrint { target: self.handle.0 }).await)
}
}
impl Eq for Expr {}

View File

@@ -1,3 +1,5 @@
use std::cell::RefCell;
use std::marker::PhantomData;
use std::mem;
use std::pin::{Pin, pin};
use std::rc::Rc;
@@ -6,22 +8,38 @@ use futures::{FutureExt, Stream, StreamExt, stream};
use orchid_base::{
FmtCtx, FmtUnit, Format, OrcErr, OrcErrv, Pos, Sym, Variants, match_mapping, tl_cache,
};
use substack::Substack;
use task_local::task_local;
use crate::{AtomFactory, AtomicFeatures, Expr, ToExpr, ToExprFuture, api, request, sys_id};
/// Newly generated AST. Values of this type should not typically be constructed
/// manually but through the helpers in this module
#[derive(Clone, Copy, Debug)]
struct ExprSerializeCx<'a> {
closures: Substack<'a, u64>,
lambda_counter: &'a RefCell<u64>,
}
/// Release notifications will not be sent for the slots. Use this with
/// messages that imply ownership transfer
pub async fn serialize(expr: GExpr) -> api::Expression {
let cx = ExprSerializeCx { closures: Substack::Bottom, lambda_counter: &RefCell::new(0) };
expr.serialize(cx).await
}
/// Smart object representing AST not-yet-sent to the interpreter. This type can
/// be cloned and persisted, and it must not have unbound arguments. The helper
/// functions in this module let you build trees of [ToExpr] implementors which
/// represent lambdas and their arguments separately, and then convert them into
/// [GExpr] in one pass.
#[derive(Clone, Debug)]
pub struct GExpr {
/// AST node type
pub kind: GExprKind,
kind: GExprKind,
/// Code location associated with the expression for debugging purposes
pub pos: Pos,
pos: Pos,
}
impl GExpr {
/// Release notifications will not be sent for the slots. Use this with
/// messages that imply ownership transfer
pub async fn serialize(self) -> api::Expression {
async fn serialize(self, cx: ExprSerializeCx<'_>) -> api::Expression {
if let GExprKind::Slot(ex) = self.kind {
let hand = ex.handle();
mem::drop(ex);
@@ -32,8 +50,8 @@ impl GExpr {
}
} else {
api::Expression {
location: api::Location::Inherit,
kind: self.kind.serialize().boxed_local().await,
location: self.pos.to_api(),
kind: self.kind.serialize(cx).boxed_local().await,
}
}
}
@@ -42,7 +60,7 @@ impl GExpr {
/// Send the expression to the interpreter to be compiled and to become
/// shareable across extensions
pub async fn create(self) -> Expr {
Expr::deserialize(request(api::Create(sys_id(), self.serialize().await)).await).await
Expr::deserialize(request(api::Create(sys_id(), serialize(self).await)).await).await
}
}
impl Format for GExpr {
@@ -56,8 +74,8 @@ impl Format for GExpr {
pub enum GExprKind {
/// Function call
Call(Box<GExpr>, Box<GExpr>),
/// Lambda expression. Argument numbers are matched when equal
Lambda(u64, Box<GExpr>),
/// Lambda expression. Argument must be the same for slot
Lambda(Box<GExpr>),
/// Slot for a lambda argument
Arg(u64),
/// The second expression is only valid after the first one had already been
@@ -80,23 +98,40 @@ pub enum GExprKind {
Bottom(OrcErrv),
}
impl GExprKind {
async fn serialize(self) -> api::ExpressionKind {
pub fn at(self, pos: Pos) -> GExpr { GExpr { kind: self, pos } }
async fn serialize(self, cx: ExprSerializeCx<'_>) -> api::ExpressionKind {
match_mapping!(self, Self => api::ExpressionKind {
Call(
f => Box::new(f.serialize().await),
x => Box::new(x.serialize().await)
f => Box::new(f.serialize(cx).await),
x => Box::new(x.serialize(cx).await)
),
Seq(
a => Box::new(a.serialize().await),
b => Box::new(b.serialize().await)
a => Box::new(a.serialize(cx).await),
b => Box::new(b.serialize(cx).await)
),
Lambda(arg, body => Box::new(body.serialize().await)),
Arg(arg),
Const(name.to_api()),
Bottom(err.to_api()),
NewAtom(fac.clone().build().await),
} {
Self::Slot(_) => panic!("processed elsewhere")
Self::Slot(_) => panic!("processed elsewhere"),
Self::Lambda(body) => {
let id: u64;
{
let mut g = cx.lambda_counter.borrow_mut();
id = *g;
*g += 1;
};
let cx = ExprSerializeCx {
lambda_counter: cx.lambda_counter,
closures: cx.closures.push(id)
};
api::ExpressionKind::Lambda(id,
Box::new(body.serialize(cx).await)
)
},
Self::Arg(arg) => {
api::ExpressionKind::Arg(*cx.closures.iter().nth(arg as usize).expect("Unbound arg"))
},
})
}
}
@@ -106,9 +141,9 @@ impl Format for GExprKind {
GExprKind::Call(f, x) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("{0} ({1})")))
.units([f.print(c).await, x.print(c).await]),
GExprKind::Lambda(arg, body) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("\\{0}.{1}")))
.units([arg.to_string().into(), body.print(c).await]),
GExprKind::Lambda(body) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("\\{1}")))
.units([body.print(c).await]),
GExprKind::Arg(arg) => arg.to_string().into(),
GExprKind::Seq(a, b) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("[{0}] {1}")))
@@ -123,7 +158,11 @@ impl Format for GExprKind {
}
}
fn inherit(kind: GExprKind) -> GExpr { GExpr { pos: Pos::Inherit, kind } }
pub fn inherit(kind: GExprKind) -> GExpr { GExpr { pos: Pos::Inherit, kind } }
task_local! {
pub static CLOSURE_DEPTH: u64;
}
impl ToExpr for Sym {
async fn to_expr(self) -> Expr
@@ -135,6 +174,8 @@ impl ToExpr for Sym {
/// Creates an expression from a new atom that we own.
pub fn new_atom<A: AtomicFeatures>(atom: A) -> GExpr { inherit(GExprKind::NewAtom(atom.factory())) }
pub fn slot(expr: Expr) -> GExpr { GExpr { pos: Pos::SlotTarget, kind: GExprKind::Slot(expr) } }
/// An expression which is only valid if a number of dependencies had already
/// been normalized
pub fn seq(
@@ -155,17 +196,49 @@ pub fn seq(
})
}
/// Argument bound by an enclosing [lam] or [dyn_lambda]
pub fn arg(n: u64) -> GExpr { inherit(GExprKind::Arg(n)) }
/// A lambda expression. The difference from [dyn_lambda] is purely aesthetic
pub fn lam<const N: u64>(b: impl ToExpr) -> ToExprFuture<impl Future<Output = GExpr>> {
dyn_lambda(N, b)
#[derive(Debug, Clone, Copy)]
pub enum ArgState {
Building,
Serializing { depth: u64 },
Ready,
}
/// A lambda expression. The difference from [lam] is purely aesthetic
pub fn dyn_lambda(n: u64, b: impl ToExpr) -> ToExprFuture<impl Future<Output = GExpr>> {
ToExprFuture(async move { inherit(GExprKind::Lambda(n, Box::new(b.to_gen().await))) })
/// Argument bound by an enclosing [lam] or [dyn_lambda]
#[derive(Debug, Clone, Copy)]
pub struct GenArg<'a>(*const RefCell<ArgState>, PhantomData<&'a ()>);
impl ToExpr for GenArg<'_> {
async fn to_gen(self) -> GExpr {
// SAFETY: Created from a Rc that lives as long as the lifetime arg, see [lam]
let state = unsafe { self.0.as_ref().unwrap() };
match (*state.borrow(), CLOSURE_DEPTH.try_with(|r| *r)) {
(ArgState::Serializing { .. }, Err(_)) =>
panic!("Lambda should have cleared up argstate alongside CLOSURE_DEPTH"),
(ArgState::Serializing { depth }, Ok(total)) => inherit(GExprKind::Arg(total - depth)),
(ArgState::Building, _) =>
panic!("Argument serialized before lambda. Likely an over-eager ToExpr impl"),
(ArgState::Ready, _) =>
unreachable!("The arg should never be available this long, the GenArg is a convenience"),
}
}
}
/// A lambda expression.
pub fn lam<'a>(
cb: impl for<'b> AsyncFnOnce(GenArg<'b>) -> GExpr + 'a,
) -> ToExprFuture<impl Future<Output = GExpr> + 'a> {
let state = Rc::new(RefCell::new(ArgState::Building));
ToExprFuture(async move {
let rank = CLOSURE_DEPTH.try_with(|r| *r + 1).unwrap_or(0);
match *state.borrow_mut() {
ref mut state @ ArgState::Building => *state = ArgState::Serializing { depth: rank },
ArgState::Serializing { .. } => panic!("Lambda serialized twice, found interrupted"),
ArgState::Ready => panic!("Lambda serialized twice"),
}
let gen_arg = GenArg(Rc::as_ptr(&state), PhantomData);
let ret = CLOSURE_DEPTH.scope(rank, async { cb(gen_arg).await.to_gen().await }).await;
mem::drop(state);
inherit(GExprKind::Lambda(Box::new(ret)))
})
}
/// one or more items that are convertible to expressions. In practice, a

View File

@@ -1,6 +1,8 @@
#![allow(refining_impl_trait, reason = "Has various false-positives around lints")]
use orchid_api as api;
mod streams;
pub use streams::*;
mod atom;
pub use atom::*;
mod cmd_atom;

View File

@@ -16,7 +16,7 @@ use task_local::task_local;
use crate::gen_expr::GExpr;
use crate::tree::{GenTok, GenTokTree};
use crate::{Expr, ToExpr, api, request, sys_id};
use crate::{ExecHandle, Expr, ToExpr, TryFromExpr, api, exec, request, sys_id};
/// [PTokTree] without [orchid_base::Pos] metadata
pub type PTok = Token<Expr, Never>;
@@ -118,7 +118,7 @@ impl<'a> ParsCtx<'a> {
pub fn module(&self) -> Sym { self.module.clone() }
}
type BoxConstCallback = Box<dyn FnOnce(ConstCtx) -> LocalBoxFuture<'static, GExpr>>;
type BoxConstCallback = Box<dyn for<'a> FnOnce(ConstCtx<'a>) -> LocalBoxFuture<'a, GExpr>>;
task_local! {
static CONST_TBL: RefCell<HashMap<NonZero<u64>, BoxConstCallback>>;
@@ -143,14 +143,15 @@ impl ParsedLine {
/// Define a constant. The callback is only called later if the constant is
/// referenced, and it can call [crate::refl] to base its value on the module
/// tree or use its argument for context-specific queries
pub fn cnst<'a, R: ToExpr + 'static, F: AsyncFnOnce(ConstCtx) -> R + 'static>(
pub fn cnst<'a, R: ToExpr + 'static, F: for<'b> AsyncFnOnce(ConstCtx<'b>) -> R + 'static>(
sr: &SrcRange,
comments: impl IntoIterator<Item = &'a Comment>,
exported: bool,
name: IStr,
f: F,
) -> Self {
let cb = Box::new(|ctx| async move { f(ctx).await.to_gen().await }.boxed_local());
let cb: BoxConstCallback =
Box::new(|ctx| async move { f(ctx).await.to_gen().await }.boxed_local());
let kind = ParsedLineKind::Mem(ParsedMem { name, exported, kind: ParsedMemKind::Const(cb) });
let comments = comments.into_iter().cloned().collect();
ParsedLine { comments, sr: sr.clone(), kind }
@@ -241,11 +242,11 @@ pub enum ParsedMemKind {
}
/// Enable a generated constant to query about its environment
#[derive(Clone)]
pub struct ConstCtx {
pub struct ConstCtx<'a> {
constid: api::ParsedConstId,
exec: ExecHandle<'a>,
}
impl ConstCtx {
impl<'a> ConstCtx<'a> {
/// Resolve a set of local names into the full names they would point to if
/// they were globally bound. Errors produced at this stage are soft, as the
/// names may still be found to be locally bound within the expression
@@ -272,10 +273,14 @@ impl ConstCtx {
pub async fn names_n<const N: usize>(&self, names: [&Sym; N]) -> [OrcRes<Sym>; N] {
self.names(names).collect::<Vec<_>>().await.try_into().expect("Lengths must match")
}
pub async fn exec<R: TryFromExpr>(&mut self, val: impl ToExpr) -> OrcRes<R> {
self.exec.exec(val).await
}
pub fn handle(&mut self) -> &mut ExecHandle<'a> { &mut self.exec }
}
pub(crate) async fn get_const(id: api::ParsedConstId) -> GExpr {
let cb = CONST_TBL
.with(|ent| ent.borrow_mut().remove(&id.0).expect("Bad ID or double read of parsed const"));
cb(ConstCtx { constid: id }).await
exec(async move |exec| cb(ConstCtx { constid: id, exec }).await).await
}

View File

@@ -1,6 +1,7 @@
use std::io;
use std::num::NonZero;
use std::time::Duration;
use chrono::{DateTime, Utc};
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
@@ -15,20 +16,20 @@ impl Request for Spawn {
/// Execute the atom as a command.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Coding, Hierarchy)]
pub struct RunCommand;
impl Request for RunCommand {
pub struct StartCommand;
impl Request for StartCommand {
type Response = Option<api::Expression>;
}
impl AtomMethod for RunCommand {
impl AtomMethod for StartCommand {
const NAME: &str = "orchid::cmd::run";
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Coding, Hierarchy)]
pub struct AsDuration;
impl Request for AsDuration {
type Response = Duration;
pub struct AsInstant;
impl Request for AsInstant {
type Response = DateTime<Utc>;
}
impl AtomMethod for AsDuration {
impl AtomMethod for AsInstant {
const NAME: &str = "orchid::time::as_duration";
}
@@ -58,6 +59,20 @@ pub struct IoError {
pub message: String,
pub kind: IoErrorKind,
}
impl From<io::Error> for IoError {
fn from(value: io::Error) -> Self {
Self {
message: value.to_string(),
kind: match value.kind() {
io::ErrorKind::Interrupted
| io::ErrorKind::BrokenPipe
| io::ErrorKind::NetworkDown
| io::ErrorKind::ConnectionReset => IoErrorKind::Interrupted,
_ => IoErrorKind::Other,
},
}
}
}
#[derive(Clone, Debug, Coding)]
pub enum ReadLimit {
@@ -69,7 +84,9 @@ pub enum ReadLimit {
/// Read all available data from a stream. If the returned vector is empty, the
/// stream has reached its end.
#[derive(Clone, Debug, Coding, Hierarchy)]
pub struct ReadReq(pub ReadLimit);
pub struct ReadReq {
pub limit: ReadLimit,
}
impl Request for ReadReq {
type Response = Result<Vec<u8>, IoError>;
}

View File

@@ -0,0 +1,95 @@
use std::borrow::Cow;
use std::io::Result;
use std::pin::Pin;
use std::rc::Rc;
use futures::io::BufReader;
use futures::lock::Mutex;
use futures::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use never::Never;
use orchid_base::{Receipt, ReqHandle, ReqHandleExt};
use crate::gen_expr::{GExpr, new_atom};
use crate::std_reqs::{CloseReq, FlushReq, OutputReq, ReadLimit, ReadReq, WriteReq};
use crate::{Atomic, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, ToExpr};
struct WriterState {
buf: Vec<u8>,
writer: Pin<Box<dyn AsyncWrite>>,
}
pub struct OrcWriter<T: AsyncWrite + 'static>(pub T);
impl<T: AsyncWrite + 'static> ToExpr for OrcWriter<T> {
async fn to_gen(self) -> GExpr {
new_atom(WriterAtom(Rc::new(Mutex::new(WriterState {
buf: Vec::new(),
writer: Box::pin(self.0),
}))))
}
}
#[derive(Clone)]
pub struct WriterAtom(Rc<Mutex<WriterState>>);
impl Atomic for WriterAtom {
type Variant = OwnedVariant;
type Data = ();
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<OutputReq>() }
}
impl OwnedAtom for WriterAtom {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<OutputReq> for WriterAtom {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: OutputReq) -> Result<Receipt> {
match req {
OutputReq::WriteReq(ref wr @ WriteReq { ref data }) => {
self.0.lock().await.buf.extend(data);
hand.reply(wr, Ok(())).await
},
OutputReq::FlushReq(ref fr @ FlushReq) => {
let mut g = self.0.lock().await;
let WriterState { buf, writer } = &mut *g;
hand.reply(fr, writer.write_all(&buf[..]).await.map_err(|e| e.into())).await
},
OutputReq::CloseReq(ref cr @ CloseReq) =>
hand.reply(cr, self.0.lock().await.writer.close().await.map_err(|e| e.into())).await,
}
}
}
pub struct OrcReader<T: AsyncRead + 'static>(pub T);
impl<T: AsyncRead + 'static> ToExpr for OrcReader<T> {
async fn to_gen(self) -> GExpr {
new_atom(ReaderAtom(Rc::new(Mutex::new(BufReader::new(Box::pin(self.0))))))
}
}
#[derive(Clone)]
pub struct ReaderAtom(Rc<Mutex<BufReader<Pin<Box<dyn AsyncRead>>>>>);
impl Atomic for ReaderAtom {
type Variant = OwnedVariant;
type Data = ();
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<ReadReq>() }
}
impl OwnedAtom for ReaderAtom {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<ReadReq> for ReaderAtom {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ReadReq) -> Result<Receipt> {
let mut buf = Vec::new();
let mut reader = self.0.lock().await;
let rep = match match req.limit {
ReadLimit::End => reader.read_to_end(&mut buf).await.map(|_| ()),
ReadLimit::Delimiter(b) => reader.read_until(b, &mut buf).await.map(|_| ()),
ReadLimit::Length(n) => {
buf = vec![0u8; n.get() as usize];
reader.read_exact(&mut buf).await
},
} {
Err(e) => Err(e.into()),
Ok(()) => Ok(buf),
};
hand.reply(&req, rep).await
}
}

View File

@@ -16,15 +16,15 @@ pub type ReqForSystem<T> = <CardForSystem<T> as SystemCard>::Req;
/// System as defined by author
pub trait System: Debug + 'static {
type Ctor: SystemCtor<Instance = Self>;
fn prelude(&self) -> impl Future<Output = Vec<Sym>>;
fn env(&self) -> impl Future<Output = Vec<GenMember>>;
fn lexers(&self) -> Vec<LexerObj>;
fn parsers(&self) -> Vec<ParserObj>;
fn request<'a>(
fn prelude(&self) -> impl Future<Output = Vec<Sym>> { futures::future::ready(Vec::new()) }
fn env(&self) -> impl Future<Output = Vec<GenMember>> { futures::future::ready(Vec::new()) }
fn lexers(&self) -> Vec<LexerObj> { Vec::new() }
fn parsers(&self) -> Vec<ParserObj> { Vec::new() }
fn request(
&self,
hand: Box<dyn ReqHandle<'a> + 'a>,
hand: Box<dyn ReqHandle>,
req: ReqForSystem<Self>,
) -> impl Future<Output = Receipt<'a>>;
) -> impl Future<Output = Receipt>;
}
pub trait DynSystem: Debug + 'static {
@@ -32,10 +32,7 @@ pub trait DynSystem: Debug + 'static {
fn dyn_env(&self) -> LocalBoxFuture<'_, Vec<GenMember>>;
fn dyn_lexers(&self) -> Vec<LexerObj>;
fn dyn_parsers(&self) -> Vec<ParserObj>;
fn dyn_request<'a, 'b: 'a>(
&'a self,
hand: Box<dyn ReqReader<'b> + 'b>,
) -> LocalBoxFuture<'a, Receipt<'b>>;
fn dyn_request<'a, 'b: 'a>(&'a self, hand: Box<dyn ReqReader>) -> LocalBoxFuture<'a, Receipt>;
fn card(&self) -> Box<dyn DynSystemCard>;
}
@@ -46,8 +43,8 @@ impl<T: System> DynSystem for T {
fn dyn_parsers(&self) -> Vec<ParserObj> { self.parsers() }
fn dyn_request<'a, 'b: 'a>(
&'a self,
mut hand: Box<dyn ReqReader<'b> + 'b>,
) -> LocalBoxFuture<'a, Receipt<'b>> {
mut hand: Box<dyn ReqReader>,
) -> LocalBoxFuture<'a, Receipt> {
Box::pin(async move {
let value = hand.read_req().await.unwrap();
self.request(hand.finish().await, value).await

View File

@@ -5,7 +5,10 @@ use std::num::NonZero;
use orchid_api_traits::Coding;
use orchid_base::BoxedIter;
use crate::{AtomOps, AtomTypeId, Atomic, AtomicFeatures, Fun, Lambda, Replier, SystemCtor};
use crate::{
AtomOps, AtomTypeId, Atomic, AtomicFeatures, CmdAtom, Fun, Lambda, ReaderAtom, Replier,
SystemCtor, WriterAtom,
};
/// Description of a system. This is intended to be a ZST storing the static
/// properties of a [SystemCtor] which should be known to foreign systems
@@ -56,5 +59,13 @@ pub(crate) trait DynSystemCardExt: DynSystemCard {
/// The indices of these are bitwise negated, such that the MSB of an atom index
/// marks whether it belongs to this package (0) or the importer (1)
pub(crate) fn general_atoms() -> impl Iterator<Item = Option<Box<dyn AtomOps>>> {
[Some(Fun::ops()), Some(Lambda::ops()), Some(Replier::ops())].into_iter()
[
Some(Fun::ops()),
Some(Lambda::ops()),
Some(Replier::ops()),
Some(CmdAtom::ops()),
Some(WriterAtom::ops()),
Some(ReaderAtom::ops()),
]
.into_iter()
}

View File

@@ -13,7 +13,7 @@ use substack::Substack;
use task_local::task_local;
use trait_set::trait_set;
use crate::gen_expr::{GExpr, new_atom};
use crate::gen_expr::{GExpr, new_atom, serialize};
use crate::{BorrowedExprStore, Expr, ExprFunc, ExprHandle, Fun, ToExpr, api};
/// Tokens generated by lexers and parsers
@@ -31,7 +31,7 @@ impl TokenVariant<api::Expression> for GExpr {
async fn from_api(_: api::Expression, _: &mut Self::FromApiCtx<'_>, _: SrcRange) -> Self {
panic!("Received new expression from host")
}
async fn into_api(self, _: &mut Self::ToApiCtx<'_>) -> api::Expression { self.serialize().await }
async fn into_api(self, _: &mut Self::ToApiCtx<'_>) -> api::Expression { serialize(self).await }
}
impl TokenVariant<api::ExprTicket> for Expr {
@@ -193,7 +193,7 @@ impl LazyMemKind {
pub(crate) async fn into_api(self, ctx: &mut impl TreeIntoApiCtx) -> api::MemberKind {
match self {
Self::Lazy(lazy) => api::MemberKind::Lazy(add_lazy(ctx, lazy)),
Self::Const(c) => api::MemberKind::Const(c.serialize().await),
Self::Const(c) => api::MemberKind::Const(serialize(c).await),
Self::Mod(members) => api::MemberKind::Module(api::Module {
members: stream(async |mut cx| {
for m in members {

View File

@@ -4,25 +4,26 @@ use std::pin::Pin;
use futures::future::LocalBoxFuture;
use futures::{AsyncRead, AsyncWrite};
use orchid_base::{Receipt, RepWriter, ReqHandle, ReqReader};
use unsync_pipe::{Reader, Writer};
pub struct TrivialReqCycle<'a> {
pub req: &'a [u8],
pub rep: &'a mut Vec<u8>,
pub struct TrivialReqCycle {
pub req: Reader,
pub rep: Writer,
}
impl<'a> ReqReader<'a> for TrivialReqCycle<'a> {
impl ReqReader for TrivialReqCycle {
fn reader(&mut self) -> Pin<&mut dyn AsyncRead> { Pin::new(&mut self.req) as Pin<&mut _> }
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, Box<dyn ReqHandle<'a> + 'a>> {
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, Box<dyn ReqHandle>> {
Box::pin(async { self as Box<_> })
}
}
impl<'a> ReqHandle<'a> for TrivialReqCycle<'a> {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Box<dyn RepWriter<'a> + 'a>>> {
impl ReqHandle for TrivialReqCycle {
fn start_reply(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Box<dyn RepWriter>>> {
Box::pin(async { Ok(self as Box<_>) })
}
}
impl<'a> RepWriter<'a> for TrivialReqCycle<'a> {
impl RepWriter for TrivialReqCycle {
fn writer(&mut self) -> Pin<&mut dyn AsyncWrite> { Pin::new(&mut self.rep) as Pin<&mut _> }
fn finish(self: Box<Self>) -> LocalBoxFuture<'a, io::Result<Receipt<'a>>> {
fn finish(self: Box<Self>) -> LocalBoxFuture<'static, io::Result<Receipt>> {
Box::pin(async { Ok(Receipt::_new()) })
}
}

View File

@@ -10,6 +10,7 @@ async-event = "0.2.1"
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
bound = "0.6.0"
chrono = "0.4.44"
derive_destructure = "1.0.0"
futures = { version = "0.3.31", features = ["std"], default-features = false }
futures-locks = "0.7.1"

View File

@@ -94,7 +94,7 @@ impl AtomHand {
#[must_use]
pub fn ext(&self) -> &Extension { self.sys().ext() }
pub async fn req(&self, key: api::TStrv, req: Vec<u8>) -> Option<Vec<u8>> {
self.0.owner.client().request(api::FinalFwded(self.0.api_ref(), key, req)).await.unwrap()
self.0.owner.client().request(api::Fwded(self.0.api_ref(), key, req)).await.unwrap()
}
#[must_use]
pub fn api_ref(&self) -> api::Atom { self.0.api_ref() }

View File

@@ -7,40 +7,52 @@ use std::rc::Rc;
use async_event::Event;
use async_fn_stream::stream;
use chrono::{DateTime, Utc};
use futures::channel::mpsc;
use futures::future::LocalBoxFuture;
use futures::stream::FuturesUnordered;
use futures::{SinkExt, StreamExt, select};
use never::Never;
use orchid_base::{OrcErrv, Receipt, ReqHandle, Sym};
use orchid_extension::{self as ox, AtomicFeatures as _};
use orchid_base::{OrcErrv, Receipt, ReqHandle, Sym, fmt, is, log, mk_errv, sym};
use orchid_extension::{self as ox, AtomicFeatures as _, get_arg};
use crate::ctx::Ctx;
use crate::execute::{ExecCtx, ExecResult};
use crate::expr::{Expr, ExprFromApiCtx, PathSetBuilder};
use crate::extension::Extension;
use crate::inline::ext_inline;
use crate::system::System;
use crate::tree::Root;
/// Events internally recognized by this system sent through [CommandQueue]
enum Task {
RunCommand(Expr),
Sleep(DateTime<Utc>, Expr),
Exit,
}
struct CommandQueueState {
new: VecDeque<Expr>,
new: VecDeque<Task>,
added: Rc<Event>,
wants_exit: bool,
ctx: Ctx,
}
/// Shared object serving as a communication point between the extension
/// [CmdSystemCtor] and the host toolkit [CmdRunner]
#[derive(Clone)]
struct CommandQueue(Rc<RefCell<CommandQueueState>>);
impl CommandQueue {
fn new(ctx: Ctx, init: impl IntoIterator<Item = Expr>) -> Self {
Self(Rc::new(RefCell::new(CommandQueueState {
new: init.into_iter().collect(),
new: init.into_iter().map(Task::RunCommand).collect(),
added: Rc::default(),
wants_exit: false,
ctx,
})))
}
pub fn push(&self, expr: Expr) {
pub fn push(&self, task: Task) {
let was_empty = {
let mut g = self.0.borrow_mut();
g.new.push_back(expr);
g.new.push_back(task);
g.new.len() == 1
};
if was_empty {
@@ -48,7 +60,7 @@ impl CommandQueue {
added.notify_one();
}
}
pub async fn get_new(&self) -> Expr {
pub async fn get_new(&self) -> Task {
let added = {
let mut g = self.0.borrow_mut();
if let Some(waiting) = g.new.pop_front() {
@@ -65,10 +77,13 @@ impl Debug for CommandQueue {
}
}
pub enum CmdResult {
/// All command sequences settled
/// Events the embedder may want to be notified about
pub enum CmdEvent {
/// All commands finished and there's nothing else to do
Settled,
/// Exit was requested explicitly by usercode
/// Exit was requested explicitly by usercode. This request means that all
/// internal system state should be discarded, so if it is returned by the
/// [CmdRunner], no further commands are permitted
Exit,
/// Ran out of gas
Gas,
@@ -85,39 +100,68 @@ pub enum CmdResult {
}
pub struct CmdRunner {
root: Root,
queue: CommandQueue,
gas: Option<u64>,
interrupted: Option<ExecCtx>,
futures: FuturesUnordered<LocalBoxFuture<'static, Option<CmdResult>>>,
system: System,
futures: FuturesUnordered<LocalBoxFuture<'static, Option<CmdEvent>>>,
}
impl CmdRunner {
pub async fn new(root: Root, ctx: Ctx, init: impl IntoIterator<Item = Expr>) -> Self {
Self {
futures: FuturesUnordered::new(),
gas: None,
root,
interrupted: None,
queue: CommandQueue::new(ctx, init),
}
pub async fn new(root: &mut Root, ctx: Ctx, init: impl IntoIterator<Item = Expr>) -> Self {
let queue = CommandQueue::new(ctx.clone(), init);
let ext_builder = ox::ExtensionBuilder::new("orchid::cmd").system(CmdSystemCtor(queue.clone()));
let extension = (Extension::new(ext_inline(ext_builder, ctx.clone()).await, ctx).await)
.expect("IO error on in-memory pipe");
let system_ctor = (extension.system_ctors().find(|ctor| ctor.decl.name == "orchid::cmd"))
.expect("Missing command system ctor");
let (cmd_root, system) = system_ctor.run(vec![]).await;
*root = root.merge(&cmd_root).await.expect("Could not merge command system into tree");
Self { futures: FuturesUnordered::new(), gas: None, interrupted: None, queue, system }
}
pub fn push(&self, expr: Expr) { self.queue.push(Task::RunCommand(expr)); }
#[must_use]
pub fn sys(&self) -> &System { &self.system }
#[must_use]
pub fn get_gas(&self) -> u64 { self.gas.expect("queried gas but no gas was set") }
pub fn set_gas(&mut self, gas: u64) { self.gas = Some(gas) }
pub fn disable_gas(&mut self) { self.gas = None }
pub async fn execute(&mut self) -> CmdResult {
pub async fn execute(&mut self, root: &Root) -> CmdEvent {
let waiting_on_queue = RefCell::new(false);
let (mut spawn, mut on_spawn) = mpsc::channel(1);
let (mut spawn, mut on_spawn) = mpsc::channel::<LocalBoxFuture<Option<CmdEvent>>>(1);
let mut normalize_stream = pin!(
stream(async |mut h| {
loop {
if self.queue.0.borrow().wants_exit {
h.emit(CmdResult::Exit).await;
break;
}
waiting_on_queue.replace(false);
let mut xctx = match self.interrupted.take() {
None => ExecCtx::new(self.root.clone(), self.queue.get_new().await).await,
None => match self.queue.get_new().await {
Task::RunCommand(expr) => ExecCtx::new(root.clone(), expr).await,
Task::Sleep(until, expr) => {
let queue = self.queue.clone();
let ctx = queue.0.borrow_mut().ctx.clone();
spawn
.send(Box::pin(async move {
let delta = until - Utc::now();
match delta.to_std() {
Err(_) =>
writeln!(
log("debug"),
"Negative sleep found ({delta}), requeuing as instant"
)
.await,
Ok(delay) => ctx.sleep(delay).await,
};
queue.push(Task::RunCommand(expr));
None
}))
.await
.expect("Receiver stored in parent future");
continue;
},
Task::Exit => {
h.emit(CmdEvent::Exit).await;
break;
},
},
Some(xctx) => xctx,
};
waiting_on_queue.replace(true);
@@ -126,28 +170,30 @@ impl CmdRunner {
match res {
ExecResult::Err(e, gas) => {
self.gas = gas;
h.emit(CmdResult::Err(e)).await;
h.emit(CmdEvent::Err(e)).await;
},
ExecResult::Gas(exec) => {
self.interrupted = Some(exec);
h.emit(CmdResult::Gas).await;
h.emit(CmdEvent::Gas).await;
},
ExecResult::Value(val, gas) => {
self.gas = gas;
let Some(atom) = val.as_atom().await else {
h.emit(CmdResult::NonCommand(val)).await;
h.emit(CmdEvent::NonCommand(val)).await;
continue;
};
let queue = self.queue.clone();
let ctx = queue.0.borrow_mut().ctx.clone();
spawn
.send(Box::pin(async move {
match atom.ipc(ox::std_reqs::RunCommand).await {
None => Some(CmdResult::NonCommand(val)),
match atom.ipc(ox::std_reqs::StartCommand).await {
None => Some(CmdEvent::NonCommand(val)),
Some(None) => None,
Some(Some(expr)) => {
let from_api_cx = ExprFromApiCtx { ctx, sys: atom.api_ref().owner };
queue.push(Expr::from_api(expr, PathSetBuilder::new(), from_api_cx).await);
queue.push(Task::RunCommand(
Expr::from_api(expr, PathSetBuilder::new(), from_api_cx).await,
));
None
},
}
@@ -161,17 +207,26 @@ impl CmdRunner {
.fuse()
);
loop {
if self.queue.0.borrow().wants_exit {
break CmdResult::Exit;
}
let task = select!(
r_opt = self.futures.by_ref().next() => match r_opt {
Some(Some(r)) => break r,
None if *waiting_on_queue.borrow() => break CmdResult::Settled,
Some(Some(r)) => {
eprintln!("Exiting because ");
break r
},
None if *waiting_on_queue.borrow() => {
eprintln!("Exiting because settled");
break CmdEvent::Settled
},
None | Some(None) => continue,
},
r = normalize_stream.by_ref().next() => break r.expect("infinite stream"),
task = on_spawn.by_ref().next() => task.expect("sender moved into infinite stream"),
r = normalize_stream.by_ref().next() => match r {
None => break CmdEvent::Exit,
Some(r) => break r,
},
task = on_spawn.by_ref().next() => match task {
None => continue,
Some(r) => r,
},
);
self.futures.push(task)
}
@@ -189,9 +244,7 @@ impl ox::SystemCard for CmdSystemCard {
}
#[derive(Debug)]
pub struct CmdSystemCtor {
queue: CommandQueue,
}
pub struct CmdSystemCtor(CommandQueue);
impl ox::SystemCtor for CmdSystemCtor {
const NAME: &'static str = "orchid::cmd";
const VERSION: f64 = 0.1;
@@ -199,7 +252,7 @@ impl ox::SystemCtor for CmdSystemCtor {
type Deps = ();
type Instance = CmdSystemInst;
fn inst(&self, _: <Self::Deps as orchid_extension::DepDef>::Sat) -> Self::Instance {
CmdSystemInst { queue: self.queue.clone() }
CmdSystemInst { queue: self.0.clone() }
}
}
@@ -217,24 +270,47 @@ impl ox::System for CmdSystemInst {
ox::tree::fun(true, "spawn", async |side: ox::Expr, cont: ox::Expr| {
ox::cmd(async move || {
let queue = ox_get_queue();
let side_xtk = side.serialize().await;
let side_xtk = side.clone().serialize().await;
let mut g = queue.0.borrow_mut();
let host_ex =
g.ctx.exprs.take_expr(side_xtk).expect("Host could not locate leaked expr by ID ");
g.new.push_back(host_ex);
Some(cont)
g.new.push_back(Task::RunCommand(host_ex));
Some(cont.clone())
})
}),
ox::tree::cnst(true, "yield", ox::cmd(async || None::<Never>)),
ox::tree::cnst(
true,
"exit",
ox::cmd(async || {
ox_get_queue().push(Task::Exit);
None::<Never>
}),
),
ox::tree::fun(true, "sleep", async |until: ox::ForeignAtom, cont: ox::Expr| {
let Some(until) = until.call(ox::std_reqs::AsInstant).await else {
return ox::gen_expr::bot(mk_errv(
is("Not an instant").await,
format!("{} is not an instant", fmt(&until).await),
[get_arg(0).pos().await],
));
};
ox::cmd(async move || {
let queue = ox_get_queue();
let cont_xtk = cont.clone().serialize().await;
let mut g = queue.0.borrow_mut();
let host_ex =
g.ctx.exprs.take_expr(cont_xtk).expect("Host could not locate leaked expr by ID ");
g.new.push_back(Task::Sleep(until, host_ex));
None::<Never>
})
}),
])
}
async fn prelude(&self) -> Vec<Sym> { vec![] }
async fn prelude(&self) -> Vec<Sym> { vec![sym!("orchid")] }
fn lexers(&self) -> Vec<ox::LexerObj> { vec![] }
fn parsers(&self) -> Vec<ox::ParserObj> { vec![] }
async fn request<'a>(
&self,
_hand: Box<dyn ReqHandle<'a> + 'a>,
req: ox::ReqForSystem<Self>,
) -> Receipt<'a> {
async fn request(&self, _hand: Box<dyn ReqHandle>, req: ox::ReqForSystem<Self>) -> Receipt {
match req {}
}
}

View File

@@ -18,7 +18,8 @@ pub trait JoinHandle {
/// It is guaranteed that the future will never be polled after this is called
fn abort(&self);
/// take the future out of the task. If the return value
/// is dropped, the spawned future is also dropped
/// is dropped, the spawned future is also dropped. The returned boxed future
/// will finish any sleep requested at spawn.
fn join(self: Box<Self>) -> LocalBoxFuture<'static, ()>;
}
@@ -30,6 +31,9 @@ pub trait Spawner {
/// exit while there are pending tasks to allow external communication
/// channels to cleanly shut down.
fn spawn_obj(&self, delay: Duration, fut: LocalBoxFuture<'static, ()>) -> Box<dyn JoinHandle>;
fn sleep(&self, delay: Duration) -> LocalBoxFuture<'static, ()> {
self.spawn_obj(delay, Box::pin(async move {})).join()
}
}
pub struct CtxData {
@@ -79,6 +83,9 @@ impl Ctx {
) -> Box<dyn JoinHandle> {
self.spawner.spawn_obj(delay, Box::pin(fut))
}
/// Return after an amount of time has passed
#[must_use]
pub async fn sleep(&self, delay: Duration) { self.spawner.sleep(delay).await }
#[must_use]
pub(crate) async fn system_inst(&self, id: api::SysId) -> Option<System> {
self.systems.read().await.get(&id).and_then(WeakSystem::upgrade)

View File

@@ -173,7 +173,7 @@ async fn print_exprkind<'a>(
None => format!("Bottom(\n\t{}\n)", indent(&e.to_string())).into(),
},
ExprKind::Call(f, x) => tl_cache!(Rc<Variants>: Rc::new(Variants::default()
.unbounded("{0b} {1l}")
// .unbounded("{0b} {1l}")
.bounded("({0b} {1})")))
.units([print_expr(f, c, visited, id_only).await, print_expr(x, c, visited, id_only).await]),
ExprKind::Identity(id) =>

View File

@@ -27,7 +27,7 @@ use crate::ctx::{Ctx, JoinHandle};
use crate::dealias::{ChildError, ChildErrorKind, walk};
use crate::expr::{Expr, ExprFromApiCtx, PathSetBuilder};
use crate::system::SystemCtor;
use crate::tree::MemberKind;
use crate::tree::MemKind;
pub struct ExtPort {
pub input: Pin<Box<dyn AsyncWrite>>,
@@ -54,17 +54,19 @@ pub struct ExtensionData {
strings: RefCell<HashSet<IStr>>,
string_vecs: RefCell<HashSet<IStrv>>,
/// Moved over from [ExtPort] to allow hooking to the extension's drop
_drop_trigger: Box<dyn Any>,
drop_trigger: Box<dyn Any>,
}
impl Drop for ExtensionData {
fn drop(&mut self) {
let client = self.client.clone();
let join_ext = self.join_ext.take().expect("Only called once in Drop");
let comm_cx = self.comm_cx.take().expect("Only used here");
let drop_trigger = std::mem::replace(&mut self.drop_trigger, Box::new(()));
stash(async move {
client.notify(api::HostExtNotif::Exit).await.unwrap();
comm_cx.exit().await.unwrap();
join_ext.join().await;
std::mem::drop(drop_trigger);
})
}
}
@@ -131,46 +133,46 @@ impl Extension {
}
let this = Self(weak.upgrade().unwrap());
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::IntReq::InternStr(s) => {
let i = is(&s.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&s, &i.to_api()).await
handle.reply(&s, i.to_api()).await
},
api::IntReq::InternStrv(v) => {
let tokens = join_all(v.0.iter().map(|m| es(*m))).await;
this.0.strings.borrow_mut().extend(tokens.iter().cloned());
let i = iv(&tokens).await;
this.0.string_vecs.borrow_mut().insert(i.clone());
handle.reply(&v, &i.to_api()).await
handle.reply(&v, i.to_api()).await
},
api::IntReq::ExternStr(si) => {
let i = es(si.0).await;
this.0.strings.borrow_mut().insert(i.clone());
handle.reply(&si, &i.to_string()).await
handle.reply(&si, i.to_string()).await
},
api::IntReq::ExternStrv(vi) => {
let i = ev(vi.0).await;
this.0.strings.borrow_mut().extend(i.iter().cloned());
this.0.string_vecs.borrow_mut().insert(i.clone());
let markerv = i.iter().map(|t| t.to_api()).collect_vec();
handle.reply(&vi, &markerv).await
handle.reply(&vi, markerv).await
},
},
api::ExtHostReq::Fwd(ref fw @ api::Fwd(ref atom, ref key, ref body)) => {
api::ExtHostReq::Fwd(ref fw @ api::Fwd { ref target, ref method, ref body }) => {
let sys =
ctx.system_inst(atom.owner).await.expect("owner of live atom dropped");
ctx.system_inst(target.owner).await.expect("owner of live atom dropped");
let client = sys.client();
let reply = client
.request(api::FinalFwded(fw.0.clone(), *key, body.clone()))
.request(api::Fwded(target.clone(), *method, body.clone()))
.await
.unwrap();
handle.reply(fw, &reply).await
handle.reply(fw, reply).await
},
api::ExtHostReq::SysFwd(ref fw @ api::SysFwd(id, ref body)) => {
let sys = ctx.system_inst(id).await.unwrap();
handle.reply(fw, &sys.request(body.clone()).await).await
handle.reply(fw, sys.request(body.clone()).await).await
},
api::ExtHostReq::SubLex(sl) => {
let (rep_in, mut rep_out) = channel(0);
@@ -180,19 +182,27 @@ impl Extension {
lex_g.get(&sl.id).cloned().expect("Sublex for nonexistent lexid");
req_in.send(ReqPair(sl.clone(), rep_in)).await.unwrap();
}
handle.reply(&sl, &rep_out.next().await.unwrap()).await
handle.reply(&sl, rep_out.next().await.unwrap()).await
},
api::ExtHostReq::ExprReq(expr_req) => match expr_req {
api::ExprReq::Inspect(ins @ api::Inspect { target }) => {
let expr = ctx.exprs.get_expr(target).expect("Invalid ticket");
handle
.reply(&ins, &api::Inspected {
.reply(&ins, api::Inspected {
refcount: expr.strong_count() as u32,
location: expr.pos().to_api(),
kind: expr.to_api().await,
})
.await
},
api::ExprReq::ExprPrint(prt @ api::ExprPrint { target }) => {
let msg = match ctx.exprs.get_expr(target) {
None => "EXPIRED_TICKET".into(),
Some(expr) => expr.print(&FmtCtxImpl::default()).await,
}
.to_api();
handle.reply(&prt, msg).await
},
api::ExprReq::Create(cre) => {
let req = Witness::of(&cre);
let api::Create(sys, expr) = cre;
@@ -203,7 +213,7 @@ impl Extension {
.await;
let expr_id = expr.id();
ctx.exprs.give_expr(expr);
handle.reply(req, &expr_id).await
handle.reply(req, expr_id).await
},
},
api::ExtHostReq::LsModule(ref ls @ api::LsModule(_sys, path)) => {
@@ -229,14 +239,14 @@ impl Extension {
let mut members = std::collections::HashMap::new();
for (k, v) in &module.members {
let kind = match v.kind(ctx.clone(), &root_data.consts).await {
MemberKind::Const => api::MemberInfoKind::Constant,
MemberKind::Module(_) => api::MemberInfoKind::Module,
MemKind::Const => api::MemberInfoKind::Constant,
MemKind::Module(_) => api::MemberInfoKind::Module,
};
members.insert(k.to_api(), api::MemberInfo { public: v.public, kind });
}
Ok(api::ModuleInfo { members })
};
handle.reply(ls, &reply).await
handle.reply(ls, reply).await
},
api::ExtHostReq::ResolveNames(ref rn) => {
let api::ResolveNames { constid, names, sys } = rn;
@@ -265,12 +275,12 @@ impl Extension {
})
.collect()
.await;
handle.reply(rn, &responses).await
handle.reply(rn, responses).await
},
api::ExtHostReq::ExtAtomPrint(ref eap @ api::ExtAtomPrint(ref atom)) => {
let atom = AtomHand::from_api(atom, Pos::None, &mut ctx.clone()).await;
let unit = atom.print(&FmtCtxImpl::default()).await;
handle.reply(eap, &unit.to_api()).await
handle.reply(eap, unit.to_api()).await
},
}
})
@@ -293,7 +303,7 @@ impl Extension {
client: Rc::new(client),
strings: RefCell::default(),
string_vecs: RefCell::default(),
_drop_trigger: init.drop_trigger,
drop_trigger: init.drop_trigger,
}
})))
}

View File

@@ -262,8 +262,9 @@ pub async fn sys_lex(ctx: &mut LexCtx<'_>) -> Option<OrcRes<Vec<ParsTokTree>>> {
})
.await;
match lx {
Err(e) =>
return Some(Err(errors.into_iter().fold(OrcErrv::from_api(e).await, |a, b| a + b))),
Err(e) => {
return Some(Err(errors.into_iter().fold(OrcErrv::from_api(e).await, |a, b| a + b)));
},
Ok(Some(lexed)) => {
ctx.set_pos(lexed.pos);
let mut stable_trees = Vec::new();

View File

@@ -1,9 +1,9 @@
use orchid_api as api;
pub mod atom;
pub mod ctx;
#[cfg(feature = "orchid-extension")]
pub mod cmd_system;
pub mod ctx;
pub mod dealias;
#[cfg(feature = "tokio")]
pub mod dylib;

View File

@@ -8,7 +8,7 @@ use substack::Substack;
use crate::ctx::Ctx;
use crate::expr::Expr;
use crate::parsed::{Item, ItemKind, ParsedMember, ParsedMemberKind, ParsedModule};
use crate::parsed::{Item, ItemKind, ParsedMemKind, ParsedMember, ParsedModule};
use crate::system::System;
type ParsSnippet<'a> = Snippet<'a, Expr, Expr>;
@@ -101,7 +101,7 @@ pub async fn parse_exportable_item<'a>(
) -> OrcRes<Vec<Item>> {
let kind = if discr == is("mod").await {
let (name, body) = parse_module(ctx, path, tail).await?;
ItemKind::Member(ParsedMember { name, exported, kind: ParsedMemberKind::Mod(body) })
ItemKind::Member(ParsedMember { name, exported, kind: ParsedMemKind::Mod(body) })
} else if let Some(parser) = ctx.systems().find_map(|s| s.get_parser(discr.clone())) {
return parser
.parse(ctx, path, tail.to_vec(), exported, comments, &mut async |stack, lines| {

View File

@@ -53,10 +53,10 @@ impl Format for Item {
let item_text = match &self.kind {
ItemKind::Import(i) => format!("import {i}").into(),
ItemKind::Member(mem) => match &mem.kind {
ParsedMemberKind::Const(_, sys) =>
ParsedMemKind::Const(_, sys) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("const {0} via {1}")))
.units([mem.name.to_string().into(), sys.print(c).await]),
ParsedMemberKind::Mod(module) =>
ParsedMemKind::Mod(module) =>
tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("module {0} {{\n\t{1}\n}}")))
.units([mem.name.to_string().into(), module.print(c).boxed_local().await]),
},
@@ -69,12 +69,12 @@ impl Format for Item {
pub struct ParsedMember {
pub name: IStr,
pub exported: bool,
pub kind: ParsedMemberKind,
pub kind: ParsedMemKind,
}
impl ParsedMember {
#[must_use]
pub fn name(&self) -> IStr { self.name.clone() }
pub fn new(exported: bool, name: IStr, kind: impl Into<ParsedMemberKind>) -> Self {
pub fn new(exported: bool, name: IStr, kind: impl Into<ParsedMemKind>) -> Self {
Self { exported, name, kind: kind.into() }
}
}
@@ -101,11 +101,11 @@ impl fmt::Debug for ParsedExpr {
}
#[derive(Debug)]
pub enum ParsedMemberKind {
pub enum ParsedMemKind {
Const(api::ParsedConstId, System),
Mod(ParsedModule),
}
impl From<ParsedModule> for ParsedMemberKind {
impl From<ParsedModule> for ParsedMemKind {
fn from(value: ParsedModule) -> Self { Self::Mod(value) }
}
#[derive(Debug, Default)]
@@ -137,7 +137,7 @@ impl ParsedModule {
.filter_map(|it| if let ItemKind::Import(i) = &it.kind { Some(i) } else { None })
}
pub fn default_item(self, name: IStr, sr: SrcRange) -> Item {
let mem = ParsedMember { exported: true, name, kind: ParsedMemberKind::Mod(self) };
let mem = ParsedMember { exported: true, name, kind: ParsedMemKind::Mod(self) };
Item { comments: vec![], sr, kind: ItemKind::Member(mem) }
}
}
@@ -157,8 +157,8 @@ impl Tree for ParsedModule {
.find(|m| m.name == key)
{
match &member.kind {
ParsedMemberKind::Const(..) => return ChildResult::Err(ChildErrorKind::Constant),
ParsedMemberKind::Mod(m) => return ChildResult::Ok(m),
ParsedMemKind::Const(..) => return ChildResult::Err(ChildErrorKind::Constant),
ParsedMemKind::Mod(m) => return ChildResult::Ok(m),
}
}
ChildResult::Err(ChildErrorKind::Missing)

View File

@@ -9,7 +9,7 @@ use crate::expr::ExprFromApiCtx;
use crate::expr_store::ExprStore;
use crate::parse::HostParseCtx;
use crate::parsed::{
Item, ItemKind, ParsTokTree, ParsedMember, ParsedMemberKind, ParsedModule, tt_to_api,
Item, ItemKind, ParsTokTree, ParsedMemKind, ParsedMember, ParsedModule, tt_to_api,
};
use crate::system::System;
@@ -88,11 +88,11 @@ async fn conv(
let mkind = match kind {
api::ParsedMemberKind::Module { lines, use_prelude } => {
let items = conv(lines, mem_path, callback, ctx).boxed_local().await?;
ParsedMemberKind::Mod(ParsedModule::new(use_prelude, items))
ParsedMemKind::Mod(ParsedModule::new(use_prelude, items))
},
api::ParsedMemberKind::Constant(cid) => {
ctx.sys.0.const_paths.insert(cid, ctx.mod_path.suffix(mem_path.unreverse()).await);
ParsedMemberKind::Const(cid, ctx.sys.clone())
ParsedMemKind::Const(cid, ctx.sys.clone())
},
};
items.push(Item {

View File

@@ -21,11 +21,11 @@ use crate::api;
use crate::ctx::Ctx;
use crate::dealias::{ChildErrorKind, Tree, absolute_path, resolv_glob, walk};
use crate::expr::{Expr, ExprFromApiCtx, PathSetBuilder};
use crate::parsed::{ItemKind, ParsedMemberKind, ParsedModule};
use crate::parsed::{ItemKind, ParsedMemKind, ParsedModule};
use crate::system::System;
pub struct RootData {
pub root: Module,
pub root: Mod,
pub consts: MemoMap<Sym, Expr>,
pub ctx: Ctx,
}
@@ -34,17 +34,13 @@ pub struct Root(pub Rc<RwLock<RootData>>);
impl Root {
#[must_use]
pub fn new(ctx: Ctx) -> Self {
Root(Rc::new(RwLock::new(RootData {
root: Module::default(),
consts: MemoMap::default(),
ctx,
})))
Root(Rc::new(RwLock::new(RootData { root: Mod::default(), consts: MemoMap::default(), ctx })))
}
#[must_use]
pub async fn from_api(api: api::Module, sys: &System) -> Self {
let consts = MemoMap::new();
let mut tfac = TreeFromApiCtx { consts: &consts, path: iv(&[][..]).await, sys };
let root = Module::from_api(api, &mut tfac).await;
let root = Mod::from_api(api, &mut tfac).await;
Root(Rc::new(RwLock::new(RootData { root, consts, ctx: sys.ctx().clone() })))
}
pub async fn merge(&self, new: &Root) -> Result<Self, MergeErr> {
@@ -71,14 +67,14 @@ impl Root {
root: &this.root,
ctx: &this.ctx,
};
let mut module = Module::from_parsed(parsed, pars_prefix.clone(), &mut tfpctx).await;
let mut module = Mod::from_parsed(parsed, pars_prefix.clone(), &mut tfpctx).await;
for step in pars_prefix.iter().rev() {
let kind = OnceCell::from(MemberKind::Module(module));
let kind = OnceCell::from(MemKind::Module(module));
let members = HashMap::from([(
step.clone(),
Rc::new(Member { public: true, lazy: RefCell::new(None), kind }),
)]);
module = Module { imports: HashMap::new(), members }
module = Mod { imports: HashMap::new(), members }
}
let root = (this.root.merge(&module, this.ctx.clone(), &consts).await)
.expect("Merge conflict between parsed and existing module");
@@ -159,11 +155,11 @@ pub struct ResolvedImport {
}
#[derive(Clone, Default)]
pub struct Module {
pub struct Mod {
pub imports: HashMap<IStr, Result<ResolvedImport, Vec<ResolvedImport>>>,
pub members: HashMap<IStr, Rc<Member>>,
}
impl Module {
impl Mod {
#[must_use]
pub async fn from_api(api: api::Module, ctx: &mut TreeFromApiCtx<'_>) -> Self {
let mut members = HashMap::new();
@@ -178,11 +174,11 @@ impl Module {
let cx = ExprFromApiCtx { ctx: ctx.sys.ctx().clone(), sys: ctx.sys.id() };
let expr = Expr::from_api(val, PathSetBuilder::new(), cx).await;
ctx.consts.insert(name.clone(), expr);
(None, Some(MemberKind::Const))
(None, Some(MemKind::Const))
},
api::MemberKind::Module(m) => {
let m = Self::from_api(m, &mut ctx.push(mem_name.clone()).await).boxed_local().await;
(None, Some(MemberKind::Module(m)))
(None, Some(MemKind::Module(m)))
},
};
members.insert(
@@ -305,7 +301,7 @@ impl Module {
match &item.kind {
ItemKind::Member(mem) => {
let path = path.to_vname().suffix([mem.name.clone()]).to_sym().await;
let kind = OnceCell::from(MemberKind::from_parsed(&mem.kind, path.clone(), ctx).await);
let kind = OnceCell::from(MemKind::from_parsed(&mem.kind, path.clone(), ctx).await);
members.insert(
mem.name.clone(),
Rc::new(Member { kind, lazy: RefCell::default(), public: mem.exported }),
@@ -314,14 +310,14 @@ impl Module {
ItemKind::Import(_) => (),
}
}
Module { imports, members }
Mod { imports, members }
}
pub async fn merge(
&self,
other: &Module,
other: &Mod,
ctx: Ctx,
consts: &MemoMap<Sym, Expr>,
) -> Result<Module, MergeErr> {
) -> Result<Mod, MergeErr> {
if !self.imports.is_empty() || !other.imports.is_empty() {
return Err(MergeErr { path: VPath::new([]), kind: MergeErrKind::Imports });
}
@@ -335,7 +331,7 @@ impl Module {
return Err(MergeErr { path: VPath::new([]), kind: MergeErrKind::Visibility });
}
match (own.kind(ctx.clone(), consts).await, mem.kind(ctx.clone(), consts).await) {
(MemberKind::Module(own_sub), MemberKind::Module(sub)) => {
(MemKind::Module(own_sub), MemKind::Module(sub)) => {
match own_sub.merge(sub, ctx.clone(), consts).boxed_local().await {
Ok(module) => {
members.insert(
@@ -343,7 +339,7 @@ impl Module {
Rc::new(Member {
lazy: RefCell::new(None),
public: own.public,
kind: OnceCell::from(MemberKind::Module(module)),
kind: OnceCell::from(MemKind::Module(module)),
}),
);
},
@@ -361,7 +357,7 @@ impl Module {
slot.insert(mem.clone());
}
}
Ok(Module { imports: HashMap::new(), members })
Ok(Mod { imports: HashMap::new(), members })
}
}
@@ -380,13 +376,13 @@ pub enum MergeErrKind {
pub struct FromParsedCtx<'a> {
pars_prefix: Sym,
pars_root: &'a ParsedModule,
root: &'a Module,
root: &'a Mod,
ctx: &'a Ctx,
consts: &'a MemoMap<Sym, Expr>,
deferred_consts: &'a mut Vec<(Sym, api::SysId, api::ParsedConstId)>,
}
impl Tree for Module {
impl Tree for Mod {
type Ctx<'a> = (Ctx, &'a MemoMap<Sym, Expr>);
async fn child(
&self,
@@ -401,8 +397,8 @@ impl Tree for Module {
return Err(ChildErrorKind::Private);
}
match &member.kind(ctx.clone(), consts).await {
MemberKind::Module(m) => Ok(m),
MemberKind::Const => Err(ChildErrorKind::Constant),
MemKind::Module(m) => Ok(m),
MemKind::Const => Err(ChildErrorKind::Constant),
}
}
fn children(&self, public_only: bool) -> hashbrown::HashSet<IStr> {
@@ -413,11 +409,11 @@ impl Tree for Module {
pub struct Member {
pub public: bool,
pub lazy: RefCell<Option<LazyMemberHandle>>,
pub kind: OnceCell<MemberKind>,
pub kind: OnceCell<MemKind>,
}
impl Member {
#[must_use]
pub async fn kind<'a>(&'a self, ctx: Ctx, consts: &MemoMap<Sym, Expr>) -> &'a MemberKind {
pub async fn kind<'a>(&'a self, ctx: Ctx, consts: &MemoMap<Sym, Expr>) -> &'a MemKind {
(self.kind.get_or_init(async {
let handle = self.lazy.borrow_mut().take().expect("If kind is uninit, lazy must be Some");
handle.run(ctx, consts).await
@@ -426,20 +422,20 @@ impl Member {
}
}
pub enum MemberKind {
pub enum MemKind {
Const,
Module(Module),
Module(Mod),
}
impl MemberKind {
impl MemKind {
#[must_use]
async fn from_parsed(parsed: &ParsedMemberKind, path: Sym, ctx: &mut FromParsedCtx<'_>) -> Self {
async fn from_parsed(parsed: &ParsedMemKind, path: Sym, ctx: &mut FromParsedCtx<'_>) -> Self {
match parsed {
ParsedMemberKind::Const(id, sys) => {
ParsedMemKind::Const(id, sys) => {
ctx.deferred_consts.push((path, sys.id(), *id));
MemberKind::Const
MemKind::Const
},
ParsedMemberKind::Mod(m) =>
MemberKind::Module(Module::from_parsed(m, path, ctx).boxed_local().await),
ParsedMemKind::Mod(m) =>
MemKind::Module(Mod::from_parsed(m, path, ctx).boxed_local().await),
}
}
}
@@ -452,7 +448,7 @@ pub struct LazyMemberHandle {
}
impl LazyMemberHandle {
#[must_use]
pub async fn run(mut self, ctx: Ctx, consts: &MemoMap<Sym, Expr>) -> MemberKind {
pub async fn run(mut self, ctx: Ctx, consts: &MemoMap<Sym, Expr>) -> MemKind {
let sys = ctx.system_inst(self.sys).await.expect("Missing system for lazy member");
match sys.get_tree(self.id).await {
api::MemberKind::Const(c) => {
@@ -460,12 +456,12 @@ impl LazyMemberHandle {
let expr = Expr::from_api(c, PathSetBuilder::new(), ctx).await;
let (.., path) = self.destructure();
consts.insert(path, expr);
MemberKind::Const
MemKind::Const
},
api::MemberKind::Module(m) => {
let (.., path) = self.destructure();
MemberKind::Module(
Module::from_api(m, &mut TreeFromApiCtx { sys: &sys, consts, path: path.tok() }).await,
MemKind::Module(
Mod::from_api(m, &mut TreeFromApiCtx { sys: &sys, consts, path: path.tok() }).await,
)
},
api::MemberKind::Lazy(id) => {

View File

@@ -2,9 +2,8 @@ use std::borrow::Cow;
use never::Never;
use orchid_base::fmt;
use orchid_extension::Expr;
use orchid_extension::gen_expr::new_atom;
use orchid_extension::{Atomic, OwnedAtom, OwnedVariant, TAtom, ToExpr, exec};
use orchid_extension::{Atomic, Expr, OwnedAtom, OwnedVariant, TAtom, ToExpr, exec};
use crate::macros::mactree::{MacTok, MacTree};

View File

@@ -4,16 +4,17 @@ use futures::{FutureExt, StreamExt, stream};
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_base::{
Comment, OrcRes, Paren, Parsed, Snippet, Sym, expect_tok, is, report, sym, token_errv,
Comment, OrcRes, Paren, Parsed, Snippet, Sym, expect_tok, is, report, token_errv,
try_pop_no_fluff, with_reporter,
};
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::{
ConstCtx, PSnippet, PTok, PTokTree, ParsCtx, ParsedLine, Parser, TAtom, TryFromExpr,
};
use crate::macros::lower::lower;
use crate::macros::mactree::{MacTok, MacTree, MacTreeSeq};
use crate::macros::ph_lexer::PhAtom;
use crate::macros::resolve::resolve;
#[derive(Default)]
pub struct LetLine;
@@ -35,15 +36,22 @@ impl Parser for LetLine {
};
let Parsed { tail, .. } = expect_tok(tail, is("=").await).await?;
let aliased = parse_tokv(tail).await;
Ok(vec![ParsedLine::cnst(&line.sr(), &comments, exported, name, async move |ctx| {
let macro_input =
MacTok::S(Paren::Round, with_reporter(dealias_mac_v(&aliased, &ctx)).await?).at(sr.pos());
Ok(call(sym!(macros::resolve), new_atom(macro_input)))
})])
Ok(vec![ParsedLine::cnst(
&line.sr(),
&comments,
exported,
name.clone(),
async move |mut ctx| {
let macro_input =
MacTok::S(Paren::Round, with_reporter(dealias_mac_v(&aliased, &ctx)).await?).at(sr.pos());
let resolved = resolve(ctx.handle(), macro_input).await;
Ok(lower(&resolved, substack::Substack::Bottom).await)
},
)])
}
}
pub async fn dealias_mac_v(aliased: &MacTreeSeq, ctx: &ConstCtx) -> MacTreeSeq {
pub async fn dealias_mac_v(aliased: &MacTreeSeq, ctx: &ConstCtx<'_>) -> MacTreeSeq {
let keys = aliased.glossary().iter().cloned().collect_vec();
let mut names: HashMap<_, _> = HashMap::new();
let mut stream = pin!(ctx.names(&keys).zip(stream::iter(&keys)));

View File

@@ -0,0 +1,81 @@
use std::collections::VecDeque;
use async_fn_stream::stream;
use futures::{FutureExt, StreamExt, stream};
use itertools::Itertools;
use orchid_base::{OrcErrv, Paren, Sym, fmt, is, mk_errv, report};
use orchid_extension::ToExpr;
use orchid_extension::gen_expr::{GExpr, GExprKind, bot, call};
use substack::Substack;
use crate::macros::mactree::MacTreeSeq;
use crate::{MacTok, MacTree};
fn on_err(e: OrcErrv) -> GExpr {
report(e.clone());
bot(e)
}
pub async fn lower(mt: &MacTree, args: Substack<'_, Sym>) -> GExpr {
match mt.tok() {
MacTok::Resolved(inner) => lower(inner, args).boxed_local().await,
MacTok::Bottom(b) => on_err(b.clone()),
MacTok::Slot => on_err(mk_errv(
is("Lowering intermediary slotted mactree").await,
"Found a Slot during lowering, which should only exist in temporary templates",
[mt.pos()],
)),
MacTok::Ph(ph) => on_err(mk_errv(
is("Lowering placeholder").await,
format!("Found {ph} during lowering. Placeholders should only exist in macro patterns"),
[mt.pos()],
)),
MacTok::S(Paren::Curly | Paren::Square, _) => on_err(mk_errv(
is("Lowering {...} or [...]").await,
format!("Cannot lower this syntax, probable incorrect macro call: {}", fmt(mt).await),
[mt.pos()],
)),
MacTok::S(Paren::Round, b) if b.items.is_empty() => on_err(mk_errv(
is("Cannot lower empty ()").await,
"Attempted to lower empty (). All expressions must have a value.",
[mt.pos()],
)),
MacTok::Lambda(arg, body) => {
let MacTok::Name(n) = arg.tok() else {
return on_err(mk_errv(
is("Lowering lambda with complex param").await,
format!("In code after macros, lambda params can only be names, not {}", fmt(arg).await),
[arg.pos()],
));
};
if body.items.is_empty() {
return on_err(mk_errv(
is("Lowering lambda with empty body").await,
"Lambdas without a body are invalid, all expressions in Orchid must have a value",
[mt.pos()],
));
}
let body = lower_seq(body, args.push(n.clone())).await;
GExprKind::Lambda(Box::new(body)).at(mt.pos())
},
MacTok::S(Paren::Round, body) => lower_seq(body, args).await,
MacTok::Name(n) => match args.iter().find_position(|b| *b == n) {
Some((i, _)) => GExprKind::Arg(i.try_into().unwrap()).at(mt.pos()),
None => n.clone().to_gen().await,
},
MacTok::Value(expr) => expr.clone().to_gen().await,
}
}
pub async fn lower_seq(mtv: &MacTreeSeq, args: Substack<'_, Sym>) -> GExpr {
let mut exprs = stream(async |mut h| {
for mt in mtv.items.iter() {
h.emit(lower(mt, args.clone()).await).await
}
})
.collect::<VecDeque<_>>()
.boxed_local()
.await;
let first = exprs.pop_front().expect("We checked first that it isn't empty");
stream::iter(exprs).fold(first, async |f, x| call(f, x).await).await
}

View File

@@ -1,8 +1,9 @@
use orchid_base::sym;
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::tree::{GenMember, fun, prefix};
use orchid_extension::{TAtom, exec};
use substack::Substack;
use crate::macros::lower::lower;
use crate::macros::mactree::MacTree;
use crate::macros::resolve::resolve;
use crate::macros::utils::{build_macro, mactree, mactreev};
@@ -10,74 +11,111 @@ use crate::{HomoTpl, UntypedTuple};
pub async fn gen_macro_lib() -> Vec<GenMember> {
prefix("macros", [
fun(true, "resolve", async |tpl: TAtom<MacTree>| resolve(tpl.own().await).await),
fun(true, "resolve", async |tpl: TAtom<MacTree>| {
exec(async move |mut h| new_atom(resolve(&mut h, tpl.own().await).await)).await
}),
fun(true, "lower", async |mt: TAtom<MacTree>| lower(&mt.own().await, Substack::Bottom).await),
prefix("common", [
build_macro(None, ["..", "_", "="]).finish(),
build_macro(Some(1), ["+"])
.rule(mactreev!("...$" lhs 1 macros::common::+ "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::add::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(30), ["+"])
.rule(
mactreev!("...$" lhs 1 "macros::common::+" "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::add::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(Some(1), ["-"])
.rule(mactreev!("...$" lhs 1 macros::common::- "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::sub::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(30), ["-"])
.rule(
mactreev!("...$" lhs 1 "macros::common::-" "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::sub::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(Some(2), ["*"])
.rule(mactreev!("...$" lhs 1 macros::common::* "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::mul::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(20), ["*"])
.rule(
mactreev!("...$" lhs 1 "macros::common::*" "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::mul::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(Some(2), ["/"])
.rule(mactreev!("...$" lhs 1 macros::common::/ "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::div::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(20), ["/"])
.rule(
mactreev!("...$" lhs 1 "macros::common::/" "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::div::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(Some(2), ["%"])
.rule(mactreev!("...$" lhs 1 macros::common::% "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::mod::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(20), ["%"])
.rule(
mactreev!("...$" lhs 1 "macros::common::%" "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::mod::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(Some(3), ["."])
.rule(mactreev!("...$" lhs 1 macros::common::. "...$" rhs 0), [async |[lhs, rhs]| {
call(sym!(std::ops::get::resolve), (resolve(lhs).await, resolve(rhs).await)).await
}])
build_macro(Some(10), ["."])
.rule(
mactreev!("...$" lhs 1 "macros::common::." "...$" rhs 0),
async |mut cx, [lhs, rhs]| {
Ok(mactree!("std::ops::get::resolve"
"push" cx.recur(lhs).await;
"push" cx.recur(rhs).await;))
},
)
.finish(),
build_macro(None, ["comma_list", ","])
.rule(
mactreev!(macros::common::comma_list ( "...$" head 0 macros::common::, "...$" tail 1)),
[async |[head, tail]| {
exec(async |mut h| {
let recur = resolve(mactree!(macros::common::comma_list "push" tail ;)).await;
let mut tail = h.exec::<HomoTpl<TAtom<MacTree>>>(recur).await?;
tail.0.insert(0, h.exec(new_atom(head)).await?);
Ok(tail)
})
.await
}],
mactreev!("macros::common::comma_list" (
"...$" head 0 "macros::common::," "...$" tail 1
)),
async |mut cx, [head, tail]| {
let mut tail: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!("macros::common::comma_list" "push" tail ;)).await?;
tail.0.insert(0, cx.exec(new_atom(head)).await?);
Ok(mactree!("Val" tail;))
},
)
.rule(mactreev!(macros::common::comma_list ( "...$" final_tail 0 )), [async |[tail]| {
HomoTpl(vec![new_atom(tail)])
}])
.rule(mactreev!(macros::common::comma_list()), [async |[]| UntypedTuple(Vec::new())])
.rule(
mactreev!("macros::common::comma_list" ( "...$" final_tail 0 )),
async |_cx, [tail]| Ok(mactree!("Val" HomoTpl(vec![new_atom(tail)]);)),
)
.rule(mactreev!("macros::common::comma_list"()), async |_cx, []| {
Ok(mactree!("Val" UntypedTuple(Vec::new());))
})
.finish(),
build_macro(None, ["semi_list", ";"])
.rule(
mactreev!(macros::common::semi_list ( "...$" head 0 macros::common::; "...$" tail 1)),
[async |[head, tail]| {
exec(async |mut h| {
let recur = resolve(mactree!(macros::common::semi_list "push" tail ;)).await;
let mut tail = h.exec::<HomoTpl<TAtom<MacTree>>>(recur).await?;
tail.0.insert(0, h.exec(new_atom(head)).await?);
Ok(tail)
})
.await
}],
mactreev!("macros::common::semi_list" (
"...$" head 0 "macros::common::;" "...$" tail 1
)),
async |mut cx, [head, tail]| {
let mut tail: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!("macros::common::semi_list" "push" tail ;)).await?;
tail.0.insert(0, cx.exec(new_atom(head)).await?);
Ok(mactree!("Val" tail))
},
)
.rule(mactreev!(macros::common::semi_list ( "...$" final_tail 0 )), [async |[tail]| {
HomoTpl(vec![new_atom(tail)])
}])
.rule(mactreev!(macros::common::semi_list()), [async |[]| UntypedTuple(Vec::new())])
.rule(
mactreev!("macros::common::semi_list" ( "...$" final_tail 0 )),
async |_cx, [tail]| Ok(mactree!("Val" HomoTpl(vec![new_atom(tail)]))),
)
.rule(mactreev!("macros::common::semi_list"()), async |_cx, []| {
Ok(mactree!("Val" UntypedTuple(Vec::new())))
})
.finish(),
]),
])

View File

@@ -8,10 +8,8 @@ use orchid_base::{
Comment, OrcRes, Paren, Parsed, Snippet, Token, clone, expect_end, expect_tok, is, line_items,
mk_errv, report, sym, token_errv, try_pop_no_fluff, with_reporter,
};
use orchid_extension::TAtom;
use orchid_extension::{ToExpr, TryFromExpr};
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser, TAtom, ToExpr, TryFromExpr};
use crate::macros::let_line::{dealias_mac_v, parse_tokv};
use crate::macros::macro_value::{Macro, MacroData, Rule};
@@ -130,7 +128,7 @@ impl Parser for MacroLine {
let macro_input =
MacTok::S(Paren::Round, with_reporter(dealias_mac_v(&body_mactree, &ctx)).await?)
.at(body_sr.pos());
Ok(call(sym!(macros::resolve), new_atom(macro_input)))
Ok(call(sym!(macros::lower), call(sym!(macros::resolve), new_atom(macro_input))))
}))
}
let mac_cell = Rc::new(OnceCell::new());

View File

@@ -48,9 +48,7 @@ pub struct MacroSystemInst {
}
impl System for MacroSystemInst {
type Ctor = MacroSystem;
async fn request<'a>(&self, _: Box<dyn ReqHandle<'a> + 'a>, req: Never) -> Receipt<'a> {
match req {}
}
async fn request(&self, _: Box<dyn ReqHandle>, req: Never) -> Receipt { match req {} }
async fn prelude(&self) -> Vec<Sym> {
vec![
sym!(macros::common::+),

View File

@@ -2,10 +2,8 @@ use std::borrow::Cow;
use std::rc::Rc;
use never::Never;
use orchid_base::IStr;
use orchid_base::Sym;
use orchid_extension::Atomic;
use orchid_extension::{OwnedAtom, OwnedVariant};
use orchid_base::{IStr, Sym};
use orchid_extension::{Atomic, OwnedAtom, OwnedVariant};
use crate::macros::mactree::MacTreeSeq;
use crate::macros::rule::matcher::Matcher;

View File

@@ -9,9 +9,7 @@ use orchid_api_derive::Coding;
use orchid_base::{
FmtCtx, FmtUnit, Format, IStr, OrcErrv, Paren, Pos, Sym, Variants, indent, tl_cache,
};
use orchid_extension::Atomic;
use orchid_extension::Expr;
use orchid_extension::{OwnedAtom, OwnedVariant};
use orchid_extension::{Atomic, Expr, OwnedAtom, OwnedVariant};
fn union_rc_sets(seq: impl IntoIterator<Item = Rc<HashSet<Sym>>>) -> Rc<HashSet<Sym>> {
let mut acc = Rc::<HashSet<Sym>>::default();
@@ -116,6 +114,7 @@ impl MacTree {
MacTok::Lambda(arg, body) =>
MacTok::Lambda(ro(changed, |changed| arg.map(changed, map)), body.map(changed, map)),
MacTok::Name(_) | MacTok::Value(_) => return self.clone(),
MacTok::Resolved(inner) => return inner.map(changed, map),
MacTok::Slot | MacTok::Ph(_) | MacTok::Bottom(_) => return self.clone(),
MacTok::S(p, body) => MacTok::S(*p, body.map(changed, map)),
},
@@ -154,11 +153,23 @@ pub enum MacTok {
/// never accessed as variables by usercode
Ph(Ph),
Bottom(OrcErrv),
/// This node type cannot be manually constructed in Orchid and should not be
/// manually constructed in Rust.
///
/// - can only match placeholders
/// - skipped by Resolve and glossary
/// - transparent for all other purposes
///
/// It is used to wrap macro return values to prevent double-resolve
///
/// TODO: consider including optional metadata about the match
Resolved(MacTree),
}
impl MacTok {
pub fn build_glossary(&self) -> Rc<HashSet<Sym>> {
match self {
MacTok::Bottom(_) | MacTok::Ph(_) | MacTok::Slot | MacTok::Value(_) => Rc::default(),
MacTok::Bottom(_) | MacTok::Ph(_) | MacTok::Slot | MacTok::Value(_) | MacTok::Resolved(_) =>
Rc::default(),
MacTok::Name(sym) => Rc::new(HashSet::from([sym.clone()])),
MacTok::S(_, body) => union_rc_sets(body.items.iter().map(|mt| mt.glossary.clone())),
MacTok::Lambda(arg, body) =>
@@ -172,9 +183,10 @@ impl MacTok {
impl Format for MacTok {
async fn print<'a>(&'a self, c: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
match self {
Self::Value(v) => v.print(c).await,
Self::Value(v) => tl_cache!(Rc<Variants>: Rc::new(Variants::default().bounded("$({0b})")))
.units([v.print(c).await]),
Self::Lambda(arg, b) => tl_cache!(Rc<Variants>: Rc::new(Variants::default()
.unbounded("\\{0} {1l}")
// .unbounded("\\{0} {1l}")
.bounded("(\\{0} {1b})")))
.units([arg.print(c).boxed_local().await, b.print(c).await]),
Self::Name(n) => format!("{n}").into(),
@@ -186,6 +198,7 @@ impl Format for MacTok {
}
.units([body.print(c).await]),
Self::Slot => "$SLOT".into(),
Self::Resolved(res) => res.print(c).boxed_local().await,
Self::Bottom(err) => match err.one() {
Some(err) => format!("Bottom({err}) ").into(),
None => format!("Botttom(\n{}) ", indent(&err.to_string())).into(),

View File

@@ -2,12 +2,10 @@ use std::ops::RangeInclusive;
use futures::FutureExt;
use itertools::chain;
use orchid_base::Paren;
use orchid_base::{OrcRes, PARENS, is, mk_errv};
use orchid_base::{OrcRes, PARENS, Paren, is, mk_errv};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::{LexContext, Lexer, err_not_applicable};
use orchid_extension::p_tree2gen;
use orchid_extension::tree::{GenTok, GenTokTree, x_tok};
use orchid_extension::{LexContext, Lexer, err_not_applicable, p_tree2gen};
use crate::macros::instantiate_tpl::InstantiateTplCall;
use crate::macros::let_line::parse_tok;

View File

@@ -1,160 +1,126 @@
use std::borrow::Cow;
use async_fn_stream::stream;
use futures::future::join_all;
use futures::{Stream, StreamExt, stream};
use futures::{StreamExt, stream};
use never::Never;
use orchid_api_derive::Coding;
use orchid_base::{OrcRes, Sym, fmt, is, mk_errv, sym};
use orchid_extension::ToExpr;
use orchid_extension::{ExecHandle, exec};
use orchid_extension::{Expr, ExprHandle};
use orchid_extension::gen_expr::{GExpr, arg, bot, call, call_v, lam, new_atom};
use orchid_base::{OrcRes, Sym, fmt, is, mk_errv};
use orchid_extension::gen_expr::{bot, call, call_v, lam, new_atom};
use orchid_extension::tree::{GenMember, fun, prefix};
use orchid_extension::{Atomic, OwnedAtom, OwnedVariant, TAtom};
use orchid_extension::{Atomic, ExecHandle, Expr, OwnedAtom, OwnedVariant, TAtom, ToExpr, exec};
use crate::macros::resolve::resolve;
use crate::macros::utils::{build_macro, mactree, mactreev};
use crate::std::reflection::sym_atom::SymAtom;
use crate::{HomoTpl, MacTok, MacTree, OrcOpt, Tpl, UntypedTuple, api};
use crate::{HomoTpl, MacTok, MacTree, OrcOpt, Tpl, UntypedTuple};
#[derive(Clone, Coding)]
pub struct MatcherData {
keys: Vec<api::TStrv>,
matcher: api::ExprTicket,
}
impl MatcherData {
async fn matcher(&self) -> Expr { Expr::from_handle(ExprHandle::from_ticket(self.matcher).await) }
pub async fn run_matcher(
&self,
h: &mut ExecHandle<'_>,
val: impl ToExpr,
) -> OrcRes<OrcOpt<HomoTpl<Expr>>> {
h.exec::<OrcOpt<HomoTpl<Expr>>>(call(self.matcher().await, val)).await
}
pub fn keys(&self) -> impl Stream<Item = Sym> {
stream(async |mut h| {
for tk in &self.keys {
h.emit(Sym::from_api(*tk).await).await
}
})
}
}
#[derive(Clone)]
pub struct MatcherAtom {
/// The names that subresults may be bound to
pub(super) keys: Vec<Sym>,
/// Takes the value-to-be-matched, returns an `option (tuple T1..TN)` of the
/// subresults to be bound to the names returned by [Self::keys]
pub(super) matcher: Expr,
pub(super) matcher: MacTree,
}
impl Atomic for MatcherAtom {
type Data = MatcherData;
type Data = ();
type Variant = OwnedVariant;
}
impl OwnedAtom for MatcherAtom {
type Refs = Never;
async fn val(&self) -> std::borrow::Cow<'_, Self::Data> {
Cow::Owned(MatcherData {
keys: self.keys.iter().map(|t| t.to_api()).collect(),
matcher: self.matcher.handle().ticket(),
})
}
async fn val(&self) -> std::borrow::Cow<'_, Self::Data> { Cow::Owned(()) }
}
pub async fn match_one(
h: &mut ExecHandle<'_>,
mat: impl ToExpr,
value: impl ToExpr,
) -> OrcRes<OrcOpt<HomoTpl<Expr>>> {
h.exec::<OrcOpt<HomoTpl<Expr>>>(call(mat, value)).await
}
pub async fn gen_match_macro_lib() -> Vec<GenMember> {
prefix("pattern", [
fun(
true,
"match_one",
async |mat: TAtom<MatcherAtom>, value: Expr, then: Expr, default: Expr| {
exec(async move |mut h| match mat.run_matcher(&mut h, value).await? {
OrcOpt(Some(values)) => Ok(call_v(then, values.0).await),
OrcOpt(None) => Ok(default.to_gen().await),
})
.await
},
),
fun(true, "matcher", async |names: HomoTpl<TAtom<SymAtom>>, matcher: Expr| {
fun(true, "match_one", async |mat: Expr, value: Expr, then: Expr, default: Expr| {
exec(async move |mut h| match match_one(&mut h, mat, value).await? {
OrcOpt(Some(values)) => Ok(call_v(then, values.0).await),
OrcOpt(None) => Ok(default.to_gen().await),
})
.await
}),
fun(true, "matcher", async |names: HomoTpl<TAtom<SymAtom>>, matcher: TAtom<MacTree>| {
new_atom(MatcherAtom {
keys: join_all(names.0.iter().map(async |atm| Sym::from_api(atm.0).await)).await,
matcher,
matcher: matcher.own().await,
})
}),
build_macro(None, ["match", "match_rule", "_row", "=>"])
.rule(mactreev!("pattern::match" "...$" value 0 { "..$" rules 0 }), [
async |[value, rules]| {
exec(async move |mut h| {
let rule_lines = h
.exec::<HomoTpl<TAtom<MacTree>>>(call(
sym!(macros::resolve),
new_atom(mactree!(macros::common::semi_list "push" rules.clone();)),
))
.await?;
let mut rule_atoms = Vec::<(TAtom<MatcherAtom>, Expr)>::new();
for line_mac in rule_lines.0.iter() {
let Tpl((matcher, body)) = h
.exec(call(
sym!(macros::resolve),
new_atom(mactree!(pattern::_row "push" line_mac.own().await ;)),
))
.await?;
rule_atoms.push((matcher, body));
}
let base_case = lam::<0>(bot(mk_errv(
.rule(
mactreev!("pattern::match" "...$" value 0 { "..$" rules 0 }),
async |mut cx, [value, rules]| {
let rule_lines: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!("macros::common::semi_list" "push" rules.clone())).await?;
let mut rule_atoms = Vec::<(TAtom<MatcherAtom>, TAtom<MacTree>)>::new();
for line_mac in rule_lines.0.iter() {
let Tpl((matcher, body)) =
cx.recur_call(mactree!("pattern::_row" "push" line_mac.own().await)).await?;
rule_atoms.push((matcher, body));
}
let base_case = lam(async |_| {
bot(mk_errv(
is("No branches match").await,
"None of the patterns matches this value",
[rules.pos()],
)))
.await;
let match_expr = stream::iter(rule_atoms.into_iter().rev())
.fold(base_case, async |tail, (mat, body)| {
lam::<0>(call(sym!(pattern::match_one), (mat, arg(0), body, call(tail, arg(0)))))
.await
})
.await;
Ok(call(match_expr, resolve(value).await))
))
})
.await
},
])
.rule(mactreev!(pattern::match_rule (( "...$" pattern 0 ))), [async |[pattern]| {
resolve(mactree!(pattern::match_rule "push" pattern; )).await
}])
.rule(mactreev!(pattern::match_rule ( macros::common::_ )), [async |[]| {
Ok(new_atom(MatcherAtom {
keys: Vec::new(),
matcher: lam::<0>(OrcOpt(Some(Tpl(())))).await.create().await,
}))
}])
.rule(mactreev!(pattern::_row ( "...$" pattern 0 pattern::=> "...$" value 1 )), [
async |[pattern, mut value]| {
exec(async move |mut h| -> OrcRes<Tpl<(TAtom<MatcherAtom>, GExpr)>> {
let Ok(pat) = h
.exec::<TAtom<MatcherAtom>>(call(
sym!(macros::resolve),
new_atom(mactree!(pattern::match_rule "push" pattern.clone();)),
.create()
.await;
let match_chain = stream::iter(rule_atoms.into_iter().rev())
.fold(mactree!("Val" base_case;), async |tail, (mat, body)| {
mactree!("l" "pattern::x" (
"pattern::match_one"
"Val" mat.ex();
"pattern::x"
"push" body.own().await;
("push" tail;
"pattern::x")
))
.await
else {
return Err(mk_errv(
is("Invalid pattern").await,
format!("Could not parse {} as a match pattern", fmt(&pattern).await),
[pattern.pos()],
));
};
value = (pat.keys())
.fold(value, async |value, name| mactree!("l_" name; ( "push" value ; )))
.await;
Ok(Tpl((pat, resolve(value).await)))
})
.await
})
.await;
Ok(mactree!("push" match_chain; "push" value))
},
])
)
.rule(mactreev!("pattern::match_rule" (( "...$" pattern 0 ))), async |mut cx, [pattern]| {
Ok(cx.recur(mactree!("pattern::match_rule" "push" pattern)).await)
})
.rule(mactreev!("pattern::match_rule"("macros::common::_")), async |_cx, []| {
let matcher = new_atom(MatcherAtom {
keys: Vec::new(),
matcher: mactree!("l" "pattern::unused" ("Val" OrcOpt(Some(Tpl(()))))),
});
Ok(mactree!("Val" matcher))
})
.rule(
mactreev!("pattern::_row" ( "...$" pattern 0 "pattern::=>" "...$" value 1 )),
async |mut cx, [pattern, mut value]| {
let Ok(pat): OrcRes<TAtom<MatcherAtom>> =
cx.recur_call(mactree!("pattern::match_rule" "push" pattern.clone())).await
else {
return Err(mk_errv(
is("Invalid pattern").await,
format!("Could not parse {} as a match pattern", fmt(&pattern).await),
[pattern.pos()],
));
};
value = stream::iter(pat.own().await.keys.iter().cloned())
.fold(value, async |value, name| mactree!("l_" name; ( "push" value )))
.await;
Ok(mactree!("Val" Tpl((pat, new_atom(cx.recur(value).await)))))
},
)
.finish(),
fun(true, "ref_body", async |val| OrcOpt(Some(UntypedTuple(vec![val])))),
build_macro(None, ["ref"])
.rule(mactreev!(pattern::match_rule(pattern::ref "$" name)), [async |[name]| {
.rule(mactreev!("pattern::match_rule"("pattern::ref" "$" name)), async |_cx, [name]| {
let MacTok::Name(name) = name.tok() else {
return Err(mk_errv(
is("pattern 'ref' requires a name to bind to").await,
@@ -166,11 +132,12 @@ pub async fn gen_match_macro_lib() -> Vec<GenMember> {
[name.pos()],
));
};
Ok(new_atom(MatcherAtom {
let atom = new_atom(MatcherAtom {
keys: vec![name.clone()],
matcher: sym!(pattern::ref_body).to_expr().await,
}))
}])
matcher: mactree!("pattern::ref_body"),
});
Ok(mactree!("Val" atom))
})
.finish(),
])
}

View File

@@ -1,5 +1,6 @@
mod instantiate_tpl;
mod let_line;
pub mod lower;
mod macro_lib;
mod macro_line;
pub mod macro_system;

View File

@@ -1,10 +1,8 @@
use orchid_api_derive::Coding;
use orchid_base::{FmtUnit, OrcRes, es, is, mk_errv, name_char, name_start};
use orchid_extension::Atomic;
use orchid_extension::gen_expr::new_atom;
use orchid_extension::{LexContext, Lexer, err_not_applicable};
use orchid_extension::tree::{GenTokTree, x_tok};
use orchid_extension::{ThinAtom, ThinVariant};
use orchid_extension::{Atomic, LexContext, Lexer, ThinAtom, ThinVariant, err_not_applicable};
use crate::macros::mactree::{Ph, PhKind};

View File

@@ -1,71 +1,60 @@
use std::collections::VecDeque;
use std::ops::{Add, Range};
use async_fn_stream::stream;
use futures::{FutureExt, StreamExt, stream};
use futures::FutureExt;
use hashbrown::{HashMap, HashSet};
use itertools::Itertools;
use orchid_base::{NameLike, Paren, Pos, Sym, VPath, fmt, is, log, mk_errv};
use orchid_extension::gen_expr::{GExpr, arg, bot, call, call_v, dyn_lambda, new_atom};
use orchid_extension::{ReflMemKind, TAtom, ToExpr, exec, refl};
use orchid_base::{NameLike, Paren, Pos, VPath, fmt, is, log, mk_errv, report};
use orchid_extension::gen_expr::{call_v, new_atom};
use orchid_extension::{ExecHandle, ReflMemKind, TAtom, refl};
use subslice_offset::SubsliceOffset;
use substack::Substack;
use crate::macros::macro_value::{Macro, Rule};
use crate::macros::mactree::MacTreeSeq;
use crate::macros::rule::state::{MatchState, StateEntry};
use crate::{MacTok, MacTree};
pub async fn resolve(val: MacTree) -> GExpr {
exec(async move |mut h| {
writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await).await;
let root = refl();
let mut macros = HashMap::new();
for n in val.glossary() {
let (foot, body) = n.split_last_seg();
let new_name = VPath::new(body.iter().cloned())
.name_with_suffix(is(&format!("__macro__{foot}")).await)
.to_sym()
.await;
if let Ok(ReflMemKind::Const) = root.get_by_path(&new_name).await.map(|m| m.kind()) {
let Ok(mac) = h.exec::<TAtom<Macro>>(new_name).await else { continue };
let mac = mac.own().await;
macros.entry(mac.0.canonical_name.clone()).or_insert(mac);
pub async fn resolve(h: &mut ExecHandle<'_>, val: MacTree) -> MacTree {
writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await).await;
let root = refl();
let mut macros = HashMap::new();
for n in val.glossary() {
let (foot, body) = n.split_last_seg();
let new_name = VPath::new(body.iter().cloned())
.name_with_suffix(is(&format!("__macro__{foot}")).await)
.to_sym()
.await;
if let Ok(ReflMemKind::Const) = root.get_by_path(&new_name).await.map(|m| m.kind()) {
let Ok(mac) = h.exec::<TAtom<Macro>>(new_name).await else { continue };
let mac = mac.own().await;
macros.entry(mac.0.canonical_name.clone()).or_insert(mac);
}
}
let mut exclusive = Vec::new();
let mut prios = Vec::<u64>::new();
let mut priod = Vec::<FilteredMacroRecord>::new();
for (_, mac) in macros.iter() {
let mut record = FilteredMacroRecord { mac, rules: Vec::new() };
for (rule_i, rule) in mac.0.rules.iter().enumerate() {
if rule.pattern.glossary.is_subset(val.glossary()) {
record.rules.push(rule_i);
}
}
let mut exclusive = Vec::new();
let mut prios = Vec::<u64>::new();
let mut priod = Vec::<FilteredMacroRecord>::new();
for (_, mac) in macros.iter() {
let mut record = FilteredMacroRecord { mac, rules: Vec::new() };
for (rule_i, rule) in mac.0.rules.iter().enumerate() {
if rule.pattern.glossary.is_subset(val.glossary()) {
record.rules.push(rule_i);
}
}
if !record.rules.is_empty() {
match mac.0.prio {
None => exclusive.push(record),
Some(prio) => {
let i = prios.partition_point(|p| *p > prio);
prios.insert(i, prio);
priod.insert(i, record);
},
}
if !record.rules.is_empty() {
match mac.0.prio {
None => exclusive.push(record),
Some(prio) => {
let i = prios.partition_point(|p| *p > prio);
prios.insert(i, prio);
priod.insert(i, record);
},
}
}
let mut rctx = ResolveCtx { exclusive, priod };
let gex = resolve_one(&mut rctx, Substack::Bottom, &val).await;
writeln!(
log("debug"),
"Macro-resolution over {}\nreturned {}",
fmt(&val).await,
fmt(&gex).await
)
}
let mut rctx = ResolveCtx { exclusive, priod, h: &mut *h };
let gex = resolve_one(&mut rctx, &val).await.unwrap_or(val.clone());
writeln!(log("debug"), "Macro-resolution over {} yielded {}", fmt(&val).await, fmt(&gex).await)
.await;
gex
})
.await
gex
}
/// Rules belonging to one macro that passed a particular filter
@@ -75,45 +64,33 @@ pub struct FilteredMacroRecord<'a> {
rules: Vec<usize>,
}
struct ResolveCtx<'a> {
struct ResolveCtx<'a, 'b> {
/// If these overlap, that's a compile-time error
pub exclusive: Vec<FilteredMacroRecord<'a>>,
/// If these overlap, the priorities decide the order. In case of a tie, the
/// order is unspecified
pub priod: Vec<FilteredMacroRecord<'a>>,
pub h: &'a mut ExecHandle<'b>,
}
async fn resolve_one(
ctx: &mut ResolveCtx<'_>,
arg_stk: Substack<'_, Sym>,
value: &MacTree,
) -> GExpr {
async fn resolve_one(ctx: &mut ResolveCtx<'_, '_>, value: &MacTree) -> Option<MacTree> {
match value.tok() {
MacTok::Ph(_) | MacTok::Slot => panic!("Forbidden element in value mactree"),
MacTok::Bottom(err) => bot(err.clone()),
MacTok::Value(v) => v.clone().to_gen().await,
MacTok::Name(n) => match arg_stk.iter().position(|arg| arg == n) {
Some(de_bruijn) => arg((arg_stk.len() - 1 - de_bruijn).try_into().unwrap()),
None => n.clone().to_gen().await,
},
MacTok::Bottom(_) | MacTok::Value(_) | MacTok::Name(_) | MacTok::Resolved(_) => None,
MacTok::Lambda(arg, body) => {
let MacTok::Name(name) = &*arg.tok else {
return bot(mk_errv(
is("Syntax error after macros").await,
"This token ends up as a binding, consider replacing it with a name",
[arg.pos()],
));
};
let arg_pos = arg_stk.len() as u64;
let arg_stk = arg_stk.push(name.clone());
dyn_lambda(arg_pos, resolve_seq(ctx, arg_stk, body.clone(), value.pos()).await).await
let new_arg = resolve_one(ctx, arg).boxed_local().await;
let new_body = resolve_seq(ctx, body, value.pos()).boxed_local().await;
if new_arg.is_none() && new_body.is_none() {
return None;
}
let tok = MacTok::Lambda(
new_arg.unwrap_or_else(|| arg.clone()),
new_body.unwrap_or_else(|| body.clone()),
);
Some(tok.at(value.pos()))
},
MacTok::S(Paren::Round, body) => resolve_seq(ctx, arg_stk, body.clone(), value.pos()).await,
MacTok::S(..) => bot(mk_errv(
is("Leftover [] or {} not matched by macro").await,
format!("{} was not matched by any macro", fmt(value).await),
[value.pos()],
)),
MacTok::S(pty, body) => (resolve_seq(ctx, body, value.pos()).boxed_local().await)
.map(|body| MacTok::S(*pty, body).at(value.pos())),
}
}
@@ -132,18 +109,12 @@ fn subsection<T>(
}
async fn resolve_seq(
ctx: &mut ResolveCtx<'_>,
arg_stk: Substack<'_, Sym>,
val: MacTreeSeq,
ctx: &mut ResolveCtx<'_, '_>,
val: &MacTreeSeq,
fallback_pos: Pos,
) -> GExpr {
) -> Option<MacTreeSeq> {
if val.items.is_empty() {
return bot(mk_errv(
is("Empty sequence").await,
"() or (\\arg ) left after macro execution. \
This is usually caused by an incomplete call to a macro with bad error detection",
[fallback_pos],
));
return None;
}
// A sorted collection of overlapping but non-nested matches to exclusive
// macros
@@ -196,6 +167,7 @@ async fn resolve_seq(
x_matches.splice(lt_start..lt_start + lt_range.len(), [new_r]);
}
}
let mut any_match = !x_matches.is_empty();
// apply exclusive matches
if !x_matches.is_empty() {
// ranges of indices into x_matches which setwise conflict with each other.
@@ -226,18 +198,19 @@ async fn resolve_seq(
})
.reduce(|l, r| l + r);
if let Some(error) = error {
return bot(error);
report(error.clone());
return Some(MacTreeSeq::new([MacTok::Bottom(error).at(fallback_pos)]));
}
// no conflicts, apply all exclusive matches
for (range, mac, rule, state) in x_matches.into_iter().rev() {
// backwards so that the non-overlapping ranges remain valid
let pos = (state.names().flat_map(|r| r.1).cloned().reduce(Pos::add))
.expect("All macro rules must contain at least one locally defined name");
let subex = mk_body_call(mac, rule, &state, pos.clone()).await.to_expr().await;
new_val.splice(range, [MacTok::Value(subex).at(pos)]);
let subex = call_body(ctx.h, mac, rule, &state, pos.clone()).await;
new_val.splice(range, [subex]);
}
};
// Does this glossary refresh actually pay off?
// TODO: Does this glossary refresh actually pay off?
let top_glossary = (new_val.iter())
.flat_map(|t| if let MacTok::Name(t) = t.tok() { Some(t.clone()) } else { None })
.collect::<HashSet<_>>();
@@ -248,29 +221,30 @@ async fn resolve_seq(
continue;
}
let Some((pre, state, suf)) = rule.matcher.apply(&new_val, &|_| true).await else { continue };
any_match = true;
let range = pre.len()..new_val.len() - suf.len();
let pos = (state.names().flat_map(|pair| pair.1).cloned().reduce(Pos::add))
.expect("All macro rules must contain at least one locally defined name");
let subex = mk_body_call(mac, rule, &state, pos.clone()).await.to_expr().await;
let subex = call_body(ctx.h, mac, rule, &state, pos.clone()).await;
std::mem::drop(state);
new_val.splice(range, [MacTok::Value(subex).at(pos)]);
new_val.splice(range, [subex]);
}
}
let mut exprs = stream(async |mut h| {
for mt in new_val {
h.emit(resolve_one(ctx, arg_stk.clone(), &mt).await).await
}
})
.collect::<VecDeque<_>>()
.boxed_local()
.await;
let first = exprs.pop_front().expect(
"We checked first that it isn't empty, and named macros get replaced with their results",
);
stream::iter(exprs).fold(first, async |f, x| call(f, x).await).await
for item in new_val.iter_mut() {
let Some(new) = resolve_one(ctx, item).await else { continue };
*item = new;
any_match = true;
}
any_match.then_some(MacTreeSeq::new(new_val))
}
async fn mk_body_call(mac: &Macro, rule: &Rule, state: &MatchState<'_>, pos: Pos) -> GExpr {
async fn call_body(
h: &mut ExecHandle<'_>,
mac: &Macro,
rule: &Rule,
state: &MatchState<'_>,
pos: Pos,
) -> MacTree {
let mut call_args = vec![];
for name in rule.ph_names.iter() {
call_args.push(match state.get(name).expect("Missing state entry for placeholder") {
@@ -279,5 +253,9 @@ async fn mk_body_call(mac: &Macro, rule: &Rule, state: &MatchState<'_>, pos: Pos
new_atom(MacTok::S(Paren::Round, MacTreeSeq::new(vec.iter().cloned())).at(Pos::None)),
});
}
call_v(mac.0.module.suffix([rule.body.clone()]).await, call_args).await.at(pos.clone())
let f_name = mac.0.module.suffix([rule.body.clone()]).await;
match h.exec::<TAtom<MacTree>>(call_v(f_name, call_args)).await {
Err(e) => MacTok::Bottom(e).at(pos),
Ok(mt) => MacTok::Resolved(mt.own().await).at(mt.pos()),
}
}

View File

@@ -127,6 +127,7 @@ async fn mk_scalar(pattern: &MacTree) -> OrcRes<ScalMatcher> {
[pattern.pos()],
));
},
MacTok::Resolved(_) => panic!("Can only appear in macro output, not in matcher"),
MacTok::Value(_) | MacTok::Slot => panic!("Only used for templating"),
MacTok::Bottom(errv) => return Err(errv.clone()),
})

View File

@@ -1,8 +1,7 @@
use std::fmt;
use std::rc::Rc;
use orchid_base::Sym;
use orchid_base::{OrcRes, is};
use orchid_base::{OrcRes, Sym, is};
use super::any_match::any_match;
use super::build::mk_any;

View File

@@ -3,8 +3,7 @@
use std::fmt;
use itertools::Itertools;
use orchid_base::{PARENS, Paren};
use orchid_base::{IStr, Side, Sym};
use orchid_base::{IStr, PARENS, Paren, Side, Sym};
pub enum ScalMatcher {
Name(Sym),

View File

@@ -2,9 +2,7 @@
use std::any::Any;
use hashbrown::HashMap;
use orchid_base::Pos;
use orchid_base::Sym;
use orchid_base::{IStr, join_maps, match_mapping};
use orchid_base::{IStr, Pos, Sym, join_maps, match_mapping};
use crate::macros::MacTree;

View File

@@ -1,12 +1,14 @@
use orchid_extension::tree::{GenMember, prefix};
use crate::macros::resolve::resolve;
use crate::macros::utils::{build_macro, mactree, mactreev};
pub async fn gen_functional_macro_lib() -> Vec<GenMember> {
prefix("std::fn", [build_macro(Some(4), ["|>"])
.rule(mactreev!("...$" lhs 0 "std::fn::|>" "$" fun "...$" rhs 0), [async |[lhs, fun, rhs]| {
resolve(mactree!(("push" fun ; "push" lhs ;) "pushv" rhs ;)).await
}])
.rule(
mactreev!("...$" lhs 0 "std::fn::|>" "$" fun "...$" rhs 0),
async |mut cx, [lhs, fun, rhs]| {
Ok(cx.recur(mactree!("push" fun ; "push" lhs ; "pushv" rhs)).await)
},
)
.finish()])
}

View File

@@ -1,20 +1,17 @@
use futures::StreamExt;
use orchid_base::sym;
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::tree::{GenMember, fun, prefix};
use orchid_extension::{Expr, TAtom, ToExpr, exec};
use orchid_extension::{Expr, TAtom, exec};
use crate::macros::match_macros::MatcherAtom;
use crate::macros::resolve::resolve;
use crate::macros::match_macros::{MatcherAtom, match_one};
use crate::macros::utils::{build_macro, mactree, mactreev};
use crate::{OrcOpt, Tpl};
pub async fn gen_option_macro_lib() -> Vec<GenMember> {
prefix("std::option", [
fun(false, "is_some_body", |sub: TAtom<MatcherAtom>, val: OrcOpt<Expr>| {
fun(false, "is_some_body", |sub: Expr, val: OrcOpt<Expr>| {
exec(async move |mut h| {
let Some(sub_val) = val.0 else { return Ok(OrcOpt(None)) };
sub.run_matcher(&mut h, sub_val).await
match_one(&mut h, sub, sub_val).await
})
}),
fun(
@@ -25,25 +22,25 @@ pub async fn gen_option_macro_lib() -> Vec<GenMember> {
},
),
build_macro(None, ["some", "none"])
.rule(mactreev!(pattern::match_rule ( std::option::some "...$" sub_pattern 0)), [
|[sub]: [_; _]| {
exec(async move |mut h| {
let sub = h
.exec::<TAtom<MatcherAtom>>(resolve(mactree!(pattern::match_rule "push" sub;)).await)
.await?;
Ok(new_atom(MatcherAtom {
keys: sub.keys().collect().await,
matcher: call(sym!(std::option::is_some_body), sub).await.create().await,
}))
})
.rule(
mactreev!("pattern::match_rule" ( "std::option::some" "...$" sub_pattern 0)),
async |mut cx, [sub]| {
let sub: TAtom<MatcherAtom> =
cx.recur_call(mactree!("pattern::match_rule" "push" sub;)).await?;
let sub = sub.own().await;
let matcher = new_atom(MatcherAtom {
keys: sub.keys,
matcher: mactree!("std::option::is_some_body" "push" sub.matcher),
});
Ok(mactree!("Val" matcher))
},
])
.rule(mactreev!(pattern::match_rule(std::option::none)), [async |[]: [_; _]| {
new_atom(MatcherAtom {
)
.rule(mactreev!("pattern::match_rule"("std::option::none")), async |_cx, []| {
Ok(mactree!("Val" new_atom(MatcherAtom {
keys: vec![],
matcher: sym!(std::option::is_none_body).to_expr().await,
})
}])
matcher: mactree!("std::option::is_none_body"),
})))
})
.finish(),
])
}

View File

@@ -1,41 +1,31 @@
use orchid_base::sym;
use orchid_extension::TAtom;
use orchid_extension::ToExpr;
use orchid_extension::exec;
use orchid_extension::Expr;
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::tree::{GenMember, prefix};
use crate::macros::resolve::resolve;
use crate::macros::utils::{build_macro, mactree, mactreev};
use crate::std::string::str_atom::IntStrAtom;
use crate::{HomoTpl, MacTree, Tpl};
pub async fn gen_record_macro_lib() -> Vec<GenMember> {
prefix("std::record", [build_macro(None, ["r", "_row"])
.rule(mactreev!(std::record::r[ "...$" elements 0 ]), [async |[elements]: [_; _]| {
exec(async move |mut h| {
let tup = h
.exec::<HomoTpl<TAtom<MacTree>>>(call(
sym!(macros::resolve),
new_atom(mactree!((macros::common::comma_list "push" elements ;))),
))
.await?;
let mut record = sym!(std::record::empty).to_gen().await;
for item_exprh in tup.0 {
let Tpl((key, value)) = h
.exec::<Tpl<(TAtom<IntStrAtom>, Expr)>>(
resolve(mactree!(std::record::_row "push" item_exprh.own().await ;)).await,
)
.await?;
record = call(sym!(std::record::set), (record, key, value)).await;
}
Ok(record)
})
.await
}])
.rule(mactreev!(std::record::_row ( "$" name "...$" value 1 )), [async |[name, value]| {
Ok(Tpl((resolve(name).await, resolve(value).await)))
}])
.rule(mactreev!("std::record::r"[ "...$" elements 0 ]), async |mut cx, [elements]| {
let tup: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!(("macros::common::comma_list" "push" elements))).await?;
let mut record = mactree!("std::record::empty");
for item_exprh in tup.0 {
let Tpl((key, value)): Tpl<(TAtom<MacTree>, TAtom<MacTree>)> =
cx.recur_call(mactree!("std::record::_row" "push" item_exprh.own().await)).await?;
record = mactree!("std::record::set"
"push" record;
"push" key.own().await;
"push" value.own().await);
}
Ok(record)
})
.rule(
mactreev!("std::record::_row" ( "$" name "...$" value 1 )),
async |mut cx, [name, value]| {
Ok(mactree!("Val" Tpl((new_atom(cx.recur(name).await), new_atom(cx.recur(value).await)))))
},
)
.finish()])
}

View File

@@ -1,102 +1,93 @@
use async_fn_stream::stream;
use futures::{StreamExt, stream};
use orchid_base::{OrcRes, sym};
use orchid_extension::TAtom;
use orchid_extension::ToExpr;
use orchid_extension::exec;
use orchid_extension::Expr;
use orchid_extension::gen_expr::{GExpr, call, new_atom};
use orchid_extension::tree::{GenMember, fun, prefix};
use orchid_extension::{Expr, TAtom, ToExpr, exec};
use crate::macros::match_macros::MatcherAtom;
use crate::macros::utils::{build_macro, mactree, mactreev};
use crate::macros::match_macros::{MatcherAtom, match_one};
use crate::macros::utils::{RuleCtx, build_macro, mactree, mactreev};
use crate::{HomoTpl, MacTree, OrcOpt};
pub async fn gen_tuple_macro_lib() -> Vec<GenMember> {
prefix("std::tuple", [
build_macro(None, ["t"])
.rule(mactreev!(std::tuple::t [ "...$" elements 0 ]), [|[elements]: [_; _]| {
exec(async move |mut h| {
let tup = h
.exec::<HomoTpl<TAtom<MacTree>>>(call(sym!(macros::resolve),
new_atom(mactree!((macros::common::comma_list "push" elements ;))),
))
.await?;
let val = stream::iter(&tup.0[..])
.fold(sym!(std::tuple::empty).to_gen().await, async |head, new| {
call(sym!(std::tuple::cat), (
head,
call(sym!(std::tuple::one), call(
sym!(macros::resolve),
new.clone(),
)),
)).await
})
.await;
Ok(val)
})
}])
.rule(
mactreev!(pattern::match_rule(std::tuple::t[ "...$" elements 0 macros::common::, macros::common::..])),
[async |[elements]: [_; _]| parse_tpl(elements, Some(mactree!(macros::common::_))).await],
)
.rule(
mactreev!(pattern::match_rule(
std::tuple::t[ "...$" elements 1 macros::common::, macros::common::.. "...$" tail 0]
)),
[async |[elements, tail]: [_; _]| parse_tpl(elements, Some(tail)).await],
)
.rule(mactreev!(pattern::match_rule(std::tuple::t[ "...$" elements 0])), [
|[elements]: [_; _]| parse_tpl(elements, None),
])
.finish(),
fun(false, "matcher_body", tuple_matcher_body),
])
build_macro(None, ["t"])
.rule(mactreev!("std::tuple::t" [ "...$" elements 0 ]), async |mut cx, [elements]| {
let tup: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!("macros::common::comma_list" "push" elements)).await?;
let val = stream(async |mut h| {
for item in &tup.0[..] {
h.emit(cx.recur(item.own().await).await).await
}
})
.fold(mactree!("std::tuple::empty"), async |head, new| {
mactree!(
"std::tuple::cat"
"push" head;
("std::tuple::one"
"push" new)
)
})
.await;
Ok(val)
})
.rule(
mactreev!("pattern::match_rule"(
"std::tuple::t"[ "...$" elements 0 "macros::common::," "macros::common::.."]
)),
async |cx, [elements]| parse_tpl(cx, elements, Some(mactree!("macros::common::_"))).await,
)
.rule(
mactreev!("pattern::match_rule"(
"std::tuple::t"[ "...$" elements 1 "macros::common::," "macros::common::.." "...$" tail 0]
)),
async |cx, [elements, tail]| parse_tpl(cx, elements, Some(tail)).await,
)
.rule(
mactreev!("pattern::match_rule"("std::tuple::t"[ "...$" elements 0])),
async |cx, [elements]| parse_tpl(cx, elements, None).await,
)
.finish(),
fun(false, "matcher_body", tuple_matcher_body),
])
}
fn parse_tpl(elements: MacTree, tail_matcher: Option<MacTree>) -> impl Future<Output = GExpr> {
exec(async move |mut h| -> OrcRes<GExpr> {
let tup = h
.exec::<HomoTpl<TAtom<MacTree>>>(call(sym!(macros::resolve), new_atom(
mactree!((macros::common::comma_list "push" elements ;)),
)))
.await?;
let mut subs = Vec::with_capacity(tup.0.len());
for mac_a in &tup.0[..] {
let sub = h
.exec::<TAtom<MatcherAtom>>(call(sym!(macros::resolve), new_atom(
mactree!(pattern::match_rule ("push" mac_a.own().await ;)),
)))
.await?;
subs.push(sub);
}
let tail_matcher = match tail_matcher {
Some(mac) => Some(
h.exec::<TAtom<MatcherAtom>>(call(sym!(macros::resolve), new_atom(
mactree!(pattern::match_rule "push" mac ;),
)))
.await?,
),
None => None,
};
Ok(new_atom(MatcherAtom {
keys: stream::iter(&subs[..])
.flat_map(|t| t.keys())
.chain(stream::iter(&tail_matcher).flat_map(|mat| mat.keys()))
.collect()
.await,
matcher: call(sym!(std::tuple::matcher_body), (
HomoTpl(subs),
OrcOpt(tail_matcher),
))
.to_expr()
.await,
}))
})
async fn parse_tpl(
mut cx: RuleCtx<'_>,
elements: MacTree,
tail_matcher: Option<MacTree>,
) -> OrcRes<MacTree> {
let tup: HomoTpl<TAtom<MacTree>> =
cx.recur_call(mactree!("macros::common::comma_list" "push" elements)).await?;
let mut keys = Vec::new();
let mut sub_matchers = mactree!("std::tuple::empty");
for mac_a in &tup.0[..] {
let sub: TAtom<MatcherAtom> =
cx.recur_call(mactree!("pattern::match_rule" ("push" mac_a.own().await))).await?;
let owned = sub.own().await;
keys.extend(owned.keys);
sub_matchers =
mactree!("std::tuple::cat" "push" sub_matchers; ("std::tuple::one" "push" owned.matcher));
}
let tail_matcher = match tail_matcher {
Some(mac) => {
let atm: TAtom<MatcherAtom> =
cx.recur_call(mactree!("pattern::match_rule" "push" mac)).await?;
let owned = atm.own().await;
keys.extend(owned.keys);
mactree!("std::option::some" "push" owned.matcher)
},
None => mactree!("std::option::none"),
};
Ok(mactree!("Val" new_atom(MatcherAtom {
keys,
matcher: mactree!("std::tuple::matcher_body" "push" sub_matchers; "push" tail_matcher),
})))
}
fn tuple_matcher_body(
children: HomoTpl<TAtom<MatcherAtom>>,
tail: OrcOpt<TAtom<MatcherAtom>>,
children: HomoTpl<Expr>,
tail: OrcOpt<Expr>,
value: HomoTpl<Expr>,
) -> impl Future<Output = GExpr> {
exec(async move |mut h| -> OrcRes<OrcOpt<HomoTpl<Expr>>> {
@@ -105,7 +96,7 @@ fn tuple_matcher_body(
}
let mut binds = Vec::new();
for (sub_mat, sub_val) in children.0.iter().zip(&value.0) {
match sub_mat.run_matcher(&mut h, sub_val.clone()).await? {
match match_one(&mut h, sub_mat.clone(), sub_val.clone()).await? {
OrcOpt(None) => return Ok(OrcOpt(None)),
OrcOpt(Some(subres)) => binds.extend(subres.0),
}
@@ -119,7 +110,7 @@ fn tuple_matcher_body(
call(sym!(std::tuple::cat), (prefix, new.clone())).await
})
.await;
match tail_mat.run_matcher(&mut h, tail_tpl).await? {
match match_one(&mut h, tail_mat, tail_tpl).await? {
OrcOpt(Some(tail_binds)) => binds.extend(tail_binds.0),
OrcOpt(None) => return Ok(OrcOpt(None)),
}

View File

@@ -6,14 +6,18 @@ use futures::StreamExt;
use futures::future::LocalBoxFuture;
use itertools::{Itertools, chain};
use never::Never;
use orchid_base::{NameLike, Sym, VPath, is};
use orchid_extension::ToExpr;
use orchid_base::{NameLike, OrcRes, Sym, VPath, is};
use orchid_extension::gen_expr::{GExpr, new_atom};
use orchid_extension::tree::{GenMember, MemKind, cnst, lazy};
use orchid_extension::{Atomic, OwnedAtom, OwnedVariant, TAtom};
use orchid_extension::tree::{GenMember, MemKind, lazy};
use orchid_extension::{
Atomic, ExecHandle, OwnedAtom, OwnedVariant, TAtom, ToExpr, TryFromExpr, exec,
};
use substack::Substack;
use crate::macros::lower::lower;
use crate::macros::macro_value::{Macro, MacroData, Rule};
use crate::macros::mactree::MacTreeSeq;
use crate::macros::resolve::resolve;
use crate::macros::rule::matcher::Matcher;
use crate::{MacTok, MacTree};
@@ -23,7 +27,7 @@ pub type Args = Vec<MacTree>;
pub struct MacroBodyArgCollector {
argc: usize,
args: Args,
cb: Rc<dyn Fn(Args) -> LocalBoxFuture<'static, GExpr>>,
cb: Rc<dyn for<'a> Fn(RuleCtx<'a>, Args) -> LocalBoxFuture<'a, GExpr>>,
}
impl Atomic for MacroBodyArgCollector {
type Data = ();
@@ -44,7 +48,11 @@ impl OwnedAtom for MacroBodyArgCollector {
});
self.args.push(atom.own().await);
if self.argc == self.args.len() {
(self.cb)(self.args).await.to_gen().await
exec(async move |handle| {
let rule_ctx = RuleCtx { handle };
(self.cb)(rule_ctx, self.args).await
})
.await
} else {
new_atom(self)
}
@@ -53,6 +61,29 @@ impl OwnedAtom for MacroBodyArgCollector {
fn body_name(name: &str, counter: usize) -> String { format!("({name})::{counter}") }
pub struct RuleCtx<'a> {
handle: ExecHandle<'a>,
}
impl RuleCtx<'_> {
/// Recursively resolve a subexpression
pub async fn recur(&mut self, mt: MacTree) -> MacTree { resolve(&mut self.handle, mt).await }
/// Recursively resolve a value from a delegate macro which is expected to
/// match only keywords and return any subexpressions in a datastructure
///
/// If this is used with syntax that matches an expression macro, names bound
/// in the enclosing scope will not be correctly matched and the conversion at
/// the end will fail.
pub async fn recur_call<T: TryFromExpr>(&mut self, mt: MacTree) -> OrcRes<T> {
let resolved = self.recur(mt).await;
let lowered = lower(&resolved, Substack::Bottom).await;
self.exec(lowered).await
}
/// Normalize a value, run an expression to completion.
pub async fn exec<T: TryFromExpr>(&mut self, val: impl ToExpr) -> OrcRes<T> {
self.handle.exec(val).await
}
}
pub(crate) fn build_macro(
prio: Option<u64>,
own_kws: impl IntoIterator<Item = &'static str>,
@@ -71,34 +102,33 @@ pub(crate) struct MacroBuilder {
body_consts: Vec<GenMember>,
}
impl MacroBuilder {
pub(crate) fn rule<const N: usize, R: ToExpr>(
pub(crate) fn rule<const N: usize>(
mut self,
pat: MacTreeSeq,
body: [impl AsyncFn([MacTree; N]) -> R + 'static; 1],
body: impl AsyncFn(RuleCtx, [MacTree; N]) -> OrcRes<MacTree> + 'static,
) -> Self {
let [body] = body;
let body = Rc::new(body);
let name = &body_name(self.own_kws[0], self.body_consts.len());
self.body_consts.extend(match N {
0 => lazy(true, name, async move |_| {
let argv = [].into_iter().collect_array().expect("N is 0");
MemKind::Const(body(argv).await.to_gen().await)
}),
1.. => cnst(
true,
name,
self.body_consts.extend(lazy(true, name, async |_| {
MemKind::Const(if N == 0 {
exec(async move |handle| {
let empty = std::iter::empty().collect_array::<N>().unwrap();
Ok(new_atom(body(RuleCtx { handle }, empty).await?))
})
.await
} else {
new_atom(MacroBodyArgCollector {
argc: N,
args: Vec::new(),
cb: Rc::new(move |argv| {
cb: Rc::new(move |rec, argv| {
let arr =
argv.into_iter().collect_array::<N>().expect("argc should enforce the length");
let body = body.clone();
Box::pin(async move { body(arr).await.to_gen().await })
Box::pin(async move { body(rec, arr).await.map(new_atom).to_gen().await })
}),
}),
),
});
})
})
}));
self.patterns.push(pat);
self
}
@@ -176,17 +206,26 @@ macro_rules! mactreev_impl {
}).at(orchid_base::Pos::Inherit));
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
(@RECUR $ret:ident "Val" $arg:expr) => {
$crate::macros::utils::mactreev_impl!(@RECUR $ret "Val" $arg ;)
};
(@RECUR $ret:ident "Val" $arg:expr ; $($tail:tt)*) => {
$ret.push(
$crate::macros::mactree::MacTok::Value($arg)
$crate::macros::mactree::MacTok::Value(orchid_extension::ToExpr::to_expr($arg).await)
.at(orchid_base::Pos::Inherit)
);
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
(@RECUR $ret:ident "push" $arg:expr) => {
$crate::macros::utils::mactreev_impl!(@RECUR $ret "push" $arg ;)
};
(@RECUR $ret:ident "push" $arg:expr ; $($tail:tt)*) => {
$ret.push($arg);
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
(@RECUR $ret:ident "pushv" $arg:expr) => {
$crate::macros::utils::mactreev_impl!(@RECUR $ret "pushv" $arg ;)
};
(@RECUR $ret:ident "pushv" $arg:expr ; $($tail:tt)*) => {
let $crate::macros::mactree::MacTok::S(_, body) = $arg.tok() else {
panic!("pushv used with non-vec value")
@@ -203,9 +242,15 @@ macro_rules! mactreev_impl {
).at(orchid_base::Pos::Inherit));
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
(@RECUR $ret:ident "l" $argh:tt $(:: $arg:tt)+ ($($body:tt)*) $($tail:tt)*) => {
(@RECUR $ret:ident "l" $arg:literal ($($body:tt)*) $($tail:tt)*) => {
assert!(
$arg.contains("::"),
"{} was treated as a name, but it doesn't have a namespace prefix",
$arg
);
let sym = orchid_base::Sym::parse($arg).await.expect("Empty string in sym literal in Rust");
$ret.push(MacTok::Lambda(
MacTok::Name(sym!($argh $(:: $arg)+).await).at(orchid_base::Pos::Inherit),
MacTok::Name(sym).at(orchid_base::Pos::Inherit),
$crate::macros::utils::mactreev!($($body)*)
).at(orchid_base::Pos::Inherit));
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
@@ -216,12 +261,9 @@ macro_rules! mactreev_impl {
"{} was treated as a name, but it doesn't have a namespace prefix",
$name
);
let sym = orchid_base::Sym::parse(
$name
).await.expect("Empty string in sym literal in Rust");
let sym = orchid_base::Sym::parse($name).await.expect("Empty string in sym literal in Rust");
$ret.push(
$crate::macros::mactree::MacTok::Name(sym)
.at(orchid_base::Pos::Inherit)
$crate::macros::mactree::MacTok::Name(sym).at(orchid_base::Pos::Inherit)
);
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
@@ -255,20 +297,6 @@ macro_rules! mactreev_impl {
);
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
(@RECUR $ret:ident $ns:ident :: $nhead:tt $($tail:tt)*) => {
$crate::macros::utils::mactreev_impl!(@NAME_MUNCHER $ret ($ns :: $nhead) $($tail)*)
};
(@NAME_MUNCHER $ret:ident ($($munched:tt)*) :: $name:tt $($tail:tt)*) => {
$crate::macros::utils::mactreev_impl!(@NAME_MUNCHER $ret ($($munched)* :: $name) $($tail)*)
};
(@NAME_MUNCHER $ret:ident ($($munched:tt)*) $($tail:tt)*) => {
let sym = orchid_base::sym!($($munched)*);
$ret.push(
$crate::macros::mactree::MacTok::Name(sym)
.at(orchid_base::Pos::Inherit)
);
$crate::macros::utils::mactreev_impl!(@RECUR $ret $($tail)*);
};
() => { Vec::new() };
}
macro_rules! mactreev {
@@ -282,4 +310,6 @@ macro_rules! mactreev {
};
}
pub(crate) use {mactree, mactreev, mactreev_impl};
pub(crate) use mactree;
pub(crate) use mactreev;
pub(crate) use mactreev_impl;

View File

@@ -4,8 +4,7 @@ use std::rc::Rc;
use futures::AsyncWrite;
use orchid_api_traits::Encode;
use orchid_extension::Atomic;
use orchid_extension::{DeserializeCtx, OwnedAtom, OwnedVariant};
use orchid_extension::{Atomic, DeserializeCtx, OwnedAtom, OwnedVariant};
#[derive(Clone)]
pub struct BlobAtom(pub(crate) Rc<Vec<u8>>);

View File

@@ -1,10 +1,8 @@
use orchid_api_derive::Coding;
use orchid_base::{FmtUnit, OrcRes, sym};
use orchid_extension::{ToExpr, TryFromExpr};
use orchid_extension::Expr;
use orchid_extension::gen_expr::GExpr;
use orchid_extension::tree::{GenMember, cnst, comments, fun, prefix};
use orchid_extension::{Atomic, TAtom, ThinAtom, ThinVariant};
use orchid_extension::{Atomic, Expr, TAtom, ThinAtom, ThinVariant, ToExpr, TryFromExpr};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Coding)]
pub struct Bool(pub bool);

View File

@@ -1,349 +0,0 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::{Ordering, Reverse};
use std::collections::{BinaryHeap, VecDeque};
use std::fmt::Debug;
use std::mem;
use std::num::NonZeroU64;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use std::time::Instant;
use async_event::Event;
use chrono::TimeDelta;
use futures::channel::{mpsc, oneshot};
use futures::{FutureExt, select};
use hashbrown::HashMap;
use never::Never;
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
use orchid_base::{FmtCtxImpl, OrcRes};
use orchid_extension::ToExpr;
use orchid_extension::entrypoint::spawn;
use orchid_extension::Expr;
use orchid_extension::gen_expr::{GExpr, IntoGExprStream, call, new_atom};
use orchid_extension::system::cted;
use orchid_extension::tree::{GenMember, cnst, comments, fun, prefix};
use orchid_extension::{
Atomic, ForeignAtom, OwnedAtom, OwnedVariant, err_not_callable, err_not_command,
};
use rust_decimal::prelude::Zero;
use tokio::task::{JoinHandle, spawn_local};
use tokio::time::sleep;
use crate::std::std_system::StdReq;
use crate::std::time::OrcDT;
use crate::{StdSystem, api};
#[derive(Clone, Copy, Coding, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct AsyncTaskId(NonZeroU64);
/// Signals to the scheduler that some async work is in progress, and to take
/// ownership of this expression representing the progress of that work. This
/// doesn't have to be called before [FinishAsyncWork] if keeping the work and
/// thus the requesting system alive is not necessary
#[derive(Debug, Clone, Coding, Hierarchy)]
#[extends(FutureReq, StdReq)]
pub struct AddAsyncWork(pub api::ExprTicket);
impl Request for AddAsyncWork {
type Response = AsyncTaskId;
}
/// Signals to the scheduler that some async work has been finished, and to
/// return this expression from a future `std::future::yield` call.
/// If [AddAsyncWork] was called before this, include the [AsyncTaskId] you
/// received to unlink the work from the scheduler so that cleanup is not
/// blocked.
#[derive(Debug, Clone, Coding, Hierarchy)]
#[extends(FutureReq, StdReq)]
pub struct FinishAsyncWork(pub Option<AsyncTaskId>, pub api::ExprTicket);
impl Request for FinishAsyncWork {
type Response = Result<(), SchedulerError>;
}
#[derive(Debug, Clone, Coding)]
pub struct SchedulerError;
#[derive(Debug, Clone, Coding, Hierarchy)]
#[extendable]
#[extends(StdReq)]
pub enum FutureReq {
AddAsyncWork(AddAsyncWork),
FinishAsyncWork(FinishAsyncWork),
}
#[derive(Clone)]
struct Timer {
set_at: Instant,
delay: TimeDelta,
repetition: Option<u64>,
cancelled: Rc<Event>,
action: Expr,
}
impl Timer {
pub fn next_occurrence(&self) -> Instant {
let delay_mult = i32::try_from(self.repetition.unwrap_or(0) + 1).unwrap();
self.set_at + (self.delay * delay_mult).to_std().unwrap()
}
}
impl PartialEq for Timer {
fn eq(&self, other: &Self) -> bool { self.next_occurrence().eq(&other.next_occurrence()) }
}
impl Eq for Timer {}
impl PartialOrd for Timer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Ord for Timer {
fn cmp(&self, other: &Self) -> Ordering { self.next_occurrence().cmp(&other.next_occurrence()) }
}
impl Atomic for Timer {
type Variant = OwnedVariant;
type Data = ();
}
impl OwnedAtom for Timer {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn command(mut self) -> CmdResult {
let sleep_until =
self.set_at + (self.delay * self.repetition.unwrap_or(1) as i32).to_std().unwrap();
let (timer_ready, on_timer_ready) = oneshot::channel();
let task = spawn(self.delay.to_std().unwrap(), async move { mem::drop(timer_ready.send(())) });
let res =
self.cancelled.wait_until_or_timeout(|| Some(()), on_timer_ready.map(mem::drop)).await;
task.abort();
// cancelled
if let Some(()) = res {
return Continuation::default().into();
}
// TODO: add binary API for sleep and
let mut ret = Continuation::default().into();
let mut ret = vec![self.action.to_gen().await];
if let Some(rep) = self.repetition.as_mut() {
*rep = *rep + 1;
ret.push(new_atom(self));
}
Ok(ret)
}
}
struct SchedulerState {
/// Waker to call when async work finishes
finish_waker: Waker,
timer_task: Option<(Instant, JoinHandle<()>)>,
id: NonZeroU64,
background: HashMap<AsyncTaskId, Expr>,
ready: VecDeque<Expr>,
timers: BinaryHeap<Reverse<Timer>>,
}
impl SchedulerState {
fn activate_timers(&mut self, now: Instant) {
while let Some(t) = self.timers.peek()
&& t.0.next_occurrence() < now
{
let mut timer = self.timers.pop().unwrap().0;
let work = timer.action.clone();
self.ready.push_back(work);
if let Some(count) = timer.repetition {
timer.repetition = Some(count + 1);
self.timers.push(Reverse(timer));
}
}
}
}
impl Debug for SchedulerState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SchedulerState").finish_non_exhaustive()
}
}
impl Default for SchedulerState {
fn default() -> Self {
SchedulerState {
background: HashMap::new(),
finish_waker: Waker::noop().clone(),
id: NonZeroU64::MIN,
timer_task: None,
ready: VecDeque::new(),
timers: BinaryHeap::new(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Scheduler(Rc<RefCell<SchedulerState>>);
impl Scheduler {
pub(crate) async fn add(&self, req: &AddAsyncWork) -> <AddAsyncWork as Request>::Response {
let expr = Expr::deserialize(req.0).await;
let mut this = self.0.borrow_mut();
let id = AsyncTaskId(this.id);
this.background.insert(id, expr);
this.id = this.id.checked_add(1).unwrap();
id
}
pub(crate) async fn finish(
&self,
req: &FinishAsyncWork,
) -> <FinishAsyncWork as Request>::Response {
let expr = Expr::deserialize(req.1).await;
let mut g = self.0.borrow_mut();
if let Some(id) = req.0 {
g.background.remove(&id);
}
g.ready.push_back(expr);
g.finish_waker.wake_by_ref();
Ok(())
}
}
#[derive(Clone)]
struct Yield;
impl Atomic for Yield {
type Variant = OwnedVariant;
type Data = ();
}
impl OwnedAtom for Yield {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn command(self) -> OrcRes<()> { Ok(()) }
}
#[derive(Clone)]
struct Spawn(ForeignAtom, ForeignAtom);
impl Atomic for Spawn {
type Variant = OwnedVariant;
type Data = [api::ExprTicket; 2];
}
impl OwnedAtom for Spawn {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> {
Cow::Owned([self.0.clone().ex().handle().ticket(), self.1.clone().ex().handle().ticket()])
}
async fn command(self) -> OrcRes<impl IntoGExprStream> { Ok((self.1, self.0)) }
}
#[derive(Clone)]
struct Canceller {
cont: Option<Expr>,
cancel: Rc<RefCell<Option<oneshot::Sender<()>>>>,
}
impl Atomic for Canceller {
type Variant = OwnedVariant;
type Data = ();
}
impl OwnedAtom for Canceller {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn call_ref(&self, arg: Expr) -> impl ToExpr {
match &self.cont {
Some(_) => Err(err_not_callable(&self.print_atom(&FmtCtxImpl::default()).await).await),
None => Ok(new_atom(Self { cont: Some(arg), cancel: self.cancel.clone() })),
}
}
async fn command(self) -> OrcRes<impl IntoGExprStream> {
let Some(cont) = self.cont else {
return Err(err_not_command(&self.print_atom(&FmtCtxImpl::default()).await).await);
};
if let Some(canceller) = self.cancel.borrow_mut().take() {
canceller.send(());
}
Ok(cont)
}
}
#[derive(Clone)]
struct SetTimer {
delay: TimeDelta,
recurring: bool,
action: Expr,
cont: Expr,
}
impl Atomic for SetTimer {
type Variant = OwnedVariant;
type Data = ();
}
impl OwnedAtom for SetTimer {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn command(self) -> OrcRes<impl IntoGExprStream> {
let (send, recv) = oneshot::channel();
Ok((
new_atom(Timer {
set_at: Instant::now(),
delay: self.delay,
cancelled: Rc::new(recv),
repetition: self.recurring.then_some(1),
action: self.action,
}),
call(
self.cont,
new_atom(Canceller { cont: None, cancel: Rc::new(RefCell::new(Some(send))) }),
),
))
}
}
pub fn gen_future_lib() -> Vec<GenMember> {
prefix("std", [comments(
[
"This library exposes a futures executor, and tools for timing and cooperative multitasking. \
The use of these tools is only possible in a command trampoline, i.e. a caller that always \
defers to the command implementation of an atom.",
"Any command that correctly integrates with this library should return `std::future::yield` \
as its final value on all codepaths, which is the (re)entry point of the trampoline. \
Returning any other command, especially the ones in `std::exit_code` causes the program to \
immediately exit.",
"Cancellers take a continuation, stop whatever process they are associated with from \
proceeding, and call the continuation with information about the cancelled work.",
"|type canceller: \\T ((T -> cmd) -> cmd)|",
],
prefix("future", [
comments(
[
"A command without a continuation that defers control to the queued set of commands.",
"|type: cmd|",
],
cnst(true, "yield", new_atom(Yield)),
),
comments(
[
"Takes two commands and queues both to be executed one after the other.",
"|type: cmd -> cmd -> cmd|",
],
fun(true, "spawn", async |left: ForeignAtom, right: ForeignAtom| {
new_atom(Spawn(left, right))
}),
),
comments(
[
"Takes a time amount to wait, the command to perform after waiting, and a continuation, \
and returns a command that sets a single-fire timeout. The continuation will be \
called with a canceller, which reports true if the task has not yet run.",
"|type: Duration -> cmd -> (canceller bool -> cmd) -> cmd|",
],
fun(true, "timeout", async |OrcDT(delay): OrcDT, action: Expr, cont: Expr| {
new_atom(SetTimer { delay, action, cont, recurring: false })
}),
),
comments(
[
"Takes a time amount to wait between repetitions, the command to perform periodically, \
and a continuation, and returns a command. The continuation will be called with a \
canceller, which reports how many times the interval has run.",
"|type: Duration -> cmd -> (canceller Int -> cmd) -> cmd|",
],
fun(true, "interval", async |OrcDT(delay): OrcDT, action: Expr, cont: Expr| {
new_atom(SetTimer { delay, action, cont, recurring: true })
}),
),
]),
)])
}
fn get_scheduler() -> Scheduler {
let cted = cted();
let std = cted.as_any().downcast_ref::<StdSystem>().unwrap();
let sched = std.sched.get_or_init(Scheduler::default);
sched.clone()
}
pub struct AsyncTaskAtom {}

View File

@@ -1 +0,0 @@
// pub mod future_lib;

View File

@@ -1,6 +1,5 @@
pub mod binary;
pub mod boolean;
pub mod future;
pub mod number;
pub mod ops;
pub mod option;

View File

@@ -45,13 +45,9 @@ impl ToExpr for Int {
}
}
impl Supports<ProtocolMethod> for Int {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: ProtocolMethod,
) -> io::Result<Receipt<'a>> {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ProtocolMethod) -> io::Result<Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &sym!(std::number::Int).to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, sym!(std::number::Int).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::add) {
@@ -65,20 +61,16 @@ impl Supports<ProtocolMethod> for Int {
} else if name == sym!(std::ops::mod) {
sym!(std::number::imod)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
hand.reply(req, &Some(val.to_expr().await.serialize().await)).await
hand.reply(req, Some(val.to_expr().await.serialize().await)).await
},
}
}
}
impl Supports<ToStringMethod> for Int {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: ToStringMethod,
) -> io::Result<Receipt<'a>> {
hand.reply(&req, &self.0.to_string()).await
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ToStringMethod) -> io::Result<Receipt> {
hand.reply(&req, self.0.to_string()).await
}
}
@@ -112,13 +104,9 @@ impl ToExpr for Float {
}
}
impl Supports<ProtocolMethod> for Float {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: ProtocolMethod,
) -> io::Result<Receipt<'a>> {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ProtocolMethod) -> io::Result<Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &sym!(std::number::Float).to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, sym!(std::number::Float).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::add) {
@@ -132,20 +120,16 @@ impl Supports<ProtocolMethod> for Float {
} else if name == sym!(std::ops::mod) {
sym!(std::number::fmod)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
hand.reply(req, &Some(val.to_expr().await.serialize().await)).await
hand.reply(req, Some(val.to_expr().await.serialize().await)).await
},
}
}
}
impl Supports<ToStringMethod> for Float {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: ToStringMethod,
) -> io::Result<Receipt<'a>> {
hand.reply(&req, &self.0.to_string()).await
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ToStringMethod) -> io::Result<Receipt> {
hand.reply(&req, self.0.to_string()).await
}
}

View File

@@ -1,8 +1,7 @@
use orchid_base::{name_char, name_start};
use orchid_base::{OrcRes, is};
use orchid_base::{OrcRes, is, name_char, name_start};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::{LexContext, LexedData, Lexer, err_not_applicable};
use orchid_extension::tree::GenTok;
use orchid_extension::{LexContext, LexedData, Lexer, err_not_applicable};
use crate::std::string::str_atom::IntStrAtom;

View File

@@ -4,11 +4,12 @@ use std::pin::Pin;
use futures::AsyncWrite;
use orchid_api_traits::Encode;
use orchid_base::{is, mk_errv, sym};
use orchid_extension::{ToExpr, TryFromExpr};
use orchid_extension::{Expr, ExprHandle};
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::tree::{GenMember, cnst, fun, prefix};
use orchid_extension::{Atomic, DeserializeCtx, ForeignAtom, OwnedAtom, OwnedVariant, TAtom};
use orchid_extension::{
Atomic, DeserializeCtx, Expr, ExprHandle, ForeignAtom, OwnedAtom, OwnedVariant, TAtom, ToExpr,
TryFromExpr,
};
use crate::{OrcString, api};

View File

@@ -1,13 +1,9 @@
use itertools::{Itertools, chain};
use orchid_base::Sym;
use orchid_base::{
Import, Parsed, Snippet, expect_tok, line_items, parse_multiname, token_errv,
};
use orchid_base::{Paren, Token};
use orchid_base::{IStr, OrcRes, is, mk_errv};
use orchid_extension::{
PTokTree, ParsCtx, ParsedLine, ParsedLineKind, p_tree2gen, p_v2gen,
IStr, Import, OrcRes, Paren, Parsed, Snippet, Sym, Token, expect_tok, is, line_items, mk_errv,
parse_multiname, token_errv,
};
use orchid_extension::{PTokTree, ParsCtx, ParsedLine, ParsedLineKind, p_tree2gen, p_v2gen};
pub async fn parse_impls(
_: &ParsCtx<'_>,

View File

@@ -2,9 +2,8 @@ use std::rc::Rc;
use hashbrown::HashMap;
use orchid_base::{Comment, OrcRes, Parsed, Token, expect_end, is, mk_errv, sym, try_pop_no_fluff};
use orchid_extension::ToExpr;
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser, ToExpr};
use crate::std::protocol::parse_impls::parse_impls;
use crate::std::protocol::types::Tag;

View File

@@ -2,9 +2,8 @@ use std::rc::Rc;
use hashbrown::HashMap;
use orchid_base::{Comment, OrcRes, Parsed, Token, expect_end, is, mk_errv, sym, try_pop_no_fluff};
use orchid_extension::ToExpr;
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser};
use orchid_extension::{PSnippet, ParsCtx, ParsedLine, Parser, ToExpr};
use crate::std::protocol::parse_impls::parse_impls;
use crate::std::protocol::types::Tag;

View File

@@ -36,16 +36,16 @@ impl OwnedAtom for Tag {
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(self.id.to_api()) }
}
impl Supports<ProtocolMethod> for Tag {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn orchid_base::ReqHandle<'a> + '_>,
hand: Box<dyn orchid_base::ReqHandle>,
req: ProtocolMethod,
) -> std::io::Result<orchid_base::Receipt<'a>> {
) -> std::io::Result<orchid_base::Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &self.id.to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, self.id.to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) =>
hand
.reply(req, &self.impls.get(&Sym::from_api(key).await).map(|expr| expr.handle().ticket()))
.reply(req, self.impls.get(&Sym::from_api(key).await).map(|expr| expr.handle().ticket()))
.await,
}
}
@@ -87,11 +87,11 @@ impl OwnedAtom for Tagged {
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(self.tag.id.to_api()) }
}
impl Supports<ProtocolMethod> for Tagged {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn orchid_base::ReqHandle<'a> + '_>,
hand: Box<dyn orchid_base::ReqHandle>,
req: ProtocolMethod,
) -> io::Result<orchid_base::Receipt<'a>> {
) -> io::Result<orchid_base::Receipt> {
self.tag.handle(hand, req).await
}
}

View File

@@ -9,10 +9,8 @@ use hashbrown::HashMap;
use orchid_api_derive::Coding;
use orchid_api_traits::{Encode, Request};
use orchid_base::{IStr, Receipt, ReqHandle, ReqHandleExt, Sym, es, sym};
use orchid_extension::ToExpr;
use orchid_extension::Expr;
use orchid_extension::{
Atomic, DeserializeCtx, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports,
Atomic, DeserializeCtx, Expr, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, ToExpr,
};
use crate::api;
@@ -42,13 +40,9 @@ impl OwnedAtom for RecordAtom {
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<ProtocolMethod> for RecordAtom {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: ProtocolMethod,
) -> io::Result<Receipt<'a>> {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: ProtocolMethod) -> io::Result<Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &sym!(std::record::Record).to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, sym!(std::record::Record).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::get) {
@@ -56,9 +50,9 @@ impl Supports<ProtocolMethod> for RecordAtom {
} else if name == sym!(std::ops::set) {
sym!(std::record::set)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
return hand.reply(req, &Some(val.to_expr().await.serialize().await)).await;
return hand.reply(req, Some(val.to_expr().await.serialize().await)).await;
},
}
}

View File

@@ -3,10 +3,9 @@ use std::rc::Rc;
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_base::{is, mk_errv};
use orchid_extension::TAtom;
use orchid_extension::Expr;
use orchid_extension::gen_expr::{arg, new_atom};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::tree::{GenMember, cnst, fun, prefix};
use orchid_extension::{Expr, TAtom, get_arg_posv};
use crate::std::record::record_atom::RecordAtom;
use crate::std::string::str_atom::IntStrAtom;
@@ -26,7 +25,7 @@ pub fn gen_record_lib() -> Vec<GenMember> {
None => Err(mk_errv(
is("Key not found in record").await,
format!("{} is not in this record, valid keys are {}", key.0, record.0.keys().join(", ")),
[arg(0).pos.clone(), arg(1).pos.clone()],
get_arg_posv([0, 1]).await,
)),
}
}),

View File

@@ -27,12 +27,12 @@ impl OwnedAtom for SymAtom {
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(SymAtomData(self.0.tok().to_api())) }
}
impl Supports<ToStringMethod> for SymAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn orchid_base::ReqHandle<'a> + '_>,
hand: Box<dyn orchid_base::ReqHandle>,
req: ToStringMethod,
) -> std::io::Result<orchid_base::Receipt<'a>> {
hand.reply(&req, &self.0.to_string()).await
) -> std::io::Result<orchid_base::Receipt> {
hand.reply(&req, self.0.to_string()).await
}
}

View File

@@ -4,10 +4,10 @@ use futures::future::join_all;
use orchid_api_derive::{Coding, Hierarchy};
use orchid_base::{Receipt, ReqHandle, ReqHandleExt, Sym, es, sym};
use orchid_extension::gen_expr::new_atom;
use orchid_extension::ParserObj;
use orchid_extension::tree::{GenMember, merge_trivial};
use orchid_extension::{
AtomOps, AtomicFeatures, Expr, LexerObj, ReqForSystem, System, SystemCard, SystemCtor, ToExpr,
AtomOps, AtomicFeatures, Expr, LexerObj, ParserObj, ReqForSystem, System, SystemCard, SystemCtor,
ToExpr,
};
use super::number::num_lib::gen_num_lib;
@@ -27,6 +27,7 @@ use crate::std::protocol::types::{CreateTag, Tag, Tagged, gen_protocol_lib};
use crate::std::record::record_atom::{CreateRecord, RecordAtom};
use crate::std::record::record_lib::gen_record_lib;
use crate::std::reflection::sym_atom::{CreateSymAtom, SymAtom, gen_sym_lib};
use crate::std::stream::stream_cmds::{ReadStreamCmd, WriteStreamCmd};
use crate::std::stream::stream_lib::gen_stream_lib;
use crate::std::string::str_lexer::StringLexer;
use crate::std::time::{CreateDT, gen_time_lib};
@@ -72,27 +73,25 @@ impl SystemCard for StdSystem {
Some(TupleBuilder::ops()),
Some(Tag::ops()),
Some(Tagged::ops()),
Some(ReadStreamCmd::ops()),
Some(WriteStreamCmd::ops()),
]
}
}
impl System for StdSystem {
type Ctor = Self;
async fn request<'a>(
&self,
xreq: Box<dyn ReqHandle<'a> + 'a>,
req: ReqForSystem<Self>,
) -> Receipt<'a> {
async fn request(&self, xreq: Box<dyn ReqHandle>, req: ReqForSystem<Self>) -> Receipt {
match req {
StdReq::CreateInt(ref req @ CreateInt(int)) =>
xreq.reply(req, &new_atom(int).to_expr().await.serialize().await).await.unwrap(),
xreq.reply(req, new_atom(int).to_expr().await.serialize().await).await.unwrap(),
StdReq::CreateFloat(ref req @ CreateFloat(float)) =>
xreq.reply(req, &new_atom(float).to_expr().await.serialize().await).await.unwrap(),
xreq.reply(req, new_atom(float).to_expr().await.serialize().await).await.unwrap(),
StdReq::CreateDT(ref req @ CreateDT(dt)) =>
xreq.reply(req, &new_atom(dt).to_expr().await.serialize().await).await.unwrap(),
xreq.reply(req, new_atom(dt).to_expr().await.serialize().await).await.unwrap(),
StdReq::CreateTuple(ref req @ CreateTuple(ref items)) => {
let tpl = Tuple(Rc::new(join_all(items.iter().copied().map(Expr::deserialize)).await));
let tk = new_atom(tpl).to_expr().await.serialize().await;
xreq.reply(req, &tk).await.unwrap()
xreq.reply(req, tk).await.unwrap()
},
StdReq::CreateRecord(ref req @ CreateRecord(ref items)) => {
let values =
@@ -100,11 +99,11 @@ impl System for StdSystem {
.await;
let rec = RecordAtom(Rc::new(values.into_iter().collect()));
let tk = new_atom(rec).to_expr().await.serialize().await;
xreq.reply(req, &tk).await.unwrap()
xreq.reply(req, tk).await.unwrap()
},
StdReq::CreateSymAtom(ref req @ CreateSymAtom(sym_tok)) => {
let sym_atom = SymAtom(Sym::from_api(sym_tok).await);
xreq.reply(req, &new_atom(sym_atom).to_expr().await.serialize().await).await.unwrap()
xreq.reply(req, new_atom(sym_atom).to_expr().await.serialize().await).await.unwrap()
},
StdReq::CreateTag(ref req @ CreateTag { name, ref impls }) => {
let tag_atom = Tag {
@@ -119,7 +118,7 @@ impl System for StdSystem {
.collect(),
),
};
xreq.reply(req, &new_atom(tag_atom).to_expr().await.serialize().await).await.unwrap()
xreq.reply(req, new_atom(tag_atom).to_expr().await.serialize().await).await.unwrap()
},
}
}

View File

@@ -3,12 +3,15 @@ use std::io;
use std::rc::Rc;
use never::Never;
use orchid_base::{ReqHandleExt, fmt, is, mk_errv};
use orchid_extension::gen_expr::{bot, call, new_atom};
use orchid_extension::std_reqs::{ReadLimit, ReadReq, RunCommand};
use orchid_extension::{Atomic, Expr, ForeignAtom, OwnedAtom, OwnedVariant, Supports, ToExpr};
use orchid_base::{Receipt, ReqHandle, ReqHandleExt, fmt, is, mk_errv};
use orchid_extension::gen_expr::{bot, call, new_atom, serialize};
use orchid_extension::std_reqs::{CloseReq, FlushReq, ReadLimit, ReadReq, StartCommand, WriteReq};
use orchid_extension::{
Atomic, Expr, ForeignAtom, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, ToExpr,
};
use crate::std::binary::binary_atom::BlobAtom;
use crate::std::string::str_atom::StrAtom;
#[derive(Clone, Debug)]
pub struct ReadStreamCmd {
@@ -16,40 +19,99 @@ pub struct ReadStreamCmd {
pub limit: ReadLimit,
pub succ: Expr,
pub fail: Expr,
pub as_str: bool,
}
impl Atomic for ReadStreamCmd {
type Variant = OwnedVariant;
type Data = ();
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<StartCommand>() }
}
impl OwnedAtom for ReadStreamCmd {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<RunCommand> for ReadStreamCmd {
async fn handle<'a>(
&self,
hand: Box<dyn orchid_base::ReqHandle<'a> + '_>,
req: RunCommand,
) -> io::Result<orchid_base::Receipt<'a>> {
let ret = match self.hand.call(ReadReq(self.limit.clone())).await {
None => Err(mk_errv(
is("Atom is not readable").await,
format!("Expected a readable stream handle, found {}", fmt(&self.hand).await),
[self.hand.pos()],
impl Supports<StartCommand> for ReadStreamCmd {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: StartCommand) -> io::Result<Receipt> {
let ret = 'ret: {
let Some(read_res) = self.hand.call(ReadReq { limit: self.limit.clone() }).await else {
break 'ret Err(mk_errv(
is("Atom is not readable").await,
format!("Expected a readable stream handle, found {}", fmt(&self.hand).await),
[self.hand.pos()],
));
};
let res = match read_res {
Err(e) => Err(mk_errv(
is(e.kind.message()).await,
format!("An error occurred while reading: {}", e.message),
[self.hand.pos(), self.succ.pos().await],
)),
Ok(v) if !self.as_str => Ok(new_atom(BlobAtom(Rc::new(v)))),
Ok(v) => match String::from_utf8(v) {
Ok(s) => Ok(new_atom(StrAtom(Rc::new(s)))),
Err(e) => Err(mk_errv(is("Invalid utf8 in input string").await, e.to_string(), [
self.hand.pos(),
self.succ.pos().await,
])),
},
};
Ok(match res {
Err(e) => call(self.fail.clone(), bot(e)),
Ok(gex) => call(self.succ.clone(), gex),
})
};
hand.reply(&req, Some(serialize(ret.to_gen().await).await)).await
}
}
#[derive(Clone, Debug)]
pub enum WriteAction {
Write(Rc<Vec<u8>>),
Flush,
Close,
}
#[derive(Clone, Debug)]
pub struct WriteStreamCmd {
pub hand: ForeignAtom,
pub action: WriteAction,
pub succ: Expr,
pub fail: Expr,
}
impl Atomic for WriteStreamCmd {
type Variant = OwnedVariant;
type Data = ();
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<StartCommand>() }
}
impl OwnedAtom for WriteStreamCmd {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<StartCommand> for WriteStreamCmd {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: StartCommand) -> io::Result<Receipt> {
let result = match &self.action {
WriteAction::Write(bin) => self.hand.call(WriteReq { data: bin.to_vec() }).await,
WriteAction::Flush => self.hand.call(FlushReq).await,
WriteAction::Close => self.hand.call(CloseReq).await,
};
let cont = match result {
None => bot(mk_errv(
is("Not a writer").await,
format!("{} cannot be written to", fmt(&self.hand).await),
[self.hand.pos(), self.succ.pos().await],
)),
Some(Err(e)) => Ok(
Some(Err(e)) =>
call(
self.fail.clone(),
bot(mk_errv(
is(e.kind.message()).await,
format!("An error occurred while reading: {}", e.message),
format!("An error occurred while writing: {}", e.message),
[self.hand.pos(), self.succ.pos().await],
)),
)
.await,
),
Some(Ok(v)) => Ok(call(self.succ.clone(), new_atom(BlobAtom(Rc::new(v)))).await),
Some(Ok(())) => self.succ.clone().to_gen().await,
};
hand.reply(&req, &Some(ret.to_gen().await.serialize().await)).await
hand.reply(&req, Some(serialize(cont.to_gen().await).await)).await
}
}

View File

@@ -1,22 +1,26 @@
use std::num::NonZero;
use std::rc::Rc;
use itertools::Itertools;
use orchid_base::{is, mk_errv};
use orchid_extension::gen_expr::{call, new_atom};
use orchid_extension::std_reqs::ReadLimit;
use orchid_extension::tree::{GenMember, comments, fun, prefix};
use orchid_extension::{Expr, ForeignAtom, get_arg};
use orchid_extension::{Expr, ForeignAtom, TAtom, get_arg};
use crate::Int;
use crate::std::binary::binary_atom::BlobAtom;
use crate::std::stream::stream_cmds::ReadStreamCmd;
use crate::std::stream::stream_cmds::{ReadStreamCmd, WriteAction, WriteStreamCmd};
use crate::{Int, OrcString};
pub fn gen_stream_lib() -> Vec<GenMember> {
prefix("std", [comments(
["Read from and write to byte streams"],
prefix("stream", [
fun(true, "read_bin", async |hand: ForeignAtom, succ: Expr, fail: Expr| {
new_atom(ReadStreamCmd { hand, succ, fail, limit: ReadLimit::End })
new_atom(ReadStreamCmd { hand, succ, fail, as_str: false, limit: ReadLimit::End })
}),
fun(true, "read_str", async |hand: ForeignAtom, succ: Expr, fail: Expr| {
new_atom(ReadStreamCmd { hand, succ, fail, as_str: true, limit: ReadLimit::End })
}),
fun(true, "read_until", async |hand: ForeignAtom, delim: Int, succ: Expr, fail: Expr| {
let Ok(end) = delim.0.try_into() else {
@@ -29,12 +33,15 @@ pub fn gen_stream_lib() -> Vec<GenMember> {
[get_arg(1).pos().await],
));
};
Ok(new_atom(ReadStreamCmd { hand, succ, fail, limit: ReadLimit::Delimiter(end) }))
let limit = ReadLimit::Delimiter(end);
Ok(new_atom(ReadStreamCmd { hand, succ, fail, as_str: false, limit }))
}),
fun(true, "read_bytes", async |hand: ForeignAtom, count: Int, succ: Expr, fail: Expr| {
match count.0.try_into().map(NonZero::new) {
Ok(Some(nzlen)) =>
Ok(new_atom(ReadStreamCmd { hand, succ, fail, limit: ReadLimit::Length(nzlen) })),
Ok(Some(nzlen)) => {
let limit = ReadLimit::Length(nzlen);
Ok(new_atom(ReadStreamCmd { hand, succ, fail, as_str: false, limit }))
},
Ok(None) => Ok(call(succ, new_atom(BlobAtom(Rc::default()))).await),
Err(_) => Err(mk_errv(
is("Length cannot be negative").await,
@@ -43,6 +50,28 @@ pub fn gen_stream_lib() -> Vec<GenMember> {
)),
}
}),
fun(true, "read_line", async |hand: ForeignAtom, succ: Expr, fail: Expr| {
const LIMIT_BR: ReadLimit = ReadLimit::Delimiter(b'\n');
new_atom(ReadStreamCmd { hand, succ, fail, as_str: true, limit: LIMIT_BR })
}),
fun(true, "write_str", async |hand: ForeignAtom, str: OrcString, succ: Expr, fail: Expr| {
let action = WriteAction::Write(Rc::new(str.get_string().await.bytes().collect_vec()));
new_atom(WriteStreamCmd { hand, action, succ, fail })
}),
fun(
true,
"write_bin",
async |hand: ForeignAtom, bin: TAtom<BlobAtom>, succ: Expr, fail: Expr| {
let action = WriteAction::Write(bin.own().await.0.clone());
new_atom(WriteStreamCmd { hand, action, succ, fail })
},
),
fun(true, "flush", async |hand: ForeignAtom, succ: Expr, fail: Expr| {
new_atom(WriteStreamCmd { hand, action: WriteAction::Flush, succ, fail })
}),
fun(true, "close", async |hand: ForeignAtom, succ: Expr, fail: Expr| {
new_atom(WriteStreamCmd { hand, action: WriteAction::Close, succ, fail })
}),
]),
)])
}

View File

@@ -10,10 +10,9 @@ use orchid_api_traits::{Encode, Request};
use orchid_base::{
FmtCtx, FmtUnit, IStr, OrcRes, Receipt, ReqHandle, ReqHandleExt, Sym, es, is, mk_errv, sym,
};
use orchid_extension::{ToExpr, TryFromExpr};
use orchid_extension::Expr;
use orchid_extension::{
AtomMethod, Atomic, DeserializeCtx, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, TAtom,
AtomMethod, Atomic, DeserializeCtx, Expr, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports,
TAtom, ToExpr, TryFromExpr,
};
use crate::std::protocol::types::{GetImpl, ProtocolMethod};
@@ -29,7 +28,7 @@ impl AtomMethod for StringGetValMethod {
}
#[derive(Clone)]
pub struct StrAtom(Rc<String>);
pub struct StrAtom(pub(crate) Rc<String>);
impl Atomic for StrAtom {
type Variant = OwnedVariant;
type Data = ();
@@ -61,39 +60,39 @@ impl OwnedAtom for StrAtom {
}
}
impl Supports<StringGetValMethod> for StrAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
hand: Box<dyn ReqHandle>,
req: StringGetValMethod,
) -> io::Result<Receipt<'a>> {
hand.reply(&req, &self.0).await
) -> io::Result<Receipt> {
hand.reply(&req, self.0.clone()).await
}
}
impl Supports<ToStringMethod> for StrAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
hand: Box<dyn ReqHandle>,
req: ToStringMethod,
) -> io::Result<Receipt<'a>> {
hand.reply(&req, &self.0).await
) -> io::Result<Receipt> {
hand.reply(&req, self.0.to_string()).await
}
}
impl Supports<ProtocolMethod> for StrAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
hand: Box<dyn ReqHandle>,
req: ProtocolMethod,
) -> io::Result<Receipt<'a>> {
) -> io::Result<Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &sym!(std::string::StrAtom).to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, sym!(std::string::StrAtom).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::add) {
sym!(std::string::concat)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
hand.reply(req, &Some(val.to_expr().await.serialize().await)).await
hand.reply(req, Some(val.to_expr().await.serialize().await)).await
},
}
}
@@ -131,31 +130,31 @@ impl TryFromExpr for IntStrAtom {
}
}
impl Supports<ToStringMethod> for IntStrAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
hand: Box<dyn ReqHandle>,
req: ToStringMethod,
) -> io::Result<Receipt<'a>> {
hand.reply(&req, &self.0.rc()).await
) -> io::Result<Receipt> {
hand.reply(&req, self.0.to_string()).await
}
}
impl Supports<ProtocolMethod> for IntStrAtom {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
hand: Box<dyn ReqHandle>,
req: ProtocolMethod,
) -> io::Result<Receipt<'a>> {
) -> io::Result<Receipt> {
match req {
ProtocolMethod::GetTagId(req) =>
hand.reply(&req, &sym!(std::string::IntStrAtom).to_api()).await,
hand.reply(&req, sym!(std::string::IntStrAtom).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::add) {
sym!(std::string::concat)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
hand.reply(req, &Some(val.to_expr().await.serialize().await)).await
hand.reply(req, Some(val.to_expr().await.serialize().await)).await
},
}
}

View File

@@ -1,10 +1,8 @@
use itertools::Itertools;
use orchid_base::{OrcErr, OrcErrv, OrcRes, Paren, SrcRange, Sym, is, mk_errv, sym, wrap_tokv};
use orchid_extension::ToExpr;
use orchid_extension::gen_expr::new_atom;
use orchid_extension::{LexContext, Lexer, err_not_applicable};
use orchid_extension::p_tree2gen;
use orchid_extension::tree::{GenTok, GenTokTree, ref_tok, x_tok};
use orchid_extension::{LexContext, Lexer, ToExpr, err_not_applicable, p_tree2gen};
use super::str_atom::IntStrAtom;

View File

@@ -1,15 +1,14 @@
use std::borrow::Cow;
use std::io;
use std::time::Instant;
use chrono::TimeDelta;
use chrono::{DateTime, TimeDelta, Utc};
use never::Never;
use orchid_api::ExprTicket;
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
use orchid_base::{Numeric, OrcRes, Receipt, ReqHandle, ReqHandleExt};
use orchid_extension::gen_expr::{GExpr, call, new_atom};
use orchid_extension::std_reqs::{AsDuration, RunCommand};
use orchid_extension::gen_expr::{GExpr, call, new_atom, serialize};
use orchid_extension::std_reqs::{AsInstant, StartCommand};
use orchid_extension::tree::{GenMember, fun, prefix};
use orchid_extension::{
Atomic, Expr, MethodSetBuilder, OwnedAtom, OwnedVariant, Supports, TAtom, ThinAtom, ThinVariant,
@@ -44,18 +43,9 @@ impl TryFromExpr for OrcDT {
Ok(TAtom::<OrcDT>::try_from_expr(expr).await?.value)
}
}
impl Supports<AsDuration> for OrcDT {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: AsDuration,
) -> std::io::Result<Receipt<'a>> {
hand.reply(&req, &self.0.to_std().unwrap()).await
}
}
#[derive(Clone)]
pub struct InstantAtom(Instant);
pub struct InstantAtom(DateTime<Utc>);
impl Atomic for InstantAtom {
type Variant = OwnedVariant;
type Data = ();
@@ -64,26 +54,27 @@ impl OwnedAtom for InstantAtom {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<AsInstant> for InstantAtom {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: AsInstant) -> std::io::Result<Receipt> {
hand.reply(&req, self.0).await
}
}
#[derive(Clone)]
struct Now(Expr);
impl Atomic for Now {
type Variant = OwnedVariant;
type Data = ();
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<RunCommand>() }
fn reg_methods() -> MethodSetBuilder<Self> { MethodSetBuilder::new().handle::<StartCommand>() }
}
impl OwnedAtom for Now {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
}
impl Supports<RunCommand> for Now {
async fn handle<'a>(
&self,
hand: Box<dyn ReqHandle<'a> + '_>,
req: RunCommand,
) -> io::Result<Receipt<'a>> {
let cont = call(self.0.clone(), new_atom(InstantAtom(Instant::now()))).await.serialize().await;
hand.reply(&req, &Some(cont)).await
impl Supports<StartCommand> for Now {
async fn handle(&self, hand: Box<dyn ReqHandle>, req: StartCommand) -> io::Result<Receipt> {
let cont = serialize(call(self.0.clone(), new_atom(InstantAtom(Utc::now()))).await).await;
hand.reply(&req, Some(cont)).await
}
}

View File

@@ -48,13 +48,13 @@ impl OwnedAtom for Tuple {
}
}
impl Supports<ProtocolMethod> for Tuple {
async fn handle<'a>(
async fn handle(
&self,
hand: Box<dyn orchid_base::ReqHandle<'a> + '_>,
hand: Box<dyn orchid_base::ReqHandle>,
req: ProtocolMethod,
) -> std::io::Result<orchid_base::Receipt<'a>> {
) -> std::io::Result<orchid_base::Receipt> {
match req {
ProtocolMethod::GetTagId(req) => hand.reply(&req, &sym!(std::tuple).to_api()).await,
ProtocolMethod::GetTagId(req) => hand.reply(&req, sym!(std::tuple).to_api()).await,
ProtocolMethod::GetImpl(ref req @ GetImpl(key)) => {
let name = Sym::from_api(key).await;
let val = if name == sym!(std::ops::get) {
@@ -62,9 +62,9 @@ impl Supports<ProtocolMethod> for Tuple {
} else if name == sym!(std::ops::set) {
sym!(std::tuple::set)
} else {
return hand.reply(req, &None).await;
return hand.reply(req, None).await;
};
hand.reply(req, &Some(val.to_expr().await.serialize().await)).await
hand.reply(req, Some(val.to_expr().await.serialize().await)).await
},
}
}

View File

@@ -1,80 +1,157 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"[markdown]": {
// markdown denotes line breaks with trailing space
"diffEditor.ignoreTrimWhitespace": false,
// Disable editor gadgets in markdown
"editor.unicodeHighlight.ambiguousCharacters": false,
"editor.unicodeHighlight.invisibleCharacters": false,
"editor.glyphMargin": false,
"editor.guides.indentation": false,
"editor.lineNumbers": "off",
"editor.quickSuggestions": {
"comments": "off",
"strings": "off",
"other": "off",
},
"editor.rulers": [],
"editor.wordWrap": "bounded",
"editor.wordWrapColumn": 80,
// wrap lines as we go
"editor.formatOnType": true,
"folders": [
{
"path": "."
}
],
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Build All",
"command": "cargo",
"options": {
"cwd": "${workspaceFolder}"
},
"type": "shell",
"args": [
"build"
],
"problemMatcher": [
"$rustc"
],
"presentation": {
"reveal": "always"
},
"group": "build"
}
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "(LLDB) Launch",
"type": "lldb",
"request": "launch",
"preLaunchTask": "Build All",
"env": {
"ORCHID_EXTENSIONS": "target/debug/orchid_std",
"ORCHID_DEFAULT_SYSTEMS": "orchid::std;orchid::macros",
},
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/target/debug/orcx.exe",
"args": [
"--logs=stderr",
"--logs=debug>stderr",
"--logs=msg>stderr",
"exec",
"1 + 1"
]
},
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"requireExactSource": true,
"preLaunchTask": "Build All",
"environment": [
{
"name": "ORCHID_EXTENSIONS",
"value": "target/debug/orchid_std"
},
{
"name": "ORCHID_DEFAULT_SYSTEMS",
"value": "orchid::std;orchid::macros"
},
],
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/target/debug/orcx.exe",
"args": [
"--logs=stderr",
"--logs=debug>stderr",
"--logs=msg>stderr",
"exec",
"1 + 1"
]
}
],
"compounds": []
},
"settings": {
"[markdown]": {
// markdown denotes line breaks with trailing space
"diffEditor.ignoreTrimWhitespace": false,
// Disable editor gadgets in markdown
"editor.unicodeHighlight.ambiguousCharacters": false,
"editor.unicodeHighlight.invisibleCharacters": false,
"editor.glyphMargin": false,
"editor.guides.indentation": false,
"editor.lineNumbers": "off",
"editor.quickSuggestions": {
"comments": "off",
"strings": "off",
"other": "off",
},
"editor.rulers": [],
"editor.wordWrap": "bounded",
"editor.wordWrapColumn": 80,
// wrap lines as we go
"editor.formatOnType": true,
"editor.detectIndentation": false,
"editor.insertSpaces": false,
},
// Orchid is a human-made project
"chat.commandCenter.enabled": false,
},
// Orchid is a human-made project
"chat.commandCenter.enabled": false,
// use spaces for indentation for Rust for now due to a rustfmt bug
"editor.tabSize": 2,
"editor.stickyTabStops": true,
"editor.detectIndentation": false,
"editor.insertSpaces": true,
"editor.tabSize": 2,
"editor.stickyTabStops": true,
"editor.detectIndentation": false,
"editor.insertSpaces": true,
// Important; for accessibility reasons, code cannot be wider than 100ch
"editor.rulers": [ 100 ],
"editor.formatOnSave": true,
"editor.rulers": [
100
],
"editor.formatOnSave": true,
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.hg/store/**": true,
"target": true,
},
"git.confirmSync": false,
"git.enableSmartCommit": true,
"git.autofetch": true,
"rust-analyzer.assist.emitMustUse": true,
"rust-analyzer.assist.preferSelf": true,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.command": "clippy",
"rust-analyzer.check.features": "all",
"rust-analyzer.checkOnSave": true,
"rust-analyzer.completion.fullFunctionSignatures.enable": true,
"rust-analyzer.completion.termSearch.enable": true,
"rust-analyzer.inlayHints.parameterHints.enable": false,
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.rustfmt.extraArgs": [
"+nightly",
],
"rust-analyzer.showUnlinkedFileNotification": false,
"swissknife.notesEnabled": false,
"git.confirmSync": false,
"git.enableSmartCommit": true,
"git.autofetch": true,
"rust-analyzer.assist.emitMustUse": true,
"rust-analyzer.assist.preferSelf": true,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.command": "clippy",
"rust-analyzer.check.features": "all",
"rust-analyzer.checkOnSave": true,
"rust-analyzer.completion.fullFunctionSignatures.enable": true,
"rust-analyzer.completion.termSearch.enable": true,
"rust-analyzer.inlayHints.parameterHints.enable": false,
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.rustfmt.extraArgs": [
"+nightly",
],
"rust-analyzer.showUnlinkedFileNotification": false,
"swissknife.notesEnabled": false,
"todo-tree.filtering.excludeGlobs": [
"**/node_modules/*/**",
"orchidlang/**"
],
"todo-tree.regex.regex": "(//|#|<!--|--|;|/\\*|^|^[ \\t]*(-|\\d+.))\\s*($TAGS)"
},
"extensions": {
"recommendations": [
"fill-labs.dependi",
"gruntfuggly.todo-tree",
"maptz.regionfolder",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
"yzhang.markdown-all-in-one",
]
},
"extensions": {
"recommendations": [
"fill-labs.dependi",
"gruntfuggly.todo-tree",
"maptz.regionfolder",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
"yzhang.markdown-all-in-one",
]
},
},
}

View File

@@ -2,20 +2,26 @@
name = "orcx"
version = "0.1.0"
edition = "2024"
authors = ["Lawrence Bethlenfalvy <lbfalvy@protonmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
camino = "1.2.2"
clap = { version = "4.5.54", features = ["derive", "env"] }
clap = { version = "4.5.54", features = ["derive", "env", "cargo"] }
ctrlc = "3.5.1"
futures = "0.3.31"
itertools = "0.14.0"
never = "0.1.0"
orchid-api = { version = "0.1.0", path = "../orchid-api" }
orchid-base = { version = "0.1.0", path = "../orchid-base" }
orchid-extension = { version = "0.1.0", path = "../orchid-extension" }
orchid-host = { version = "0.1.0", path = "../orchid-host", features = [
"tokio",
"tokio",
"orchid-extension",
] }
stacker = "0.1.23"
substack = "1.1.1"
tokio = { version = "1.49.0", features = ["full"] }
tokio-util = { version = "0.7.18", features = ["compat"] }

View File

@@ -1,12 +1,20 @@
use orchid_base::Logger;
use never::Never;
use orchid_base::{IStr, Logger, NameLike, Receipt, ReqHandle, Sym};
use orchid_extension::{self as ox, OrcReader, OrcWriter};
use orchid_host::cmd_system::{CmdEvent, CmdRunner};
use orchid_host::dylib::ext_dylib;
use orchid_host::inline::ext_inline;
use orchid_host::tree::Root;
use tokio::time::Instant;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
pub mod parse_folder;
mod print_mod;
mod repl;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::io::Read;
use std::pin::pin;
use std::process::{Command, ExitCode};
use std::rc::Rc;
@@ -20,8 +28,8 @@ use futures::{FutureExt, Stream, TryStreamExt, io};
use itertools::Itertools;
use orchid_base::local_interner::local_interner;
use orchid_base::{
FmtCtxImpl, Format, Import, NameLike, Snippet, SrcRange, Token, VPath, fmt, fmt_v, is, log, sym,
take_first, try_with_reporter, ttv_fmt, with_interner, with_logger, with_reporter, with_stash,
FmtCtxImpl, Snippet, SrcRange, Token, VPath, fmt, fmt_v, is, log, sym, take_first,
try_with_reporter, ttv_fmt, with_interner, with_logger, with_reporter, with_stash,
};
use orchid_host::ctx::{Ctx, JoinHandle, Spawner};
use orchid_host::execute::{ExecCtx, ExecResult};
@@ -30,54 +38,134 @@ 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::parsed::{ParsTokTree, ParsedModule};
use orchid_host::subprocess::ext_command;
use orchid_host::system::init_systems;
use orchid_host::tree::{MemberKind, Module, RootData};
use orchid_host::system::{System, init_systems};
use substack::Substack;
use tokio::io::{AsyncBufReadExt, BufReader, stdin};
use tokio::task::{LocalSet, spawn_local};
use crate::parse_folder::parse_folder;
use crate::repl::repl;
/// Native interpreter for the Orchid programming language
#[derive(Parser, Debug)]
#[command(version, about, long_about)]
#[command(version, about, long_about, verbatim_doc_comment)]
pub struct Args {
#[arg(short, long, env = "ORCHID_EXTENSIONS", value_delimiter = ';')]
/// Load an extension from a file. The file extension should be omitted, the
/// loader checks for a range of platform-specific file extensions (foo.exe
/// and foo.dll on Windows, libfoo.so or foo on other platforms)
#[arg(
short,
long,
env = "ORCHID_EXTENSIONS",
value_delimiter = ';',
next_line_help = true,
verbatim_doc_comment
)]
extension: Vec<Utf8PathBuf>,
#[arg(short, long, env = "ORCHID_DEFAULT_SYSTEMS", value_delimiter = ';')]
/// Instantiate a system by name. The system must be provided by one of the
/// loaded extensions
#[arg(
short,
long,
env = "ORCHID_DEFAULT_SYSTEMS",
value_delimiter = ';',
next_line_help = true,
verbatim_doc_comment
)]
system: Vec<String>,
#[arg(short, long, default_value = "off", default_missing_value = "stderr")]
/// Send a log stream to a specific destination.
///
/// Supported formats:
/// - `--logs=msg>messaging.log`: the log channel `msg` will be routed to the
/// file `messaging.log`. If this is used, the following format should also
/// appear to specify what happens to other categories
/// - `--logs=orchid.log`: all unspecified channels will be routed to the file
/// `orchid.log`
/// - `--logs` only once with no value: all unspecified channels will be
/// routed to stderr
///
/// Some destination names have special meanings:
/// - `stderr` designates the platform-specific standard error output of the
/// interpreter process
/// - `off` discards received messages
///
/// Defaults for specific channels
/// - `warn` is routed the same as `debug`
/// - `error` is routed the same as `warn`
/// - `msg` is discarded (routed to `off`)
#[arg(
short,
long,
default_value = "off",
default_missing_value = "stderr",
next_line_help = true,
verbatim_doc_comment
)]
logs: Vec<String>,
#[command(subcommand)]
command: Commands,
/// Measure and report the timings of various events
#[arg(long, action)]
time: bool,
/// Project folder for subcommand-specific purpose
#[arg(long)]
proj: Option<Utf8PathBuf>,
/// Number of execution steps the interpreter is allowed to take. Use
/// `--no-gas` to disable the limit.
#[arg(long, default_value("10000"), next_line_help = true, verbatim_doc_comment)]
gas: u64,
/// Disable gas limiting, may cause infinite loops
#[arg(long, action)]
no_gas: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Tokenize some code using the current lexer plugins. Either --file or
/// --line, but not both, can specify the source code input
#[command(next_line_help = true, verbatim_doc_comment)]
Lex {
#[arg()]
file: Utf8PathBuf,
/// Source file input
#[arg(long)]
file: Option<Utf8PathBuf>,
/// Raw source code input
#[arg(long)]
line: Option<String>,
},
/// Parse some code into module tree - the stage after tokenization but before
/// any execution
#[command(next_line_help = true, verbatim_doc_comment)]
Parse {
#[arg(short, long)]
file: Utf8PathBuf,
},
/// Open an interactive shell
Repl,
/// Print the module tree after parsing. This is similar to `parse`, but it
/// can traverse folders and show extension modules
#[command(next_line_help = true, verbatim_doc_comment)]
ModTree {
#[arg(long)]
proj: Option<Utf8PathBuf>,
/// Module to show
#[arg(long)]
prefix: Option<String>,
},
Exec {
#[arg(long)]
proj: Option<Utf8PathBuf>,
/// Evaluate an expression. The most obvious use case is to read project
/// metadata from scripts written in other languages. If proj is set, the
/// expression can refer to constants within this project by fully qualified
/// path
#[command(next_line_help = true, verbatim_doc_comment)]
Eval {
/// Expression to evaluate
#[arg()]
code: String,
},
/// Execute effectful Orchid code
Exec {
/// Entrypoint or startup command
#[arg(long)]
main: String,
},
}
static mut STARTUP: Option<Instant> = None;
@@ -106,6 +194,12 @@ fn get_all_extensions<'a>(
}
try_stream(async |mut cx| {
for ext_path in args.extension.iter() {
let Some(file_name) = ext_path.file_name() else {
return Err(io::Error::new(
std::io::ErrorKind::IsADirectory,
format!("Extensions are always files, but {ext_path} points at a directory"),
));
};
let init = if cfg!(windows) {
if ext_path.with_extension("dll").exists() {
ext_dylib(ext_path.with_extension("dll").as_std_path(), ctx.clone()).await.unwrap()
@@ -114,12 +208,15 @@ fn get_all_extensions<'a>(
} else {
return Err(not_found_error(ext_path));
}
} else if ext_path.with_extension("so").exists() {
ext_dylib(ext_path.with_extension("so").as_std_path(), ctx.clone()).await.unwrap()
} else if ext_path.exists() {
ext_command(Command::new(ext_path.as_os_str()), ctx.clone()).await?
} else {
return Err(not_found_error(ext_path));
let lib_path = ext_path.with_file_name(format!("lib{file_name}.so"));
if lib_path.exists() {
ext_dylib(lib_path.as_std_path(), ctx.clone()).await.unwrap()
} else if ext_path.exists() {
ext_command(Command::new(ext_path.as_os_str()), ctx.clone()).await?
} else {
return Err(not_found_error(ext_path));
}
};
cx.emit(Extension::new(init, ctx.clone()).await?).await;
}
@@ -174,311 +271,245 @@ impl Spawner for SpawnerImpl {
}
}
#[tokio::main]
async fn main() -> io::Result<ExitCode> {
eprintln!("Orcx v0.1 is free software provided without warranty.");
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 logger = get_logger(&args);
let logger2 = logger.clone();
unsafe { STARTUP = Some(Instant::now()) };
let ctx = Ctx::new(SpawnerImpl, logger2);
let (signal_end_main, on_end_main) = futures::channel::oneshot::channel();
let ctx1 = ctx.clone();
local_set.spawn_local(async move {
let ctx = &ctx1;
with_stash(async move {
let extensions =
get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap();
time_print(&args, "Extensions loaded");
match args.command {
Commands::Lex { file } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut file = File::open(file.as_std_path()).unwrap();
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
match lex(is(&buf).await, sym!(usercode), &systems, ctx).await {
Ok(lexemes) =>
println!("{}", take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true)),
Err(e) => {
eprintln!("{e}");
exit_code1.replace(ExitCode::FAILURE);
},
}
},
Commands::Parse { file } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut file = File::open(file.as_std_path()).unwrap();
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
let lexemes = lex(is(&buf).await, sym!(usercode), &systems, ctx).await.unwrap();
let Some(first) = lexemes.first() else {
println!("File empty!");
return;
};
let pctx = HostParseCtxImpl { systems: &systems, ctx: ctx.clone(), src: sym!(usercode) };
let snip = Snippet::new(first, &lexemes);
match with_reporter(parse_items(&pctx, Substack::Bottom, snip)).await.unwrap() {
Err(errv) => {
eprintln!("{errv}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
},
Ok(ptree) if ptree.is_empty() => {
eprintln!("File empty only after parsing, but no errors were reported");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
},
Ok(ptree) =>
for item in ptree {
println!("{}", take_first(&item.print(&FmtCtxImpl::default()).await, true))
},
};
},
Commands::Repl => {
let mut counter = 0;
let mut imports = Vec::new();
let usercode_path = sym!(usercode);
let mut stdin = BufReader::new(stdin());
loop {
counter += 1;
let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap();
print!("\\.> ");
std::io::stdout().flush().unwrap();
let mut prompt = String::new();
stdin.read_line(&mut prompt).await.unwrap();
let name = is(&format!("_{counter}")).await;
let path = usercode_path.suffix([name.clone()]).await;
let mut lexemes =
lex(is(prompt.trim()).await, path.clone(), &systems, ctx).await.unwrap();
let Some(discr) = lexemes.first() else { continue };
writeln!(
log("debug"),
"lexed: {}",
take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true)
)
.await;
let prefix_sr = SrcRange::zw(path.clone(), 0);
let process_lexemes = async |lexemes: &[ParsTokTree]| {
let snippet = Snippet::new(&lexemes[0], lexemes);
let parse_ctx =
HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] };
match try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
.await
{
Ok(items) => Some(items),
Err(e) => {
eprintln!("{e}");
None
},
}
};
let add_imports = |items: &mut Vec<Item>, imports: &[Import]| {
items
.extend(imports.iter().map(|import| Item::new(import.sr.clone(), import.clone())));
};
if discr.is_kw(is("import").await) {
let Some(import_lines) = process_lexemes(&lexemes).await else { continue };
imports.extend(import_lines.into_iter().map(|it| match it.kind {
ItemKind::Import(imp) => imp,
_ => panic!("Expected imports from import line"),
}));
continue;
}
if !discr.is_kw(is("let").await) {
let prefix = [is("export").await, is("let").await, name.clone(), is("=").await];
lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone())));
}
let Some(mut new_lines) = process_lexemes(&lexemes).await else { continue };
let const_decl = new_lines.iter().exactly_one().expect("Multiple lines from let");
let input_sr = const_decl.sr.map_range(|_| 0..0);
let const_name = match &const_decl.kind {
ItemKind::Member(ParsedMember { name: const_name, .. }) => const_name.clone(),
_ => panic!("Expected exactly one constant declaration from let"),
};
add_imports(&mut new_lines, &imports);
imports.push(Import::new(
input_sr.clone(),
VPath::new(path.segs()),
const_name.clone(),
));
let new_module = ParsedModule::new(true, new_lines);
match with_reporter(root.add_parsed(&new_module, path.clone())).await {
Ok(new) => root = new,
Err(errv) => {
eprintln!("{errv}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
}
eprintln!("parsed");
let entrypoint =
ExprKind::Const(path.suffix([const_name.clone()]).await).at(input_sr.pos());
let mut xctx = ExecCtx::new(root.clone(), entrypoint).await;
eprintln!("executed");
xctx.set_gas(Some(1000));
match xctx.execute().await {
ExecResult::Value(val, _) => {
println!(
"{const_name} = {}",
take_first(&val.print(&FmtCtxImpl::default()).await, false)
)
},
ExecResult::Err(e, _) => println!("error: {e}"),
ExecResult::Gas(_) => println!("Ran out of gas!"),
}
}
},
Commands::ModTree { proj, prefix } => {
let (mut root, _systems) = init_systems(&args.system, &extensions).await.unwrap();
if let Some(proj_path) = proj {
let path = proj_path.into_std_path_buf();
match try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())).await {
Ok(r) => root = r,
Err(e) => {
eprintln!("{e}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
}
}
let prefix = match prefix {
Some(pref) => VPath::parse(&pref).await,
None => VPath::new([]),
};
let root_data = root.0.read().await;
print_mod(&root_data.root, prefix, &root_data).await;
async fn print_mod(module: &Module, path: VPath, root: &RootData) {
let indent = " ".repeat(path.len());
for (key, tgt) in &module.imports {
match tgt {
Ok(tgt) => println!("{indent}import {key} => {}", tgt.target),
Err(opts) => println!(
"{indent}import {key} conflicts between {}",
opts.iter().map(|i| &i.target).join(" ")
),
}
}
for (key, mem) in &module.members {
let new_path = path.clone().name_with_suffix(key.clone()).to_sym().await;
match mem.kind(root.ctx.clone(), &root.consts).await {
MemberKind::Module(module) => {
println!("{indent}module {key} {{");
print_mod(module, VPath::new(new_path.segs()), root).boxed_local().await;
println!("{indent}}}")
},
MemberKind::Const => {
let value = root.consts.get(&new_path).expect("Missing const!");
println!("{indent}const {key} = {}", fmt(value).await)
},
}
}
}
},
Commands::Exec { proj, code } => {
let path = sym!(usercode);
let prefix_sr = SrcRange::zw(path.clone(), 0);
let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap();
if let Some(proj_path) = proj {
let path = proj_path.into_std_path_buf();
match try_with_reporter(parse_folder(&root, path, sym!(src), ctx.clone())).await {
Ok(r) => root = r,
Err(e) => {
eprintln!("{e}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
}
}
let mut lexemes = match lex(is(code.trim()).await, path.clone(), &systems, ctx).await {
Ok(lexemes) => {
writeln!(
log("debug"),
"lexed: {}",
fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")
)
.await;
lexemes
},
Err(e) => {
eprintln!("{e}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
};
let parse_ctx =
HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems: &systems[..] };
let prefix = [is("export").await, is("let").await, is("entrypoint").await, is("=").await];
lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone())));
let snippet = Snippet::new(&lexemes[0], &lexemes);
let entrypoint = match try_with_reporter(parse_item(
&parse_ctx,
Substack::Bottom,
vec![],
snippet,
))
.await
{
Ok(items) => ParsedModule::new(true, items),
Err(e) => {
eprintln!("{e}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
};
let root = match with_reporter(root.add_parsed(&entrypoint, path.clone())).await {
Err(e) => {
eprintln!("{e}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
return;
},
Ok(new_root) => new_root,
};
let expr = ExprKind::Const(sym!(usercode::entrypoint)).at(prefix_sr.pos());
let mut xctx = ExecCtx::new(root.clone(), expr).await;
xctx.set_gas(Some(10_000));
match xctx.execute().await {
ExecResult::Value(val, _) => {
println!("{}", take_first(&val.print(&FmtCtxImpl::default()).await, false))
},
ExecResult::Err(e, _) => println!("error: {e}"),
ExecResult::Gas(_) => println!("Ran out of gas!"),
}
},
};
})
.await;
signal_end_main.send(()).expect("cleanup should still be waiting");
});
let cleanup = async {
if on_end_main.await.is_err() {
return;
}
tokio::time::sleep(Duration::from_secs(2)).await;
let mut extensions = HashMap::new();
let systems = ctx.systems.read().await.values().filter_map(|v| v.upgrade()).collect_vec();
let exprs = ctx.exprs.iter().collect_vec();
for system in &systems {
extensions.insert(system.ext().name().clone(), system.ext().clone());
}
if extensions.is_empty() && systems.is_empty() && exprs.is_empty() {
return;
}
eprintln!("Shutdown is taking long. The following language constructs are still live:");
eprintln!("Extensions: {}", extensions.keys().join(", "));
for sys in &systems {
eprintln!("System: {:?} = {}", sys.id(), sys.ctor().name())
}
for (rc, expr) in &exprs {
eprintln!("{rc}x {:?} = {}", expr.id(), fmt(expr).await)
}
std::process::abort()
};
futures::future::select(
pin!(cleanup),
pin!(with_interner(local_interner(), with_logger(logger, local_set))),
)
.await;
let x = *exit_code.borrow();
Ok(x)
#[derive(Debug, Default)]
struct StdIoSystem;
impl ox::SystemCard for StdIoSystem {
type Ctor = Self;
type Req = Never;
fn atoms() -> impl IntoIterator<Item = Option<Box<dyn ox::AtomOps>>> { [] }
}
impl ox::SystemCtor for StdIoSystem {
const NAME: &'static str = "orcx::stdio";
const VERSION: f64 = 0.0;
type Card = Self;
type Deps = ();
type Instance = Self;
fn inst(&self, _: <Self::Deps as orchid_extension::DepDef>::Sat) -> Self::Instance { Self }
}
impl ox::System for StdIoSystem {
type Ctor = Self;
async fn request(&self, _: Box<dyn ReqHandle>, req: ox::ReqForSystem<Self>) -> Receipt {
match req {}
}
async fn env(&self) -> Vec<ox::tree::GenMember> {
// TODO: this is impractical, try dialogue interface eg. prompt, choice, println
ox::tree::module(true, "stdio", [
ox::tree::fun(true, "get_stdin", async |cb: ox::Expr| {
ox::cmd(async move || {
Some(ox::gen_expr::call(cb.clone(), OrcReader(tokio::io::stdin().compat())))
})
}),
ox::tree::fun(true, "get_stdout", async |cb: ox::Expr| {
ox::cmd(async move || {
Some(ox::gen_expr::call(cb.clone(), OrcWriter(tokio::io::stdout().compat_write())))
})
}),
ox::tree::fun(true, "get_stderr", async |cb: ox::Expr| {
ox::cmd(async move || {
Some(ox::gen_expr::call(cb.clone(), OrcWriter(tokio::io::stderr().compat_write())))
})
}),
])
}
}
async fn load_proj_if_set(root: &mut Root, args: &Args, ctx: &Ctx) -> Result<(), String> {
if let Some(proj_path) = &args.proj {
let path = proj_path.clone().into_std_path_buf();
*root = try_with_reporter(parse_folder(root, path, sym!(src), ctx.clone()))
.await
.map_err(|e| e.to_string())?
}
Ok(())
}
async fn add_const_at(
root: &mut Root,
ctx: &Ctx,
systems: &[System],
path: Sym,
value: IStr,
) -> Result<(), String> {
let (name, parent) = path.split_last_seg();
let path = Sym::new(parent.iter().cloned())
.await
.map_err(|_| format!("Const path must have two segments, found {path}"))?;
let prefix_sr = SrcRange::zw(path.clone(), 0);
let mut lexemes =
lex(value.clone(), path.clone(), systems, ctx).await.map_err(|e| e.to_string())?;
writeln!(log("debug"), "lexed: {}", fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")).await;
let parse_ctx = HostParseCtxImpl { ctx: ctx.clone(), src: path.clone(), systems };
let prefix = [is("export").await, is("let").await, name, is("=").await];
lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone())));
let snippet = Snippet::new(&lexemes[0], &lexemes);
let items = try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
.await
.map_err(|e| e.to_string())?;
let entrypoint = ParsedModule::new(true, items);
*root =
with_reporter(root.add_parsed(&entrypoint, path.clone())).await.map_err(|e| e.to_string())?;
Ok(())
}
fn main() -> io::Result<ExitCode> {
// Use a 10MB stack for single-threaded, unoptimized operation
stacker::grow(10 * 1024 * 1024, || {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
let args = Args::parse();
eprintln!("Orcx v{} is free software provided without warranty.", clap::crate_version!());
let exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS));
let local_set = LocalSet::new();
let exit_code1 = exit_code.clone();
let logger = get_logger(&args);
let logger2 = logger.clone();
unsafe { STARTUP = Some(Instant::now()) };
let ctx = Ctx::new(SpawnerImpl, logger2);
let (signal_end_main, on_end_main) = futures::channel::oneshot::channel();
let ctx1 = ctx.clone();
local_set.spawn_local(async move {
let ctx = &ctx1;
let res = with_stash(async move {
let mut extensions =
get_all_extensions(&args, ctx).try_collect::<Vec<Extension>>().await.unwrap();
time_print(&args, "Extensions loaded");
match &args.command {
Commands::Lex { file, line } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut buf = String::new();
match (&file, &line) {
(Some(file), None) => {
let mut file = File::open(file.as_std_path()).unwrap();
file.read_to_string(&mut buf).unwrap();
},
(None, Some(line)) => buf = line.clone(),
(None, None) | (Some(_), Some(_)) =>
return Err("`lex` expected exactly one of --file and --line".to_string()),
};
let lexemes = lex(is(&buf).await, sym!(usercode), &systems, ctx)
.await
.map_err(|e| e.to_string())?;
println!("{}", take_first(&ttv_fmt(&lexemes, &FmtCtxImpl::default()).await, true))
},
Commands::Parse { file } => {
let (_, systems) = init_systems(&args.system, &extensions).await.unwrap();
let mut file = File::open(file.as_std_path()).unwrap();
let mut buf = String::new();
file.read_to_string(&mut buf).unwrap();
let lexemes = lex(is(&buf).await, sym!(usercode), &systems, ctx).await.unwrap();
let first = lexemes.first().ok_or("File empty!".to_string())?;
let pctx =
HostParseCtxImpl { systems: &systems, ctx: ctx.clone(), src: sym!(usercode) };
let snip = Snippet::new(first, &lexemes);
let ptree = try_with_reporter(parse_items(&pctx, Substack::Bottom, snip))
.await
.map_err(|e| e.to_string())?;
if ptree.is_empty() {
return Err(
"File empty only after parsing, but no errors were reported".to_string(),
);
}
for item in ptree {
println!("{}", fmt(&item).await)
}
},
Commands::Repl => repl(&args, &extensions, ctx.clone()).await?,
Commands::ModTree { prefix } => {
let (mut root, _systems) = init_systems(&args.system, &extensions).await.unwrap();
load_proj_if_set(&mut root, &args, ctx).await?;
let prefix = match prefix {
Some(pref) => VPath::parse(pref).await,
None => VPath::new([]),
};
let root_data = root.0.read().await;
print_mod::print_mod(&root_data.root, prefix, &root_data).await;
},
Commands::Eval { code } => {
let path = sym!(usercode::entrypoint);
let (mut root, systems) = init_systems(&args.system, &extensions).await.unwrap();
load_proj_if_set(&mut root, &args, ctx).await?;
add_const_at(&mut root, ctx, &systems[..], path.clone(), is(code).await).await?;
let expr = ExprKind::Const(path.clone()).at(SrcRange::zw(path.clone(), 0).pos());
let mut xctx = ExecCtx::new(root.clone(), expr).await;
if !args.no_gas {
xctx.set_gas(Some(args.gas));
}
match xctx.execute().await {
ExecResult::Value(val, _) => println!("{}", fmt(&val).await),
ExecResult::Err(e, _) => println!("error: {e}"),
ExecResult::Gas(_) => println!("Exceeded gas limit of {}", args.gas),
}
},
Commands::Exec { main } => {
let path = sym!(usercode::entrypoint);
let (mut root, mut systems) = init_systems(&args.system, &extensions).await.unwrap();
let io_ext_builder = ox::ExtensionBuilder::new("orcx::stdio").system(StdIoSystem);
let io_ext_init = ext_inline(io_ext_builder, ctx.clone()).await;
let io_ext =
Extension::new(io_ext_init, ctx.clone()).await.map_err(|e| e.to_string())?;
let io_ctor = (io_ext.system_ctors().find(|ctor| ctor.name() == "orcx::stdio"))
.expect("Missing io system ctor");
let (io_root, io_system) = io_ctor.run(vec![]).await;
root = root.merge(&io_root).await.expect("Failed to merge stdio root into tree");
systems.push(io_system);
extensions.push(io_ext);
let mut crun = CmdRunner::new(&mut root, ctx.clone(), []).await;
systems.push(crun.sys().clone());
load_proj_if_set(&mut root, &args, ctx).await?;
add_const_at(&mut root, ctx, &systems[..], path.clone(), is(main).await).await?;
crun.push(ExprKind::Const(path.clone()).at(SrcRange::zw(path.clone(), 0).pos()));
if !args.no_gas {
crun.set_gas(args.gas);
}
match crun.execute(&root).await {
CmdEvent::Exit | CmdEvent::Settled => (),
CmdEvent::Err(e) => println!("error: {e}"),
CmdEvent::Gas => println!("Exceeded gas limit of {}", args.gas),
CmdEvent::NonCommand(val) => {
println!("Non-command value: {}", fmt(&val).await)
},
}
},
};
Ok(())
})
.await;
if let Err(s) = res {
eprintln!("{s}");
*exit_code1.borrow_mut() = ExitCode::FAILURE;
}
signal_end_main.send(()).expect("cleanup should still be waiting");
});
let cleanup = async {
if on_end_main.await.is_err() {
return;
}
tokio::time::sleep(Duration::from_secs(2)).await;
let mut extensions = HashMap::new();
let systems = ctx.systems.read().await.values().filter_map(|v| v.upgrade()).collect_vec();
let exprs = ctx.exprs.iter().collect_vec();
for system in &systems {
extensions.insert(system.ext().name().clone(), system.ext().clone());
}
if extensions.is_empty() && systems.is_empty() && exprs.is_empty() {
return;
}
eprintln!("Shutdown is taking long. The following language constructs are still live:");
eprintln!("Extensions: {}", extensions.keys().join(", "));
for sys in &systems {
eprintln!("System: {:?} = {}", sys.id(), sys.ctor().name())
}
for (rc, expr) in &exprs {
eprintln!("{rc}x {:?} = {}", expr.id(), fmt(expr).await)
}
std::process::abort()
};
futures::future::select(
pin!(cleanup),
pin!(with_interner(local_interner(), with_logger(logger, local_set))),
)
.await;
let x = *exit_code.borrow();
Ok(x)
})
})
}

View File

@@ -3,10 +3,7 @@ use std::path::{Path, PathBuf};
use futures::FutureExt;
use itertools::Itertools;
use orchid_base::SrcRange;
use orchid_base::Sym;
use orchid_base::Snippet;
use orchid_base::{OrcRes, async_io_err, is, os_str_to_string, report};
use orchid_base::{OrcRes, Snippet, SrcRange, Sym, async_io_err, is, os_str_to_string, report};
use orchid_host::ctx::Ctx;
use orchid_host::lex::lex;
use orchid_host::parse::{HostParseCtxImpl, parse_items};

31
orcx/src/print_mod.rs Normal file
View File

@@ -0,0 +1,31 @@
use futures::FutureExt;
use itertools::Itertools;
use orchid_base::{NameLike, VPath, fmt};
use orchid_host::tree::{MemKind, Mod, RootData};
pub async fn print_mod(module: &Mod, path: VPath, root: &RootData) {
let indent = " ".repeat(path.len());
for (key, tgt) in &module.imports {
match tgt {
Ok(tgt) => println!("{indent}import {key} => {}", tgt.target),
Err(opts) => println!(
"{indent}import {key} conflicts between {}",
opts.iter().map(|i| &i.target).join(" ")
),
}
}
for (key, mem) in &module.members {
let new_path = path.clone().name_with_suffix(key.clone()).to_sym().await;
match mem.kind(root.ctx.clone(), &root.consts).await {
MemKind::Module(module) => {
println!("{indent}module {key} {{");
print_mod(module, VPath::new(new_path.segs()), root).boxed_local().await;
println!("{indent}}}")
},
MemKind::Const => {
let value = root.consts.get(&new_path).expect("Missing const!");
println!("{indent}const {key} = {}", fmt(value).await)
},
}
}
}

114
orcx/src/repl.rs Normal file
View File

@@ -0,0 +1,114 @@
use std::io::Write;
use itertools::Itertools;
use orchid_base::{
FmtCtxImpl, FmtTTV, Format, Import, NameLike, Snippet, SrcRange, Token, VPath, fmt, is, log, sym,
take_first, try_with_reporter, with_reporter,
};
use orchid_host::ctx::Ctx;
use orchid_host::execute::{ExecCtx, ExecResult};
use orchid_host::expr::ExprKind;
use orchid_host::extension::Extension;
use orchid_host::lex::lex;
use orchid_host::parse::{HostParseCtxImpl, parse_item};
use orchid_host::parsed::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule};
use orchid_host::system::init_systems;
use substack::Substack;
use tokio::io::{AsyncBufReadExt, BufReader, stdin};
use crate::Args;
use crate::print_mod::print_mod;
pub async fn repl(args: &Args, extensions: &[Extension], ctx: Ctx) -> Result<(), String> {
eprintln!("Orchid REPL. Commands are prefixed with `:`, use `:help` to learn about the REPL.");
let mut counter = 0;
let mut imports = Vec::new();
let usercode_path = sym!(usercode);
let mut stdin = BufReader::new(stdin());
let (mut root, systems) = init_systems(&args.system, extensions).await.unwrap();
loop {
counter += 1;
print!("\\.> ");
std::io::stdout().flush().unwrap();
let mut prompt = String::new();
stdin.read_line(&mut prompt).await.unwrap();
if let Some(cmdline) = prompt.trim().strip_prefix(":") {
if cmdline == "help" {
println!(
"All lines not prefixed with `:` are interpreted as Orchid code.\n\
Orchid lines are executed in distinct modules to avoid namespace clutter.\n\
Use `let <name> = <value>` to define a constant\n\
All other lines are \n\
\n\
Recognized commands prefixed with `:` are:\n\
- :help print this message\n\
- :modtree display the current module tree\n\
- :exit close the REPL"
)
} else if cmdline == "exit" {
break Ok(());
} else if cmdline == "modtree" {
let root_data = root.0.read().await;
print_mod(&root_data.root, VPath::new([]), &root_data).await
} else {
println!("Could not recognize command. Valid commands are: help")
}
continue;
}
let name = is(&format!("_{counter}")).await;
let path = usercode_path.suffix([name.clone()]).await;
let mut lexemes = lex(is(prompt.trim()).await, path.clone(), &systems, &ctx).await.unwrap();
let Some(discr) = lexemes.first() else { continue };
writeln!(log("debug"), "lexed: {}", fmt(&FmtTTV(&lexemes)).await).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[..] };
try_with_reporter(parse_item(&parse_ctx, Substack::Bottom, vec![], snippet))
.await
.map_err(|e| e.to_string())
};
let add_imports = |items: &mut Vec<Item>, imports: &[Import]| {
items.extend(imports.iter().map(|import| Item::new(import.sr.clone(), import.clone())));
};
if discr.is_kw(is("import").await) {
let import_lines = process_lexemes(&lexemes).await?;
imports.extend(import_lines.into_iter().map(|it| match it.kind {
ItemKind::Import(imp) => imp,
_ => panic!("Expected imports from import line"),
}));
continue;
}
let mut prefix = vec![is("export").await];
if !discr.is_kw(is("let").await) {
prefix.extend([is("let").await, name.clone(), is("=").await]);
}
lexemes.splice(0..0, prefix.into_iter().map(|n| Token::Name(n).at(prefix_sr.clone())));
let mut new_lines = process_lexemes(&lexemes).await?;
let const_decl = new_lines.iter().exactly_one().expect("Multiple lines from let");
let input_sr = const_decl.sr.map_range(|_| 0..0);
let const_name = match &const_decl.kind {
ItemKind::Member(ParsedMember { name: const_name, .. }) => const_name.clone(),
_ => panic!("Expected exactly one constant declaration from let"),
};
add_imports(&mut new_lines, &imports);
imports.push(Import::new(input_sr.clone(), VPath::new(path.segs()), const_name.clone()));
let new_module = ParsedModule::new(true, new_lines);
root =
with_reporter(root.add_parsed(&new_module, path.clone())).await.map_err(|e| e.to_string())?;
let entrypoint = ExprKind::Const(path.suffix([const_name.clone()]).await).at(input_sr.pos());
let mut xctx = ExecCtx::new(root.clone(), entrypoint).await;
xctx.set_gas(Some(1000));
match xctx.execute().await {
ExecResult::Value(val, _) => {
println!(
"let {const_name} = {}",
take_first(&val.print(&FmtCtxImpl::default()).await, true)
)
},
ExecResult::Err(e, _) => println!("error: {e}"),
ExecResult::Gas(_) => println!("Ran out of gas!"),
}
}
}

View File

@@ -18,6 +18,7 @@ overflow_delimited_expr = true
use_small_heuristics = "Max"
fn_single_line = true
where_single_line = true
format_code_in_doc_comments = false
# literals
hex_literal_case = "Lower"

Some files were not shown because too many files have changed in this diff Show More