macros, vm, bytecode: add #[handler] await-style primop macro

This commit is contained in:
2026-08-23 17:41:50 +08:00
parent 89f5d84009
commit ac7574565d
16 changed files with 1770 additions and 610 deletions
+33 -25
View File
@@ -152,13 +152,15 @@ pub enum Continuation {
PAdd,
PAddErrorContext,
PAll,
PAllCallPred,
PAllCheck,
PAll0,
PAll1,
PAll2,
PAll3,
PAny,
PAnyCallPred,
PAnyCheck,
PAny0,
PAny1,
PAny2,
PAny3,
PAppendContext,
PAppendContextLoop,
@@ -182,9 +184,12 @@ pub enum Continuation {
PConcatStringsSep,
PConvertHash,
PDeepSeq,
PDeepSeqPush,
PDeepSeqLoop,
PDeepSeq0,
PDeepSeq1,
PDeepSeq2,
PDeepSeq3,
PDeepSeq4,
PDeepSeq5,
PDerivation,
PDerivationStrict,
@@ -198,19 +203,21 @@ pub enum Continuation {
PFetchTree,
PFetchUrl,
PFilterForceList,
PFilterSetupStack,
PFilterCallPred,
PFilterCheck,
PFilter0,
PFilter1,
PFilter2,
PFilter3,
PFilter4,
PFilterSource,
PFindFile,
PFloor,
PFoldlStrict,
PFoldlStrictEmpty,
PFoldlStrictCall1,
PFoldlStrictCall2,
PFoldlStrictUpdate,
PFoldlStrict0,
PFoldlStrict1,
PFoldlStrict2,
PFoldlStrict3,
PFoldlStrict4,
PFoldlStrict5,
PFromJSON,
PFromTOML,
PFunctionArgs,
@@ -258,7 +265,8 @@ pub enum Continuation {
PReadFileType,
PRemoveAttrs,
PReplaceStrings,
PSeq,
PSeq0,
PSeq1,
PSort,
PSplit,
PSplitVersion,
@@ -319,8 +327,8 @@ impl Continuation {
Abort => Self::PAbort,
Add => Self::PAdd,
AddErrorContext => Self::PAddErrorContext,
All => Self::PAll,
Any => Self::PAny,
All => Self::PAll0,
Any => Self::PAny0,
AppendContext => Self::PAppendContext,
AttrNames => Self::PAttrNames,
AttrValues => Self::PAttrValues,
@@ -336,7 +344,7 @@ impl Continuation {
ConcatMap => Self::PConcatMap,
ConcatStringsSep => Self::PConcatStringsSep,
ConvertHash => Self::PConvertHash,
DeepSeq => Self::PDeepSeq,
DeepSeq => Self::PDeepSeq0,
Derivation => Self::PDerivation,
DerivationStrict => Self::PDerivationStrict,
DirOf => Self::PDirOf,
@@ -348,11 +356,11 @@ impl Continuation {
FetchTarball => Self::PFetchTarball,
FetchTree => Self::PFetchTree,
FetchUrl => Self::PFetchUrl,
Filter => Self::PFilterForceList,
Filter => Self::PFilter0,
FilterSource => Self::PFilterSource,
FindFile => Self::PFindFile,
Floor => Self::PFloor,
FoldlStrict => Self::PFoldlStrict,
FoldlStrict => Self::PFoldlStrict0,
FromJSON => Self::PFromJSON,
FromTOML => Self::PFromTOML,
FunctionArgs => Self::PFunctionArgs,
@@ -396,7 +404,7 @@ impl Continuation {
RemoveAttrs => Self::PRemoveAttrs,
ReplaceStrings => Self::PReplaceStrings,
ScopedImport => Self::PScopedImport,
Seq => Self::PSeq,
Seq => Self::PSeq0,
Sort => Self::PSort,
Split => Self::PSplit,
SplitVersion => Self::PSplitVersion,
+2 -2
View File
@@ -466,7 +466,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
body: GhostRoIrRef<'id, 'ir>,
}
let (ret, thunks) = ctx.with_thunk_scope(|ctx| {
let (ret, thunks) = ctx.with_thunk_scope(|ctx| -> Result<Ret> {
let (param, body) = match raw_param {
ast::Param::IdentParam(id) => {
let param_sym = ctx.intern_string(id.to_string());
@@ -506,7 +506,7 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo
}
};
Result::Ok(Ret { param, body })
Ok(Ret { param, body })
});
let Ret { param, body } = ret?;
+1 -1
View File
@@ -4,7 +4,7 @@ use std::sync::Arc;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
pub type Result<T> = core::result::Result<T, Box<Error>>;
pub type Result<T, E = Box<Error>> = core::result::Result<T, E>;
#[derive(Clone, Debug)]
pub enum SourceType {
+1 -1
View File
@@ -9,7 +9,7 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "3.0", features = ["full", "visit-mut"] }
syn = { version = "3.0", features = ["full", "visit", "visit-mut"] }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+10
View File
@@ -1,5 +1,15 @@
extern crate proc_macro;
mod handler;
#[proc_macro_attribute]
pub fn handler(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
handler::handler(attr.into(), item.into()).into()
}
// Adapted from `__unelide_lifetimes` in `gc-arena-derive`.
// Licensed under the MIT license.
// See: https://github.com/kyren/gc-arena
+2
View File
@@ -2,6 +2,7 @@ mod boxing;
mod forced;
mod host;
mod machine;
mod macro_support;
mod path_util;
mod resolve;
mod slot;
@@ -13,6 +14,7 @@ pub use fix_bytecode::{BytecodeReader, OperandData};
pub use forced::*;
pub use host::*;
pub use machine::*;
pub use macro_support::*;
pub use path_util::*;
pub use resolve::*;
pub use slot::*;
+165
View File
@@ -0,0 +1,165 @@
use std::ops::ControlFlow;
use gc_arena::Mutation;
use crate::{
Break, BytecodeReader, Forced, Machine, MachineExt, Slot, SlotContent, Step, StrictValue,
Value, ValueVariant,
};
pub trait UnwrapSlot {
type Unwrapped;
}
impl<T> UnwrapSlot for Slot<T> {
type Unwrapped = T;
}
/// Force-target protocol behind the `#[primop]` `force` form: `Value` only
/// needs WHNF, `StrictValue` *is* WHNF, and content types are checked through
/// [`Forced`] on their stored representation.
pub trait ForceTarget<'gc>: SlotContent<'gc> {
/// Type-check the stack slot refined by the previous `force(slot)`; the
/// slot is already WHNF when this runs.
fn refine_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
depth: usize,
) -> Step;
/// Pop and convert the previous suspension's result into the annotated
/// binding's stored representation.
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, <Self as SlotContent<'gc>>::Ty>;
}
impl<'gc> ForceTarget<'gc> for Value<'gc> {
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
_depth: usize,
) -> Step {
Step::Continue(())
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, Value<'gc>> {
match m.force_and_retry::<StrictValue<'gc>>(reader, mc) {
ControlFlow::Continue(v) => ControlFlow::Continue(v.relax()),
ControlFlow::Break(b) => ControlFlow::Break(b),
}
}
}
impl<'gc> ForceTarget<'gc> for StrictValue<'gc> {
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
_depth: usize,
) -> Step {
Step::Continue(())
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, StrictValue<'gc>> {
m.force_and_retry::<StrictValue<'gc>>(reader, mc)
}
}
impl<'gc, T> ForceTarget<'gc> for T
where
T: ValueVariant<'gc>,
<T as ValueVariant<'gc>>::Ty: Forced<'gc>,
{
#[inline(always)]
fn refine_check<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
depth: usize,
) -> Step {
<<T as ValueVariant<'gc>>::Ty as Forced<'gc>>::force_and_check(
m,
reader,
mc,
depth,
reader.inst_start_pc(),
)
}
#[inline(always)]
fn conv_force<M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, <T as SlotContent<'gc>>::Ty> {
m.force_and_retry::<<T as ValueVariant<'gc>>::Ty>(reader, mc)
}
}
/// Callee protocol behind `call(&slot, ..)`: an unforced `Slot<Value>` is
/// WHNF'd before the call; refined slots already hold strict values.
pub trait CalleeReady<'gc> {
fn callee_ready<M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break>;
}
impl<'gc> CalleeReady<'gc> for Slot<Value<'gc>> {
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
self.force::<StrictValue<'gc>, M>(m, reader, mc)?;
ControlFlow::Continue(())
}
}
impl<'gc> CalleeReady<'gc> for Slot<StrictValue<'gc>> {
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
ControlFlow::Continue(())
}
}
impl<'gc, T> CalleeReady<'gc> for Slot<T>
where
T: ValueVariant<'gc>,
{
#[inline(always)]
fn callee_ready<M: Machine<'gc>>(
&self,
_m: &mut M,
_reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> ControlFlow<Break> {
ControlFlow::Continue(())
}
}
+5 -6
View File
@@ -6,6 +6,11 @@ use gc_arena::Mutation;
use crate::{Break, Forced, Machine, NixType, Step, StrictValue, Value, ValueVariant};
pub struct TypeError {
pub expected: NixType,
pub got: NixType,
}
pub trait SlotContent<'gc> {
type Ty: Into<Value<'gc>> + TryFrom<Value<'gc>> + 'gc;
}
@@ -50,12 +55,6 @@ where
T::Ty::try_from(m.peek(self.depth as usize)).expect("slot held a value of the wrong type")
}
#[inline(always)]
pub fn read_checked<M: Machine<'gc>>(&self, m: &M) -> Result<T::Ty, NixType> {
let val = m.peek(self.depth as usize);
T::Ty::try_from(val).map_err(|_err| val.ty())
}
#[inline(always)]
pub fn write<M: Machine<'gc>>(&self, m: &mut M, val: T::Ty) {
m.replace(self.depth as usize, T::Ty::into(val));
+1 -1
View File
@@ -165,7 +165,7 @@ pub(crate) fn op_return<'gc, M: Machine<'gc>>(
depth: None,
});
m.inc_call_depth();
reader.set_pc(Continuation::PDeepSeq.ip() as usize);
reader.set_pc(Continuation::PDeepSeq0.ip() as usize);
return Step::Continue(());
}
}
+4
View File
@@ -20,8 +20,12 @@ use smallvec::SmallVec;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
pub use fix_runtime::*;
#[doc(hidden)]
#[path = "macro_support.rs"]
pub mod __macro_support;
mod instructions;
mod primops;
extern crate self as fix_vm;
type VmResult<T> = std::result::Result<T, VmError>;
+36
View File
@@ -0,0 +1,36 @@
//! Single macro-facing path for the `#[primop]` support surface: the traits
//! generated code type-checks through (defined in `fix_runtime`, where the
//! impl sets are coherent) and the stub trio that keeps un-expanded primop
//! sources resolving in tooling.
use std::future::Future;
pub use fix_runtime::{CalleeReady, ForceTarget, UnwrapSlot};
use fix_runtime::{Slot, Value};
macro_rules! type_hint {
() => {
if false {
return async {
unreachable!();
};
}
};
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn force<T>(_: Slot<Value<'_>>) -> impl Future<Output = Slot<T>> {
type_hint!();
panic!("stub `force` called at runtime")
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn call<T, A, R>(_: &T, _: A) -> impl Future<Output = R> {
type_hint!();
panic!("stub `call` called at runtime")
}
#[expect(clippy::panic, reason = "deliberately panic when the stub is called")]
pub fn spill<T>(_: T) -> Slot<T> {
panic!("stub `spill` called at runtime")
}
+79 -154
View File
@@ -1,22 +1,89 @@
use fix_bytecode::Continuation;
use fix_error::Error;
use fix_error::{Error, Result};
use fix_macros::handler;
use fix_runtime::{
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value,
VmRuntimeCtx, VmRuntimeCtxExt,
AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Slot, Step, StrictValue,
Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use gc_arena::{Gc, Mutation, RefLock};
use smallvec::SmallVec;
pub fn seq<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
use crate::primops::stubs::*;
#[handler(name = PSeq)]
fn seq<'gc>(mc: &Mutation<'gc>, e1: Slot<Value<'gc>>, e2: Slot<Value<'gc>>) -> Result<Value<'gc>> {
let _e1: Slot<StrictValue<'gc>> = force(e1).await?;
Ok(e2.get())
}
#[handler(name = PDeepSeq)]
fn deep_seq<'gc>(
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e2 = m.pop();
m.drop_n(1);
m.return_from_primop(e2, reader)
e1: Slot<Value<'gc>>,
e2: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let e1: Slot<StrictValue<'gc>> = force(e1).await?;
if collect_children(e1.get()).is_empty() {
return Ok(e2.get());
}
let seen: Slot<List<'gc>> = spill(Gc::new(mc, List::default()));
let worklist: Slot<List<'gc>> = spill(List::new(mc, collect_children(e1.get())));
let count: Slot<i32> = spill(worklist.get().inner.borrow().len() as i32);
loop {
if count.get() == 0 {
return Ok(e2.get());
}
let item = worklist
.get()
.unlock(mc)
.borrow_mut()
.pop()
.expect("worklist is non-empty while count > 0");
count.set(count.get() - 1);
let item: Slot<StrictValue<'gc>> = force(item).await?;
let added = push_children(mc, seen.get(), worklist.get(), item);
count.set(count.get() + added);
}
}
fn collect_children<'gc>(val: StrictValue<'gc>) -> SmallVec<[Value<'gc>; 4]> {
if let Some(attrs) = val.downcast::<AttrSet<'gc>>() {
attrs.entries.iter().map(|&(_, v)| v).collect()
} else if let Some(list) = val.downcast::<List<'gc>>() {
list.inner.borrow().iter().copied().collect()
} else {
SmallVec::new()
}
}
fn push_children<'gc>(
mc: &Mutation<'gc>,
seen: Gc<'gc, List<'gc>>,
worklist: Gc<'gc, List<'gc>>,
item: StrictValue<'gc>,
) -> i32 {
let mut added = 0i32;
if let Some(attrs) = item.downcast::<AttrSet<'gc>>()
&& !is_value_in_seen(seen, item.relax())
{
add_value_to_seen(seen, mc, item.relax());
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.entries.iter() {
wl.push(v);
}
added = attrs.entries.len() as i32;
} else if let Some(list) = item.downcast::<List<'gc>>()
&& !is_value_in_seen(seen, item.relax())
{
add_value_to_seen(seen, mc, item.relax());
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len() as i32;
}
added
}
pub fn abort<'gc, M: Machine<'gc>>(
@@ -34,148 +101,6 @@ pub fn abort<'gc, M: Machine<'gc>>(
)))
}
pub fn deep_seq_force_top<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e1, e2] - force e1, return e2
m.force_slot(1, reader, mc)?;
let e1 = m.peek_forced(1);
let children: SmallVec<_> = if let Some(attrs) = e1.downcast::<AttrSet>() {
let attrs = &attrs.entries;
if attrs.is_empty() {
SmallVec::new()
} else {
attrs.iter().map(|&(_, v)| v).collect()
}
} else if let Some(list) = e1.downcast::<List>() {
let inner = list.inner.borrow();
if inner.is_empty() {
SmallVec::new()
} else {
inner.iter().copied().collect()
}
} else {
SmallVec::new()
};
if children.is_empty() {
let e2 = m.pop();
m.drop_n(1);
return m.return_from_primop(e2, reader);
}
let count = children.len() as i32;
let seen = Gc::new(mc, List::default());
let worklist = List::new(mc, children);
let e2 = m.pop();
m.drop_n(1);
m.push(e2);
m.push(Value::new(seen));
m.push(Value::new(worklist));
m.push(Value::new(count));
reader.set_pc(Continuation::PDeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_push<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack: [e2, seen, worklist, counter]
let counter = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
if counter == 0 {
m.drop_n(3);
let val = m.pop();
return m.return_from_primop(val, reader);
}
let worklist = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list");
let item = worklist
.unlock(mc)
.borrow_mut()
.pop()
.expect("worklist is non-empty while counter > 0");
m.replace(0, Value::new(counter - 1));
m.push(item);
// force item at TOS, resume at DeepSeqLoop after force
m.force_slot_to_pc(0, reader, mc, Continuation::PDeepSeqLoop.ip() as usize)?;
reader.set_pc(Continuation::PDeepSeqLoop.ip() as usize);
Step::Continue(())
}
pub fn deep_seq_loop<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// stack after pop: [e2, seen, worklist, counter]
let item = m.pop();
let counter = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let mut added: usize = 0;
if let Some(attrs) = item.downcast::<AttrSet>() {
let attrs = &attrs.entries;
let seen = m
.peek_forced(2)
.downcast::<List>()
.expect("stack slot must be a list");
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
let worklist = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list");
{
let mut wl = worklist.unlock(mc).borrow_mut();
for &(_, v) in attrs.iter() {
wl.push(v);
}
added = attrs.len();
}
}
} else if let Some(list) = item.downcast::<List>() {
let seen = m
.peek_forced(2)
.downcast::<List>()
.expect("stack slot must be a list");
if !is_value_in_seen(seen, item) {
add_value_to_seen(seen, mc, item);
let worklist = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list");
{
let inner = list.inner.borrow();
let mut wl = worklist.unlock(mc).borrow_mut();
for &v in inner.iter() {
wl.push(v);
}
added = inner.len();
}
}
}
m.replace(0, Value::new(counter + added as i32));
reader.set_pc(Continuation::PDeepSeqPush.ip() as usize);
Step::Continue(())
}
pub fn force_result_shallow<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
+99 -379
View File
@@ -1,396 +1,116 @@
use fix_bytecode::Continuation;
use fix_runtime::{
BytecodeReader, List, Machine, MachineExt, NixType, Slot, Step, StrictValue, Value,
};
use fix_error::Result;
use fix_macros::handler;
use fix_runtime::{List, Slot, StrictValue, Value};
use gc_arena::Mutation;
use crate::slots;
use crate::primops::stubs::*;
pub mod filter {
use super::*;
#[expect(
clippy::unreachable,
reason = "dispatch_cont routes only the PFilter* continuations to this function, so the fallback arm is unreachable"
)]
pub fn dispatch<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
cont: Continuation,
) -> Step {
use Continuation::*;
match cont {
PFilterForceList => force_list(m, reader, mc),
PFilterSetupStack => setup_stack(m, reader, mc),
PFilterCallPred => call_pred(m, reader, mc),
PFilterCheck => check(m, reader, mc),
_ => unreachable!(),
#[handler(name = PFilter)]
fn filter<'gc>(
mc: &Mutation<'gc>,
pred: Slot<Value<'gc>>,
list: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let list: Slot<List<'gc>> = force(list).await?;
if list.get().inner.borrow().is_empty() {
return Ok(Value::new(list.get()));
}
let idx: Slot<i32> = spill(0i32);
let acc: Slot<List<'gc>> = spill(List::new_gc(mc));
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if keep {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let elem = list.get().inner.borrow()[idx.get() as usize];
acc.get().unlock(mc).borrow_mut().push(elem);
}
}
fn force_list<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
slots! {
list: Value;
_pred: Value;
};
list.force_to_pc(m, reader, mc, Continuation::PFilterSetupStack.ip() as usize)?;
setup_stack(m, reader, mc)
}
fn setup_stack<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
slots! {
list: List;
_pred: Value;
};
let list = match list.read_checked(m) {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
if list.inner.borrow().is_empty() {
let val = m.pop();
let _pred = m.pop();
return m.return_from_primop(val, reader);
if idx.get() as usize == list.get().inner.borrow().len() - 1 {
return Ok(Value::new(acc.get()));
}
// prepare stack layout: [ pred list idx acc ]
m.push(Value::new(0));
m.push(Value::new(List::new_gc(mc)));
reader.set_pc(Continuation::PFilterCallPred.ip() as usize);
Step::Continue(())
idx.set(idx.get() + 1);
}
}
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
fn call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
slots! {
_acc: List;
idx: i32;
list: List;
pred: Value;
};
let pred: Slot<StrictValue> = pred.force(m, reader, mc)?;
let elem = list.read(m).inner.borrow()[idx.read(m) as usize];
m.push(pred.read(m).relax());
m.call(reader, mc, elem, Continuation::PFilterCheck.ip() as usize)
#[handler(name = PAll)]
fn all<'gc>(
mc: &Mutation<'gc>,
pred: Slot<Value<'gc>>,
list: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let list: Slot<List<'gc>> = force(list).await?;
if list.get().inner.borrow().is_empty() {
return Ok(Value::new(true));
}
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
pub fn check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
slots! {
acc: List;
idx: i32;
list: List;
_pred: StrictValue;
};
let ret = m.force_and_retry::<bool>(reader, mc)?;
let list = list.read(m).as_ref().inner.borrow();
let acc = acc.read(m);
let old_idx = idx.read(m);
if ret {
let mut acc = acc.unlock(mc).borrow_mut();
acc.push(list[old_idx as usize]);
let idx: Slot<i32> = spill(0i32);
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if !keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(Value::new(keep));
}
if old_idx as usize == list.len() - 1 {
let acc = m.pop();
m.drop_n(3);
return m.return_from_primop(acc, reader);
idx.set(idx.get() + 1);
}
}
#[handler(name = PAny)]
fn any<'gc>(
mc: &Mutation<'gc>,
pred: Slot<Value<'gc>>,
list: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let list: Slot<List<'gc>> = force(list).await?;
if list.get().inner.borrow().is_empty() {
return Ok(Value::new(false));
}
let idx: Slot<i32> = spill(0i32);
loop {
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let keep: bool = call(&pred, list.get().inner.borrow()[idx.get() as usize]).await?;
if keep || idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(Value::new(keep));
}
idx.write(m, old_idx + 1);
reader.set_pc(Continuation::PFilterCallPred.ip() as usize);
Step::Continue(())
idx.set(idx.get() + 1);
}
}
// foldl' op nul list
//
// Stack layouts across phases:
// Entry: [op, nul, list]
// Empty: [op, nul]
// Call1: [op, list, idx, acc]
// Call2: [op, list, idx, acc, intermediate]
// Update: [op, list, idx, acc, result]
pub fn foldl_strict_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
#[handler(name = PFoldlStrict)]
fn foldl_strict<'gc>(
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list_val = m.peek_forced(0);
let Some(list) = list_val.downcast::<List>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
m.drop_n(1);
reader.set_pc(Continuation::PFoldlStrictEmpty.ip() as usize);
return Step::Continue(());
op: Slot<Value<'gc>>,
nul: Slot<Value<'gc>>,
list: Slot<Value<'gc>>,
) -> Result<Value<'gc>> {
let list: Slot<List<'gc>> = force(list).await?;
if list.get().inner.borrow().is_empty() {
return Ok(nul.get());
}
let list_val = m.pop();
let nul_val = m.pop();
m.push(list_val);
m.push(Value::new(0i32));
m.push(nul_val);
reader.set_pc(Continuation::PFoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn foldl_strict_empty<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let nul = m.force_and_retry::<StrictValue>(reader, mc)?;
m.drop_n(1);
m.return_from_primop(nul.relax(), reader)
}
pub fn foldl_strict_call1<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(3, reader, mc)?;
let op = m.peek_forced(3);
let acc = m.peek(0);
m.push(op.relax());
m.call(
reader,
mc,
acc,
Continuation::PFoldlStrictCall2.ip() as usize,
)
}
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
pub fn foldl_strict_call2<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let idx = m
.peek(2)
.downcast::<i32>()
.expect("stack slot must be an integer");
let list = m
.peek_forced(3)
.downcast::<List>()
.expect("stack slot must be a list");
let elem = list.inner.borrow()[idx as usize];
m.call(
reader,
mc,
elem,
Continuation::PFoldlStrictUpdate.ip() as usize,
)
}
#[expect(
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len()"
)]
pub fn foldl_strict_update<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
_mc: &Mutation<'gc>,
) -> Step {
let result = m.pop();
m.replace(0, result);
let idx = m
.peek(1)
.downcast::<i32>()
.expect("stack slot must be an integer");
let list = m
.peek_forced(2)
.downcast::<List>()
.expect("stack slot must be a list");
let len = list.inner.borrow().len();
if (idx as usize) + 1 == len {
let acc = m.pop();
m.drop_n(3);
return m.return_from_primop(acc, reader);
let idx: Slot<i32> = spill(0i32);
let acc = spill(nul.get());
loop {
let f = call(&op, acc.get()).await?;
#[expect(
clippy::indexing_slicing,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
let new_acc: StrictValue<'gc> =
call(&f, list.get().inner.borrow()[idx.get() as usize]).await?;
acc.set(new_acc.relax());
if idx.get() as usize + 1 == list.get().inner.borrow().len() {
return Ok(acc.get());
}
idx.set(idx.get() + 1);
}
m.replace(1, Value::new(idx + 1));
reader.set_pc(Continuation::PFoldlStrictCall1.ip() as usize);
Step::Continue(())
}
pub fn all_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new(true), reader);
}
// prepare stack layout: [ pred list idx ]
m.push(Value::new(0));
reader.set_pc(Continuation::PAllCallPred.ip() as usize);
Step::Continue(())
}
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
pub fn all_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pred = m.peek_forced(2);
let idx = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let elem = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list")
.inner
.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, Continuation::PAllCheck.ip() as usize)
}
#[expect(
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len()"
)]
pub fn all_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
let idx = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let list = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list");
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || !ret {
m.drop_n(3);
return m.return_from_primop(Value::new(ret), reader);
}
m.replace(0, Value::new(idx + 1));
reader.set_pc(Continuation::PAllCallPred.ip() as usize);
Step::Continue(())
}
pub fn any_entry<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
m.force_slot(0, reader, mc)?;
let list = match m.peek_forced(0).expect::<List>() {
Ok(list) => list,
Err(got) => return m.finish_type_err(NixType::List, got),
};
// FIXME: force callable
m.force_slot(1, reader, mc)?;
if list.inner.borrow().is_empty() {
let _list = m.pop();
let _pred = m.pop();
return m.return_from_primop(Value::new(false), reader);
}
// prepare stack layout: [ pred list idx ]
m.push(Value::new(0));
reader.set_pc(Continuation::PAnyCallPred.ip() as usize);
Step::Continue(())
}
#[expect(
clippy::indexing_slicing,
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len(), so it indexes the list in bounds"
)]
pub fn any_call_pred<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let pred = m.peek_forced(2);
let idx = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let elem = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list")
.inner
.borrow()[idx as usize];
m.push(pred.relax());
m.call(reader, mc, elem, Continuation::PAnyCheck.ip() as usize)
}
#[expect(
clippy::cast_sign_loss,
reason = "idx is a non-negative loop counter in 0..list.len()"
)]
pub fn any_check<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let ret = m.force_and_retry::<bool>(reader, mc)?;
let idx = m
.peek(0)
.downcast::<i32>()
.expect("stack slot must be an integer");
let list = m
.peek_forced(1)
.downcast::<List>()
.expect("stack slot must be a list");
let list = list.inner.borrow();
if idx as usize == list.len() - 1 || ret {
m.drop_n(3);
return m.return_from_primop(Value::new(ret), reader);
}
m.replace(0, Value::new(idx + 1));
reader.set_pc(Continuation::PAnyCallPred.ip() as usize);
Step::Continue(())
}
+11 -41
View File
@@ -5,6 +5,7 @@ mod eq;
mod io;
mod list;
mod path;
mod stubs;
pub use context::*;
pub use control::*;
@@ -18,27 +19,6 @@ pub use io::*;
pub use list::*;
pub use path::*;
#[macro_export]
macro_rules! slots {
{ $($ident:ident : $ty:ty);* $(;)? } => {
slots! { @acc [ 0 ] $($ident : $ty;)* }
};
/* { $($ident:ident : $ty:ty);* $(;)? } => {
slots! { @reverse [ $($ident : $ty;)* ] [] }
};
{ @reverse [ $ident:ident : $ty:ty; $($remain:tt)* ] [ $($acc:tt)* ] } => {
slots! { @reverse [ $($remain)* ] [ $ident : $ty; $($acc)* ] }
};
{ @reverse [] [ $($rev_id:ident : $rev_ty:ty;)* ] } => {
slots! { @acc [ 0 ] $($rev_id : $rev_ty;)* }
}; */
{ @acc [ $($acc:tt)* ] $ident:ident : $ty:ty; $($remain:tt)* } => {
let $ident : Slot<$ty> = Slot::new($($acc)*);
slots! { @acc [ $($acc)* + 1 ] $($remain)* }
};
{ @acc [ $($acc:tt)* ] } => {};
}
pub fn dispatch_cont<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
@@ -53,28 +33,18 @@ pub fn dispatch_cont<'gc, M: Machine<'gc>>(
match cont {
PAbort => abort(m, ctx, reader, mc),
PAll => all_entry(m, reader, mc),
PAllCallPred => all_call_pred(m, reader, mc),
PAllCheck => all_check(m, reader, mc),
PAny => any_entry(m, reader, mc),
PAnyCallPred => any_call_pred(m, reader, mc),
PAnyCheck => any_check(m, reader, mc),
PDeepSeq => deep_seq_force_top(m, reader, mc),
PDeepSeqPush => deep_seq_push(m, reader, mc),
PDeepSeqLoop => deep_seq_loop(m, reader, mc),
PSeq => seq(m, reader, mc),
PFilterForceList | PFilterSetupStack | PFilterCallPred | PFilterCheck => {
filter::dispatch(m, reader, mc, cont)
PAll0 | PAll1 | PAll2 | PAll3 => all(m, reader, mc, cont),
PAny0 | PAny1 | PAny2 | PAny3 => any(m, reader, mc, cont),
PDeepSeq0 | PDeepSeq1 | PDeepSeq2 | PDeepSeq3 | PDeepSeq4 | PDeepSeq5 => {
deep_seq(m, reader, mc, cont)
}
PSeq0 | PSeq1 => seq(m, reader, mc, cont),
PFoldlStrict => foldl_strict_entry(m, reader, mc),
PFoldlStrictEmpty => foldl_strict_empty(m, reader, mc),
PFoldlStrictCall1 => foldl_strict_call1(m, reader, mc),
PFoldlStrictCall2 => foldl_strict_call2(m, reader, mc),
PFoldlStrictUpdate => foldl_strict_update(m, reader, mc),
PFilter0 | PFilter1 | PFilter2 | PFilter3 | PFilter4 => filter(m, reader, mc, cont),
PFoldlStrict0 | PFoldlStrict1 | PFoldlStrict2 | PFoldlStrict3 | PFoldlStrict4 | PFoldlStrict5 => {
foldl_strict(m, reader, mc, cont)
}
ForceResultShallow => force_result_shallow(m, ctx, reader, mc),
ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc),
+9
View File
@@ -0,0 +1,9 @@
//! Fallback definitions for the `#[primop]` surface syntax.
//!
//! `force`, `call`, and `spill` are recognized and rewritten by the
//! `#[primop]` proc macro, so compiled code never calls them. They exist only
//! so the un-expanded source name-resolves in tooling that does not run the
//! macro (e.g. rust-analyzer while the macro crate is being rebuilt). Calling
//! one for real is impossible: they never return.
pub use crate::__macro_support::{call, force, spill};