From ac7574565d06104fd4b6308a5cf1040b846ddc19 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Sat, 22 Aug 2026 22:10:33 +0800 Subject: [PATCH] macros, vm, bytecode: add #[handler] await-style primop macro --- fix-bytecode/src/lib.rs | 58 +- fix-compiler/src/ir/downgrade.rs | 4 +- fix-error/src/lib.rs | 2 +- fix-macros/Cargo.toml | 2 +- fix-macros/src/handler.rs | 1312 ++++++++++++++++++++++++++++++ fix-macros/src/lib.rs | 10 + fix-runtime/src/lib.rs | 2 + fix-runtime/src/macro_support.rs | 165 ++++ fix-runtime/src/slot.rs | 11 +- fix-vm/src/instructions/calls.rs | 2 +- fix-vm/src/lib.rs | 4 + fix-vm/src/macro_support.rs | 36 + fix-vm/src/primops/control.rs | 233 ++---- fix-vm/src/primops/list.rs | 478 +++-------- fix-vm/src/primops/mod.rs | 52 +- fix-vm/src/primops/stubs.rs | 9 + 16 files changed, 1770 insertions(+), 610 deletions(-) create mode 100644 fix-macros/src/handler.rs create mode 100644 fix-runtime/src/macro_support.rs create mode 100644 fix-vm/src/macro_support.rs create mode 100644 fix-vm/src/primops/stubs.rs diff --git a/fix-bytecode/src/lib.rs b/fix-bytecode/src/lib.rs index 03d4d80..ac26a0e 100644 --- a/fix-bytecode/src/lib.rs +++ b/fix-bytecode/src/lib.rs @@ -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, diff --git a/fix-compiler/src/ir/downgrade.rs b/fix-compiler/src/ir/downgrade.rs index 6c378a6..9c3ea76 100644 --- a/fix-compiler/src/ir/downgrade.rs +++ b/fix-compiler/src/ir/downgrade.rs @@ -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 { 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?; diff --git a/fix-error/src/lib.rs b/fix-error/src/lib.rs index c3e07f8..da058c2 100644 --- a/fix-error/src/lib.rs +++ b/fix-error/src/lib.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; -pub type Result = core::result::Result>; +pub type Result> = core::result::Result; #[derive(Clone, Debug)] pub enum SourceType { diff --git a/fix-macros/Cargo.toml b/fix-macros/Cargo.toml index 0ec29bd..35be097 100644 --- a/fix-macros/Cargo.toml +++ b/fix-macros/Cargo.toml @@ -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 diff --git a/fix-macros/src/handler.rs b/fix-macros/src/handler.rs new file mode 100644 index 0000000..13bada9 --- /dev/null +++ b/fix-macros/src/handler.rs @@ -0,0 +1,1312 @@ +//! `#[handler]` compiles an async-style handler body into the CPS phase state +//! machine the VM dispatches via `fix_bytecode::Continuation`. +//! +//! The source fn never touches the machine: it declares `Slot>` +//! arguments (deepest stack slot first) plus optionally `mc` and `ctx`, and +//! suspends only through the macro-recognized let-statement forms +//! +//! ```ignore +//! let t: Slot = force(slot_or_expr).await?; // force a stack slot / temp value +//! let f: T = call(&callee, arg).await?; // call a closure / primop +//! let s: Slot = spill(expr); // push a value that survives suspensions +//! ``` +//! +//! Annotations name the slot handle; the macro unwraps `Slot` to the +//! content type `T` for the generated bindings (a typed `call` binding is +//! popped into a local of the content's stored type in the next phase's +//! prologue). +//! +//! The expansion splits the body at every suspension and spill site into one +//! free phase function per segment (`name_phase0..N`) plus a `name_dispatch` +//! router. Each phase preserves the invariants the hand-written code follows: +//! retry-sensitive operations (`force_and_retry`, callee WHNF forces) are the +//! first thing a phase does after its prologue, so no stack mutation can ever +//! re-execute; every other suspension resumes at the *next* phase's IP and +//! never re-runs the current one. +//! +//! Cross-suspension state lives on the value stack: arguments stay in their +//! slots (optionally refined to a typed slot by `force`), `spill` pushes new +//! permanent slots, and an untyped `call` binding keeps its result as a +//! transient slot that a following `call(&f, ..)` consumes directly. + +use proc_macro2::{TokenStream, TokenTree}; +use quote::{format_ident, quote}; +use syn::parse::Parse; +use syn::spanned::Spanned; +use syn::visit::Visit; +use syn::visit_mut::VisitMut; +use syn::{Expr, Ident, ItemFn, LocalInit, Safety, Token, Type}; + +struct HandlerAttr { + prefix: Ident, +} + +impl Parse for HandlerAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + return Err(input.error("expected `name = Ident`")); + } + let ident: Ident = input.parse()?; + if ident != "name" { + return Err(input.error("expected `name = Ident`")); + } + let _eq: Token![=] = input.parse()?; + let prefix: Ident = input.parse()?; + Ok(Self { prefix }) + } +} + +fn pat_ident(pat: &syn::Pat) -> Option { + match pat { + syn::Pat::Ident(p) => Some(p.ident.clone()), + syn::Pat::Type(t) => pat_ident(&t.pat), + _ => None, + } +} + +fn local_annotation(pat: &syn::Pat) -> Option { + match pat { + syn::Pat::Type(t) => Some((*t.ty).clone()), + _ => None, + } +} + +/// Unwrap a `Slot` type to its content type `T` +fn slot_content_ty(ty: Type) -> Type { + syn::parse_quote!(<#ty as ::fix_vm::__macro_support::UnwrapSlot>::Unwrapped) +} + +fn single_path_ident(expr: &Expr) -> Option { + match expr { + Expr::Path(p) if p.path.segments.len() == 1 => { + p.path.segments.first().map(|s| s.ident.clone()) + } + _ => None, + } +} + +/// The suspension / spill statement forms recognized at let-statement level. +enum Special { + Spill(Expr), + Force(Expr), + Call { callee: Ident, arg: Expr }, +} + +fn classify_call(expr: &Expr) -> Option<(Ident, &syn::punctuated::Punctuated)> { + match expr { + Expr::Call(call) => match &*call.func { + Expr::Path(p) if p.path.segments.len() == 1 => p + .path + .segments + .first() + .map(|s| (s.ident.clone(), &call.args)), + _ => None, + }, + _ => None, + } +} + +fn exactly_one_arg<'a>( + form: &str, + args: &'a syn::punctuated::Punctuated, +) -> syn::Result<&'a Expr> { + (args.len() == 1) + .then(|| args.first()) + .flatten() + .ok_or_else(|| syn::Error::new(args.span(), format!("`{form}` takes exactly one argument"))) +} + +fn classify_local(local: &syn::Local) -> syn::Result> { + let Some(LocalInit { expr: init, .. }) = &local.init else { + return Ok(None); + }; + + if let Some((name, args)) = classify_call(init) + && name == "spill" + { + let arg = exactly_one_arg("spill", args)?; + return Ok(Some(Special::Spill(arg.clone()))); + } + + let Some(awaited) = strip_try(init) else { + return Ok(None); + }; + let inner = match awaited { + Expr::Await(a) => &*a.base, + _ => { + return Err(syn::Error::new( + init.span(), + "`?` is only supported directly on `force(..).await` / `call(..).await`", + )); + } + }; + let Some((name, args)) = classify_call(inner) else { + return Err(syn::Error::new( + inner.span(), + "expected `force(..).await?` or `call(&callee, arg).await?`", + )); + }; + if name == "force" { + let arg = exactly_one_arg("force", args)?; + return Ok(Some(Special::Force(arg.clone()))); + } + if name == "call" && args.len() == 2 { + let Expr::Reference(r) = &args[0] else { + return Err(syn::Error::new( + args[0].span(), + "the callee must be passed by reference: call(&callee, arg)", + )); + }; + let Some(callee) = single_path_ident(&r.expr) else { + return Err(syn::Error::new( + r.expr.span(), + "the callee must be a simple identifier", + )); + }; + return Ok(Some(Special::Call { + callee, + arg: args[1].clone(), + })); + } + Err(syn::Error::new( + name.span(), + "unknown suspension form; expected force, call, or spill", + )) +} + +fn strip_try(expr: &Expr) -> Option<&Expr> { + match expr { + Expr::Try(t) => Some(&t.expr), + _ => None, + } +} + +enum Prologue { + None, + /// Pop and convert the previous suspension's result into a local. + Conv { + local: Ident, + ty: Type, + }, + /// Type-check the slot refined by the previous `force`. + Typecheck { + name: Ident, + ty: Type, + }, +} + +#[derive(Clone)] +enum Callee { + Perm(Ident), + Transient, + Local(Ident), +} + +#[expect( + clippy::large_enum_variant, + reason = "analysis-only data built once per phase during expansion" +)] +enum Term { + ForceSlot { + name: Ident, + next: usize, + }, + ForceTemp { + expr: Expr, + next: usize, + }, + Spill { + expr: Expr, + ty: Type, + next: usize, + }, + Call { + callee: Callee, + arg: Expr, + next: usize, + }, + Jump { + next: usize, + }, + BackEdge { + target: usize, + }, + TailOk(Expr), + TailErr(Expr), + TailNone, +} + +impl Term { + fn exprs(&self) -> Vec<&Expr> { + match self { + Term::ForceTemp { expr, .. } | Term::Spill { expr, .. } => vec![expr], + Term::Call { arg, .. } => vec![arg], + Term::TailOk(e) | Term::TailErr(e) => vec![e], + _ => Vec::new(), + } + } +} + +struct Phase { + stmts: Vec, + prologue: Prologue, + term: Term, + bindings: Vec, + stack_count: usize, + attrs: Vec, +} + +struct Binding { + name: Ident, + ty: Type, + depth: usize, +} + +struct SlotDef { + name: Ident, + ty: Type, + transient: bool, + pop_on_entry: bool, + created: usize, + alive: bool, + forced: bool, +} + +struct Walker { + prefix: String, + has_ctx: bool, + phases: Vec, + slots: Vec, + plain_prior: Vec, + seg: Vec, + seg_prologue: Prologue, + pending_bindings: Vec, + pending_stack_count: usize, + pending_attrs: Vec, + in_loop: bool, + ended_with_loop: bool, + errors: Vec, +} + +impl Walker { + fn err(&mut self, e: syn::Error) { + self.errors.push(e); + } + + fn find_slot(&self, name: &Ident) -> Option { + self.slots.iter().position(|s| s.alive && &s.name == name) + } + + fn alive_visible(&self) -> Vec<&SlotDef> { + self.slots + .iter() + .filter(|s| s.alive && !s.pop_on_entry) + .collect() + } + + fn refresh_pending(&mut self) { + let visible = self.alive_visible(); + let count = visible.len(); + self.pending_bindings = visible + .iter() + .enumerate() + .map(|(i, s)| Binding { + name: s.name.clone(), + ty: s.ty.clone(), + depth: count - 1 - i, + }) + .collect(); + self.pending_stack_count = count; + } + + fn close(&mut self, term: Term) { + if let (Prologue::Conv { .. }, Term::Call { callee, arg, .. }) = (&self.seg_prologue, &term) + { + let next = self.phases.len() + 1; + self.finish_phase(Term::Jump { next }); + self.seg_prologue = Prologue::None; + self.refresh_pending(); + self.finish_phase(Term::Call { + callee: callee.clone(), + arg: arg.clone(), + next: next + 1, + }); + return; + } + self.finish_phase(term); + } + + fn finish_phase(&mut self, term: Term) { + let stack_count = self.pending_stack_count; + let slot_names: Vec = self + .pending_bindings + .iter() + .map(|b| b.name.clone()) + .collect(); + + let mut stmts = std::mem::take(&mut self.seg); + let mut term = term; + let mut rewrite_errors = Vec::new(); + for stmt in &mut stmts { + rewrite_stmt(stmt, &slot_names, stack_count, &mut rewrite_errors); + } + let term_expr = match &mut term { + Term::Spill { expr, .. } + | Term::ForceTemp { expr, .. } + | Term::Call { arg: expr, .. } => Some(expr), + Term::TailOk(e) | Term::TailErr(e) => Some(e), + _ => None, + }; + if let Some(expr) = term_expr { + rewrite_expr(expr, &slot_names, stack_count, &mut rewrite_errors); + } + self.errors.append(&mut rewrite_errors); + + let local_defs = collect_local_defs(&stmts, &self.seg_prologue); + let mut available: Vec = slot_names; + available.extend(local_defs.iter().cloned()); + available.push(format_ident!("mc")); + let uses = collect_uses(&stmts, &term); + for u in uses { + if !available.contains(&u) && self.plain_prior.contains(&u) { + self.err(syn::Error::new( + u.span(), + format!( + "`{u}` does not survive the suspension point; \ + read it back from a slot or spill it" + ), + )); + } + } + + self.plain_prior.extend(local_defs); + self.phases.push(Phase { + stmts, + prologue: std::mem::replace(&mut self.seg_prologue, Prologue::None), + term, + bindings: std::mem::take(&mut self.pending_bindings), + stack_count, + attrs: std::mem::take(&mut self.pending_attrs), + }); + } + + fn walk_block(&mut self, stmts: Vec) { + let mut after_loop = false; + for stmt in stmts { + if after_loop { + self.err(syn::Error::new( + stmt.span(), + "statements after a loop are unreachable (loops cannot break)", + )); + continue; + } + match stmt { + syn::Stmt::Local(local) => match classify_local(&local) { + Ok(Some(special)) => self.handle_special(local, special), + Ok(None) => self.seg.push(syn::Stmt::Local(local)), + Err(e) => self.err(e), + }, + syn::Stmt::Expr(expr, semi) => { + if matches!(expr, Expr::Loop(_)) { + if semi.is_some() { + self.err(syn::Error::new( + expr.span(), + "a loop that ends the handler must be the tail expression", + )); + } + self.handle_loop(expr); + after_loop = true; + continue; + } + self.seg.push(syn::Stmt::Expr(expr, semi)); + } + other @ (syn::Stmt::Item(_) | syn::Stmt::Macro(_)) => { + self.err(syn::Error::new( + other.span(), + "items and macros are not supported in #[handler] bodies", + )); + } + } + } + } + + fn handle_loop(&mut self, expr: Expr) { + if self.in_loop { + self.err(syn::Error::new( + expr.span(), + "nested loops are not supported", + )); + return; + } + if !self.seg.is_empty() { + let next = self.phases.len() + 1; + self.close(Term::Jump { next }); + } + let loop_start = self.phases.len(); + self.in_loop = true; + let body = match expr { + Expr::Loop(syn::ExprLoop { body, .. }) => body.stmts, + other => { + self.err(syn::Error::new( + other.span(), + "expected a `loop` expression", + )); + return; + } + }; + let phases_before_body = self.phases.len(); + self.walk_block(body); + + if self.phases.len() == phases_before_body { + // no suspension inside: keep the loop as ordinary Rust + let inner = std::mem::take(&mut self.seg); + let block = syn::Block { + brace_token: default(), + stmts: inner, + }; + self.seg.push(syn::Stmt::Expr( + Expr::Loop(syn::ExprLoop { + attrs: Vec::new(), + label: None, + loop_token: default(), + body: block, + }), + None, + )); + self.in_loop = false; + return; + } + + let offenders: Vec = self + .slots + .iter() + .filter(|s| s.alive && s.transient && !s.pop_on_entry && s.created >= loop_start) + .map(|s| { + syn::Error::new( + s.name.span(), + format!( + "`{}` is kept from a `call` without a type annotation across the loop \ + back-edge; annotate the binding so it is popped, or consume it", + s.name + ), + ) + }) + .collect(); + for e in offenders { + self.err(e); + } + self.close(Term::BackEdge { target: loop_start }); + self.in_loop = false; + self.ended_with_loop = true; + } + + fn handle_special(&mut self, local: syn::Local, special: Special) { + let Some(name) = pat_ident(&local.pat) else { + self.err(syn::Error::new( + local.pat.span(), + "suspension and spill bindings must bind a plain identifier", + )); + return; + }; + let ty = match local_annotation(&local.pat) { + None => None, + Some(t) => { + if matches!(special, Special::Call { .. }) { + Some(t) + } else { + Some(slot_content_ty(t)) + } + } + }; + self.pending_attrs = local.attrs; + match special { + Special::Spill(expr) => { + if self.in_loop { + self.err(syn::Error::new( + name.span(), + "spilling inside a loop would grow the stack every iteration", + )); + return; + } + let slot_ty = ty + .clone() + .unwrap_or_else(|| syn::parse_quote!(::fix_runtime::Value<'gc>)); + let next = self.phases.len() + 1; + self.close(Term::Spill { + expr, + ty: slot_ty.clone(), + next, + }); + let created = self.phases.len() - 1; + self.slots.push(SlotDef { + name, + ty: slot_ty, + transient: false, + pop_on_entry: false, + created, + alive: true, + forced: false, + }); + self.seg_prologue = Prologue::None; + self.refresh_pending(); + } + Special::Force(expr) => { + let target_ty = + ty.unwrap_or_else(|| syn::parse_quote!(::fix_runtime::StrictValue<'gc>)); + if let Some(ident) = single_path_ident(&expr) + && let Some(idx) = self.find_slot(&ident) + && let Some(slot) = self.slots.get_mut(idx) + { + if slot.forced { + self.err(syn::Error::new( + expr.span(), + "this slot has already been forced", + )); + return; + } + slot.ty = target_ty.clone(); + slot.forced = true; + let slot_name = slot.name.clone(); + let next = self.phases.len() + 1; + self.close(Term::ForceSlot { name: ident, next }); + self.seg_prologue = Prologue::Typecheck { + name: slot_name, + ty: target_ty, + }; + self.refresh_pending(); + } else { + let next = self.phases.len() + 1; + self.close(Term::ForceTemp { expr, next }); + let created = self.phases.len() - 1; + self.slots.push(SlotDef { + name: name.clone(), + ty: target_ty.clone(), + transient: true, + pop_on_entry: true, + created, + alive: true, + forced: true, + }); + self.seg_prologue = Prologue::Conv { + local: name, + ty: target_ty, + }; + self.refresh_pending(); + } + } + Special::Call { callee, arg } => { + let callee_idx = self.find_slot(&callee); + let is_transient_top = callee_idx.is_some_and(|idx| { + self.slots.get(idx).is_some_and(|s| s.alive && s.transient) + && !self.slots.iter().skip(idx + 1).any(|s| s.alive) + }); + let callee_kind = match callee_idx { + Some(idx) if is_transient_top => { + if let Some(slot) = self.slots.get_mut(idx) { + slot.alive = false; + } + Callee::Transient + } + Some(_) => Callee::Perm(callee), + None => Callee::Local(callee), + }; + let next = self.phases.len() + 1; + self.close(Term::Call { + callee: callee_kind, + arg, + next, + }); + let created = self.phases.len() - 1; + match ty { + Some(t) => { + self.slots.push(SlotDef { + name: name.clone(), + ty: t.clone(), + transient: true, + pop_on_entry: true, + created, + alive: true, + forced: true, + }); + self.seg_prologue = Prologue::Conv { local: name, ty: t }; + } + None => { + self.slots.push(SlotDef { + name, + ty: syn::parse_quote!(::fix_runtime::StrictValue<'gc>), + transient: true, + pop_on_entry: false, + created, + alive: true, + forced: true, + }); + self.seg_prologue = Prologue::None; + } + } + self.refresh_pending(); + } + } + } +} + +fn collect_local_defs(stmts: &[syn::Stmt], prologue: &Prologue) -> Vec { + struct Collect { + defs: Vec, + } + impl Visit<'_> for Collect { + fn visit_local(&mut self, l: &syn::Local) { + if let Some(ident) = pat_ident(&l.pat) { + self.defs.push(ident); + } + syn::visit::visit_local(self, l); + } + } + let mut v = Collect { defs: Vec::new() }; + for stmt in stmts { + v.visit_stmt(stmt); + } + if let Prologue::Conv { local, .. } = prologue { + v.defs.push(local.clone()); + } + v.defs +} + +fn collect_uses(stmts: &[syn::Stmt], term: &Term) -> Vec { + struct Collect { + uses: Vec, + } + impl Visit<'_> for Collect { + fn visit_expr(&mut self, e: &Expr) { + if let Some(ident) = single_path_ident(e) { + self.uses.push(ident); + } + syn::visit::visit_expr(self, e); + } + } + let mut v = Collect { uses: Vec::new() }; + for stmt in stmts { + v.visit_stmt(stmt); + } + for e in term.exprs() { + v.visit_expr(e); + } + v.uses +} + +fn rewrite_stmt( + stmt: &mut syn::Stmt, + slot_names: &[Ident], + stack_count: usize, + errors: &mut Vec, +) { + let mut v = Rewrite { + slots: slot_names, + stack_count, + errors, + }; + v.visit_stmt_mut(stmt); +} + +fn rewrite_expr( + expr: &mut Expr, + slot_names: &[Ident], + stack_count: usize, + errors: &mut Vec, +) { + let mut v = Rewrite { + slots: slot_names, + stack_count, + errors, + }; + v.visit_expr_mut(expr); +} + +struct Rewrite<'a> { + slots: &'a [Ident], + stack_count: usize, + errors: &'a mut Vec, +} + +impl Rewrite<'_> { + fn is_slot(&self, ident: &Ident) -> bool { + self.slots.iter().any(|s| s == ident) + } +} + +impl VisitMut for Rewrite<'_> { + fn visit_expr_mut(&mut self, e: &mut Expr) { + let m = format_ident!("m"); + match e { + Expr::MethodCall(mc) => { + if let Expr::Path(p) = &*mc.receiver + && p.path.segments.len() == 1 + && let Some(segment) = p.path.segments.first() + && self.is_slot(&segment.ident) + && (mc.method == "get" || mc.method == "set") + { + let (name, arity) = if mc.method == "get" { + ("read", 0) + } else { + ("write", 1) + }; + if mc.turbofish.is_some() || mc.args.len() != arity { + self.errors.push(syn::Error::new( + mc.method.span(), + "slot access must be exactly `slot.get()` or `slot.set(value)`", + )); + return; + } + if let Some(arg) = mc.args.iter_mut().next() { + self.visit_expr_mut(arg); + } + mc.method = Ident::new(name, mc.method.span()); + mc.args.insert(0, syn::parse_quote!(m)); + return; + } + syn::visit_mut::visit_expr_mut(self, e); + } + Expr::Try(t) => { + syn::visit_mut::visit_expr_mut(self, &mut t.expr); + let inner = &*t.expr; + *e = syn::parse_quote!(match (#inner) { + ::core::result::Result::Ok(__ok) => __ok, + ::core::result::Result::Err(__err) => return #m.finish_err(__err), + }); + } + Expr::Return(r) => { + let Some(ret) = &r.expr else { + self.errors + .push(syn::Error::new(r.span(), "bare `return` is not supported")); + return; + }; + let Expr::Call(call) = &**ret else { + self.errors.push(syn::Error::new( + ret.span(), + "`return` must return `Ok(..)` or `Err(..)`", + )); + return; + }; + let name = match &*call.func { + Expr::Path(p) => p.path.get_ident().cloned(), + _ => None, + }; + let Some(name) = name.filter(|n| (n == "Ok" || n == "Err") && call.args.len() == 1) + else { + self.errors.push(syn::Error::new( + ret.span(), + "`return` must return `Ok(..)` or `Err(..)`", + )); + return; + }; + let Some(arg) = call.args.first() else { + self.errors.push(syn::Error::new( + ret.span(), + "`return` must return `Ok(..)` or `Err(..)`", + )); + return; + }; + let mut val = arg.clone(); + self.visit_expr_mut(&mut val); + let n = self.stack_count; + if name == "Ok" { + *e = syn::parse_quote!({ + let __ret = #val; + #m.drop_n(#n); + return #m.return_from_primop(__ret, reader); + }); + } else { + *e = syn::parse_quote!({ + return #m.finish_err(#val); + }); + } + } + Expr::Await(a) => { + self.errors.push(syn::Error::new( + a.span(), + "suspensions must be direct `let` statements: `let x: T = force(..).await?;`", + )); + syn::visit_mut::visit_expr_mut(self, e); + } + Expr::Closure(c) => self.errors.push(syn::Error::new( + c.span(), + "closures are not supported in #[handler] bodies", + )), + Expr::Async(a) => self.errors.push(syn::Error::new( + a.span(), + "async blocks are not supported in #[handler] bodies", + )), + Expr::While(w) => self + .errors + .push(syn::Error::new(w.span(), "`while` loops are not supported")), + Expr::ForLoop(f) => self + .errors + .push(syn::Error::new(f.span(), "`for` loops are not supported")), + Expr::Break(b) => self + .errors + .push(syn::Error::new(b.span(), "`break` is not supported")), + Expr::Continue(c) => self + .errors + .push(syn::Error::new(c.span(), "`continue` is not supported")), + Expr::Path(p) if let Some(ident) = p.path.get_ident() => { + if *ident == "m" || *ident == "reader" { + self.errors.push(syn::Error::new( + ident.span(), + "the machine and reader are not accessible in #[handler] bodies", + )); + } + if *ident == "spill" || *ident == "force" || *ident == "call" { + self.errors.push(syn::Error::new( + ident.span(), + "`spill`/`force`/`call` are only supported as direct let-statement forms", + )); + } + } + _ => syn::visit_mut::visit_expr_mut(self, e), + } + } +} + +fn expand(attr: HandlerAttr, item: ItemFn) -> Result { + let sig = &item.sig; + if sig.asyncness.is_some() || sig.constness.is_some() || matches!(sig.safety, Safety::Unsafe(_)) { + return Err(syn::Error::new( + sig.span(), + "#[handler] functions cannot be async, const, or unsafe", + )); + } + if sig.generics.params.len() != 1 + || sig + .generics + .lifetimes() + .next() + .is_none_or(|l| l.lifetime.ident != "gc") + || sig.generics.where_clause.is_some() + { + return Err(syn::Error::new( + sig.generics.span(), + "#[handler] functions take exactly one lifetime parameter, `'gc`", + )); + } + if sig.receiver().is_some() { + return Err(syn::Error::new( + sig.span(), + "#[handler] functions cannot take self", + )); + } + + let mut has_ctx = false; + let mut arg_names = Vec::new(); + for input in &sig.inputs { + let syn::FnArg::Typed(typed) = input else { + return Err(syn::Error::new( + input.span(), + "unexpected receiver argument", + )); + }; + let Some(name) = pat_ident(&typed.pat) else { + return Err(syn::Error::new( + typed.pat.span(), + "parameters must be plain identifiers", + )); + }; + if name == "mc" || name == "ctx" { + if name == "ctx" { + has_ctx = true; + } + continue; + } + arg_names.push(name); + } + + let mut walker = Walker { + prefix: attr.prefix.to_string(), + has_ctx, + phases: Vec::new(), + slots: arg_names + .iter() + .map(|name| SlotDef { + name: name.clone(), + ty: syn::parse_quote!(::fix_runtime::Value<'gc>), + transient: false, + pop_on_entry: false, + created: 0, + alive: true, + forced: false, + }) + .collect(), + plain_prior: Vec::new(), + seg: Vec::new(), + seg_prologue: Prologue::None, + pending_bindings: Vec::new(), + pending_stack_count: 0, + pending_attrs: Vec::new(), + in_loop: false, + ended_with_loop: false, + errors: Vec::new(), + }; + walker.refresh_pending(); + + let mut stmts = item.block.stmts.clone(); + let mut tail = None; + if let Some(syn::Stmt::Expr(expr, None)) = stmts.last() + && let Some((name, args)) = classify_call(expr) + && (name == "Ok" || name == "Err") + && args.len() == 1 + { + if name == "Ok" { + tail = Some(Term::TailOk(args[0].clone())); + } else { + tail = Some(Term::TailErr(args[0].clone())); + } + stmts.pop(); + } + + walker.walk_block(stmts); + + if walker.seg.is_empty() && tail.is_none() { + if !walker.ended_with_loop && walker.errors.is_empty() { + walker.err(syn::Error::new( + item.block.span(), + "#[handler] body must end with `Ok(..)`/`Err(..)` or a `loop`", + )); + } + } else { + let terminates = tail.is_some() + || walker.ended_with_loop + || matches!( + walker.seg.last(), + Some(syn::Stmt::Expr(Expr::Loop(_) | Expr::Return(_), _)) + ); + if !terminates { + walker.err(syn::Error::new( + item.block.span(), + "#[handler] body must end with `Ok(..)`/`Err(..)` or a `loop`", + )); + } + walker.close(tail.unwrap_or(Term::TailNone)); + } + + let Walker { + prefix, + has_ctx, + phases, + errors, + .. + } = walker; + if let Some(combined) = errors.into_iter().reduce(|mut a, b| { + a.combine(b); + a + }) { + return Err(combined); + } + + let phase_fns: Vec = phases + .iter() + .enumerate() + .map(|(i, phase)| emit_phase(&prefix, has_ctx, i, phase)) + .collect(); + let arms: Vec = (0..phases.len()) + .map(|i| { + let cont = format_ident!("{}{}", prefix, i); + let f = format_ident!("__phase{}", i); + if has_ctx { + quote! { ::fix_bytecode::Continuation::#cont => #f(m, ctx, reader, mc) } + } else { + quote! { ::fix_bytecode::Continuation::#cont => #f(m, reader, mc) } + } + }) + .collect(); + let ctx_param = has_ctx.then(|| { + quote! { , ctx: &mut impl ::fix_runtime::VmRuntimeCtx } + }); + let name = &sig.ident; + + // A typed closure reproducing the source signature keeps its type imports + // alive after the body is replaced and type-checks every slot argument as + // `Slot>`, and referencing the stub trio keeps the `stubs::*` + // glob import from warning as unused. + let mut expected_tys = Vec::new(); + let mut closure_params = Vec::new(); + for input in &sig.inputs { + let syn::FnArg::Typed(typed) = input else { + continue; + }; + let Some(name) = pat_ident(&typed.pat) else { + continue; + }; + if name == "ctx" { + continue; + } + let src = &*typed.ty; + if name == "mc" { + expected_tys.push(quote! { #src }); + } else { + expected_tys.push(quote! { ::fix_runtime::Slot<::fix_runtime::Value<'gc>> }); + } + closure_params.push(quote! { _: #src }); + } + let stub_ret = &sig.output; + let signature_linkage = quote! { + let _: fn(#(#expected_tys),*) #stub_ret = |#(#closure_params),*| loop {}; + let _ = (force::<()>, call::<(), (), ()>, spill::<()>); + }; + let fn_attrs = &item.attrs; + + Ok(quote! { + #(#fn_attrs)* + #[inline(always)] + pub fn #name<'gc, M: ::fix_runtime::Machine<'gc>>( + m: &mut M, + #ctx_param reader: &mut ::fix_runtime::BytecodeReader<'_>, + mc: &::gc_arena::Mutation<'gc>, + cont: ::fix_bytecode::Continuation, + ) -> ::fix_runtime::Step { + #signature_linkage + + #(#phase_fns)* + + match cont { + #(#arms,)* + #[expect( + clippy::unreachable, + reason = "dispatch_cont routes only this handler's continuations to this function" + )] + _ => unreachable!(), + } + } + }) +} + +fn emit_phase(prefix: &str, has_ctx: bool, idx: usize, phase: &Phase) -> TokenStream { + let f = format_ident!("__phase{}", idx); + let mut uses = collect_uses(&phase.stmts, &phase.term); + if let Term::ForceSlot { name, .. } = &phase.term { + uses.push(name.clone()); + } + let callee_name = match &phase.term { + Term::Call { + callee: Callee::Perm(name), + .. + } => Some(name.clone()), + _ => None, + }; + + let mut prologue = quote! {}; + if let Prologue::Conv { local, ty } = &phase.prologue { + prologue = quote! { + let #local: <#ty as ::fix_runtime::SlotContent<'gc>>::Ty = + <#ty as ::fix_vm::__macro_support::ForceTarget<'gc>>::conv_force(m, reader, mc)?; + }; + } + + let bindings: Vec = phase + .bindings + .iter() + .filter(|b| callee_name.as_ref() == Some(&b.name) || uses.contains(&b.name)) + .map(|b| { + let name = &b.name; + let ty = &b.ty; + let depth = proc_macro2::Literal::u8_suffixed(b.depth as u8); + quote! { let #name: ::fix_runtime::Slot<#ty> = ::fix_runtime::Slot::new(#depth); } + }) + .collect(); + + if let Prologue::Typecheck { name, ty } = &phase.prologue + && let Some(b) = phase.bindings.iter().find(|b| &b.name == name) + { + let depth = b.depth; + prologue = quote! { + #prologue + <#ty as ::fix_vm::__macro_support::ForceTarget<'gc>>::refine_check(m, reader, mc, #depth)?; + }; + } + + let callee_force = if let Some(name) = &callee_name { + quote! { + ::fix_vm::__macro_support::CalleeReady::callee_ready(&#name, m, reader, mc)?; + } + } else { + quote! {} + }; + + let stmts = &phase.stmts; + let term = emit_term(prefix, has_ctx, &phase.term, phase); + + let body = quote! { + #prologue + #(#bindings)* + #callee_force + #(#stmts)* + #term + }; + + let m = param_ident(&body, "m"); + let reader = param_ident(&body, "reader"); + let mc = param_ident(&body, "mc"); + let body = replace_ident(&body, "__handler_arg_m", &m); + let body = replace_ident(&body, "__handler_arg_reader", &reader); + let body = replace_ident(&body, "__handler_arg_mc", &mc); + let (ctx_param, body) = if has_ctx { + let ctx = param_ident(&body, "ctx"); + let body = replace_ident(&body, "__handler_arg_ctx", &ctx); + ( + quote! { #ctx: &mut impl ::fix_runtime::VmRuntimeCtx, }, + body, + ) + } else { + (quote! {}, body) + }; + + let attrs = &phase.attrs; + + quote! { + #(#attrs)* + #[inline(always)] + fn #f<'gc, M: ::fix_runtime::Machine<'gc>>( + #m: &mut M, + #ctx_param + #reader: &mut ::fix_runtime::BytecodeReader<'_>, + #mc: &::gc_arena::Mutation<'gc>, + ) -> ::fix_runtime::Step { + #body + } + } +} + +fn param_ident(body: &TokenStream, name: &str) -> Ident { + if uses_ident(body, name) { + Ident::new(name, proc_macro2::Span::call_site()) + } else { + format_ident!("_{name}") + } +} + +fn replace_ident(ts: &TokenStream, from: &str, to: &Ident) -> TokenStream { + ts.clone() + .into_iter() + .map(|tt| match tt { + proc_macro2::TokenTree::Ident(ref i) if i == from => { + proc_macro2::TokenTree::Ident(to.clone()) + } + proc_macro2::TokenTree::Group(g) => proc_macro2::TokenTree::Group( + proc_macro2::Group::new(g.delimiter(), replace_ident(&g.stream(), from, to)), + ), + other => other, + }) + .collect() +} + +fn emit_term(prefix: &str, has_ctx: bool, term: &Term, phase: &Phase) -> TokenStream { + let next_fn = |next: usize| format_ident!("__phase{}", next); + let next_cont = |next: usize| format_ident!("{}{}", prefix, next); + let tail_call = |next: usize| { + let nf = next_fn(next); + if has_ctx { + quote! { #nf(__handler_arg_m, __handler_arg_ctx, __handler_arg_reader, __handler_arg_mc) } + } else { + quote! { #nf(__handler_arg_m, __handler_arg_reader, __handler_arg_mc) } + } + }; + match term { + Term::ForceSlot { name, next } => { + let nc = next_cont(*next); + let call = tail_call(*next); + quote! { + #name.force_to_pc(m, reader, mc, ::fix_bytecode::Continuation::#nc.ip() as usize)?; + #call + } + } + Term::ForceTemp { expr, next } => { + let nc = next_cont(*next); + let call = tail_call(*next); + quote! { + m.push(#expr); + m.force_slot_to_pc(0usize, reader, mc, ::fix_bytecode::Continuation::#nc.ip() as usize)?; + #call + } + } + Term::Spill { expr, ty, next } => { + let nc = next_cont(*next); + quote! { + let __spilled: <#ty as ::fix_runtime::SlotContent<'gc>>::Ty = #expr; + m.push(::core::convert::Into::into(__spilled)); + reader.set_pc(::fix_bytecode::Continuation::#nc.ip() as usize); + ::fix_runtime::Step::Continue(()) + } + } + Term::Call { callee, arg, next } => { + let nc = next_cont(*next); + match callee { + Callee::Perm(name) => { + quote! { + let __call_arg = #arg; + m.push(::core::convert::Into::into(#name.read(m))); + m.call(reader, mc, __call_arg, ::fix_bytecode::Continuation::#nc.ip() as usize) + } + } + Callee::Transient => quote! { + m.call(reader, mc, #arg, ::fix_bytecode::Continuation::#nc.ip() as usize) + }, + Callee::Local(name) => quote! { + let __call_arg = #arg; + m.push(#name.relax()); + m.call(reader, mc, __call_arg, ::fix_bytecode::Continuation::#nc.ip() as usize) + }, + } + } + Term::Jump { next } => { + let nc = next_cont(*next); + quote! { + reader.set_pc(::fix_bytecode::Continuation::#nc.ip() as usize); + ::fix_runtime::Step::Continue(()) + } + } + Term::BackEdge { target } => { + let nc = next_cont(*target); + quote! { + reader.set_pc(::fix_bytecode::Continuation::#nc.ip() as usize); + ::fix_runtime::Step::Continue(()) + } + } + Term::TailOk(e) => { + let n = phase.stack_count; + quote! { + let __ret = #e; + m.drop_n(#n); + m.return_from_primop(__ret, reader) + } + } + Term::TailErr(e) => quote! { m.finish_err(#e) }, + Term::TailNone => quote! {}, + } +} + +fn uses_ident(ts: &TokenStream, name: &str) -> bool { + fn go(tt: &TokenTree, name: &str) -> bool { + match tt { + TokenTree::Ident(i) => i == name, + TokenTree::Group(g) => g.stream().into_iter().any(|t| go(&t, name)), + _ => false, + } + } + ts.clone().into_iter().any(|t| go(&t, name)) +} + +fn default() -> T { + T::default() +} + +pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = match syn::parse2::(attr) { + Ok(a) => a, + Err(e) => return e.to_compile_error(), + }; + let item = match syn::parse2::(item) { + Ok(i) => i, + Err(e) => return e.to_compile_error(), + }; + match expand(attr, item) { + Ok(ts) => ts, + Err(e) => e.to_compile_error(), + } +} diff --git a/fix-macros/src/lib.rs b/fix-macros/src/lib.rs index f49dd36..05b53dc 100644 --- a/fix-macros/src/lib.rs +++ b/fix-macros/src/lib.rs @@ -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 diff --git a/fix-runtime/src/lib.rs b/fix-runtime/src/lib.rs index 8240edb..dfeef6d 100644 --- a/fix-runtime/src/lib.rs +++ b/fix-runtime/src/lib.rs @@ -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::*; diff --git a/fix-runtime/src/macro_support.rs b/fix-runtime/src/macro_support.rs new file mode 100644 index 0000000..ce17ec7 --- /dev/null +++ b/fix-runtime/src/macro_support.rs @@ -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 UnwrapSlot for Slot { + 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: &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: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow>::Ty>; +} + +impl<'gc> ForceTarget<'gc> for Value<'gc> { + #[inline(always)] + fn refine_check>( + _m: &mut M, + _reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, + _depth: usize, + ) -> Step { + Step::Continue(()) + } + + #[inline(always)] + fn conv_force>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow> { + match m.force_and_retry::>(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: &mut M, + _reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, + _depth: usize, + ) -> Step { + Step::Continue(()) + } + + #[inline(always)] + fn conv_force>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow> { + m.force_and_retry::>(reader, mc) + } +} + +impl<'gc, T> ForceTarget<'gc> for T +where + T: ValueVariant<'gc>, + >::Ty: Forced<'gc>, +{ + #[inline(always)] + fn refine_check>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + depth: usize, + ) -> Step { + <>::Ty as Forced<'gc>>::force_and_check( + m, + reader, + mc, + depth, + reader.inst_start_pc(), + ) + } + + #[inline(always)] + fn conv_force>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow>::Ty> { + m.force_and_retry::<>::Ty>(reader, mc) + } +} + +/// Callee protocol behind `call(&slot, ..)`: an unforced `Slot` is +/// WHNF'd before the call; refined slots already hold strict values. +pub trait CalleeReady<'gc> { + fn callee_ready>( + &self, + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow; +} + +impl<'gc> CalleeReady<'gc> for Slot> { + #[inline(always)] + fn callee_ready>( + &self, + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow { + self.force::, M>(m, reader, mc)?; + ControlFlow::Continue(()) + } +} + +impl<'gc> CalleeReady<'gc> for Slot> { + #[inline(always)] + fn callee_ready>( + &self, + _m: &mut M, + _reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, + ) -> ControlFlow { + ControlFlow::Continue(()) + } +} + +impl<'gc, T> CalleeReady<'gc> for Slot +where + T: ValueVariant<'gc>, +{ + #[inline(always)] + fn callee_ready>( + &self, + _m: &mut M, + _reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, + ) -> ControlFlow { + ControlFlow::Continue(()) + } +} diff --git a/fix-runtime/src/slot.rs b/fix-runtime/src/slot.rs index 2fcc2cd..64fe74b 100644 --- a/fix-runtime/src/slot.rs +++ b/fix-runtime/src/slot.rs @@ -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> + TryFrom> + '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>(&self, m: &M) -> Result { - let val = m.peek(self.depth as usize); - T::Ty::try_from(val).map_err(|_err| val.ty()) - } - #[inline(always)] pub fn write>(&self, m: &mut M, val: T::Ty) { m.replace(self.depth as usize, T::Ty::into(val)); diff --git a/fix-vm/src/instructions/calls.rs b/fix-vm/src/instructions/calls.rs index e4b009a..28d3820 100644 --- a/fix-vm/src/instructions/calls.rs +++ b/fix-vm/src/instructions/calls.rs @@ -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(()); } } diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 09524f7..48fc785 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -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 = std::result::Result; diff --git a/fix-vm/src/macro_support.rs b/fix-vm/src/macro_support.rs new file mode 100644 index 0000000..1aad815 --- /dev/null +++ b/fix-vm/src/macro_support.rs @@ -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(_: Slot>) -> impl Future> { + type_hint!(); + panic!("stub `force` called at runtime") +} + +#[expect(clippy::panic, reason = "deliberately panic when the stub is called")] +pub fn call(_: &T, _: A) -> impl Future { + type_hint!(); + panic!("stub `call` called at runtime") +} + +#[expect(clippy::panic, reason = "deliberately panic when the stub is called")] +pub fn spill(_: T) -> Slot { + panic!("stub `spill` called at runtime") +} diff --git a/fix-vm/src/primops/control.rs b/fix-vm/src/primops/control.rs index a18b269..9814190 100644 --- a/fix-vm/src/primops/control.rs +++ b/fix-vm/src/primops/control.rs @@ -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>, e2: Slot>) -> Result> { + let _e1: Slot> = 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>, + e2: Slot>, +) -> Result> { + let e1: Slot> = force(e1).await?; + if collect_children(e1.get()).is_empty() { + return Ok(e2.get()); + } + let seen: Slot> = spill(Gc::new(mc, List::default())); + let worklist: Slot> = spill(List::new(mc, collect_children(e1.get()))); + let count: Slot = 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> = 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::>() { + attrs.entries.iter().map(|&(_, v)| v).collect() + } else if let Some(list) = val.downcast::>() { + 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::>() + && !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::>() + && !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::() { - let attrs = &attrs.entries; - if attrs.is_empty() { - SmallVec::new() - } else { - attrs.iter().map(|&(_, v)| v).collect() - } - } else if let Some(list) = e1.downcast::() { - 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::() - .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::() - .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::() - .expect("stack slot must be an integer"); - - let mut added: usize = 0; - if let Some(attrs) = item.downcast::() { - let attrs = &attrs.entries; - let seen = m - .peek_forced(2) - .downcast::() - .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::() - .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::() { - let seen = m - .peek_forced(2) - .downcast::() - .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::() - .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, diff --git a/fix-vm/src/primops/list.rs b/fix-vm/src/primops/list.rs index 883adfe..0df71d9 100644 --- a/fix-vm/src/primops/list.rs +++ b/fix-vm/src/primops/list.rs @@ -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>, + list: Slot>, +) -> Result> { + let list: Slot> = force(list).await?; + if list.get().inner.borrow().is_empty() { + return Ok(Value::new(list.get())); + } + let idx: Slot = spill(0i32); + let acc: Slot> = 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 = 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>, + list: Slot>, +) -> Result> { + let list: Slot> = 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::(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 = 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>, + list: Slot>, +) -> Result> { + let list: Slot> = force(list).await?; + if list.get().inner.borrow().is_empty() { + return Ok(Value::new(false)); + } + let idx: Slot = 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::() 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>, + nul: Slot>, + list: Slot>, +) -> Result> { + let list: Slot> = 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::(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::() - .expect("stack slot must be an integer"); - let list = m - .peek_forced(3) - .downcast::() - .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::() - .expect("stack slot must be an integer"); - let list = m - .peek_forced(2) - .downcast::() - .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 = 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::() { - 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::() - .expect("stack slot must be an integer"); - let elem = m - .peek_forced(1) - .downcast::() - .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::(reader, mc)?; - let idx = m - .peek(0) - .downcast::() - .expect("stack slot must be an integer"); - let list = m - .peek_forced(1) - .downcast::() - .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::() { - 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::() - .expect("stack slot must be an integer"); - let elem = m - .peek_forced(1) - .downcast::() - .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::(reader, mc)?; - let idx = m - .peek(0) - .downcast::() - .expect("stack slot must be an integer"); - let list = m - .peek_forced(1) - .downcast::() - .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(()) } diff --git a/fix-vm/src/primops/mod.rs b/fix-vm/src/primops/mod.rs index 6a7457b..9864b17 100644 --- a/fix-vm/src/primops/mod.rs +++ b/fix-vm/src/primops/mod.rs @@ -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), diff --git a/fix-vm/src/primops/stubs.rs b/fix-vm/src/primops/stubs.rs new file mode 100644 index 0000000..7dbd542 --- /dev/null +++ b/fix-vm/src/primops/stubs.rs @@ -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};