Skip to main content

intuicio_backend_vm/
scope.rs

1//! The interpreter itself: one scope of script operations, stepped through.
2//!
3//! A [`VmScope`] holds a list of operations and the position it reached in
4//! them. Operations that open a nested scope, such as a branch or a loop body,
5//! put a child scope inside the parent, so the call stack of the script is a
6//! chain of scopes rather than Rust recursion. That is what lets a script stop
7//! in the middle and carry on later.
8use crate::debugger::VmDebuggerHandle;
9use intuicio_core::{
10    context::Context,
11    function::FunctionBody,
12    registry::{Registry, RegistryHandle},
13    script::{ScriptExpression, ScriptFunctionGenerator, ScriptHandle, ScriptOperation},
14};
15use intuicio_data::managed::{ManagedLazy, ManagedRefMut};
16use typid::ID;
17
18/// Identifies one compiled script, so a debugger can tell scopes apart.
19///
20/// Every function installed with this backend gets one, and every nested scope
21/// of that function shares it. See
22/// [`SourceMapLocation`](crate::debugger::SourceMapLocation).
23pub type VmScopeSymbol = ID<()>;
24
25/// What one step of a [`VmScope`] did.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VmScopeResult {
28    /// The scope has more operations to run.
29    Continue,
30    /// The scope reached its end and will do nothing more.
31    Completed,
32    /// The scope hit a `Suspend` operation and stopped in the middle.
33    Suspended,
34}
35
36impl VmScopeResult {
37    /// Returns `true` only for [`VmScopeResult::Continue`].
38    pub fn can_continue(self) -> bool {
39        self == VmScopeResult::Continue
40    }
41
42    /// Returns `true` only for [`VmScopeResult::Completed`].
43    pub fn is_completed(self) -> bool {
44        self == VmScopeResult::Completed
45    }
46
47    /// Returns `true` only for [`VmScopeResult::Suspended`].
48    pub fn is_suspended(self) -> bool {
49        self == VmScopeResult::Suspended
50    }
51
52    /// Returns `true` unless the scope is done, so a suspended scope counts as
53    /// able to go on.
54    pub fn can_progress(self) -> bool {
55        !self.is_completed()
56    }
57}
58
59/// A list of script operations and the position reached in them.
60///
61/// This is the whole interpreter. Build one with [`VmScope::new`] and drive it
62/// with [`VmScope::run`], [`VmScope::run_until_suspended`] or
63/// [`VmScope::step`]. It is also the `ScriptFunctionGenerator` of this backend,
64/// so installing a script package produces one scope per function call.
65///
66/// Cloning copies the position and the child scope as well, so a clone carries
67/// on where the original stood.
68pub struct VmScope<'a, SE: ScriptExpression> {
69    handle: ScriptHandle<'a, SE>,
70    symbol: VmScopeSymbol,
71    position: usize,
72    child: Option<Box<Self>>,
73    debugger: Option<VmDebuggerHandle<SE>>,
74}
75
76impl<'a, SE: ScriptExpression> VmScope<'a, SE> {
77    /// Starts a scope at the first operation of `handle`.
78    ///
79    /// `symbol` names the script this scope belongs to, for debugging.
80    pub fn new(handle: ScriptHandle<'a, SE>, symbol: VmScopeSymbol) -> Self {
81        Self {
82            handle,
83            symbol,
84            position: 0,
85            child: None,
86            debugger: None,
87        }
88    }
89
90    /// Puts the scope back at `position`, with `child` as the nested scope that
91    /// was running there.
92    ///
93    /// This is how a scope taken apart with [`VmScope::into_inner`] is put back
94    /// together, for example after being stored somewhere.
95    ///
96    /// # Safety
97    ///
98    /// `position` and `child` must come from a scope over the same script. The
99    /// operations that follow expect the stack and the registers to look the
100    /// way the skipped operations left them. Any other position runs them
101    /// against a state they were never written for.
102    pub unsafe fn restore(mut self, position: usize, child: Option<Self>) -> Self {
103        self.position = position;
104        self.child = child.map(Box::new);
105        self
106    }
107
108    /// Attaches a debugger, builder style. Child scopes inherit it.
109    pub fn with_debugger(mut self, debugger: Option<VmDebuggerHandle<SE>>) -> Self {
110        self.debugger = debugger;
111        self
112    }
113
114    /// Splits the scope into its parts: script, symbol, position, child scope
115    /// and debugger.
116    ///
117    /// [`VmScope::restore`] puts them back together.
118    #[allow(clippy::type_complexity)]
119    pub fn into_inner(
120        self,
121    ) -> (
122        ScriptHandle<'a, SE>,
123        VmScopeSymbol,
124        usize,
125        Option<Box<Self>>,
126        Option<VmDebuggerHandle<SE>>,
127    ) {
128        (
129            self.handle,
130            self.symbol,
131            self.position,
132            self.child,
133            self.debugger,
134        )
135    }
136
137    /// Returns the script this scope belongs to.
138    pub fn symbol(&self) -> VmScopeSymbol {
139        self.symbol
140    }
141
142    /// Returns the index of the next operation to run.
143    pub fn position(&self) -> usize {
144        self.position
145    }
146
147    /// Returns `true` once every operation of this scope has run.
148    ///
149    /// Says nothing about a child scope that may still be running.
150    pub fn has_completed(&self) -> bool {
151        self.position >= self.handle.len()
152    }
153
154    /// Returns the nested scope that is running right now, if there is one.
155    pub fn child(&self) -> Option<&Self> {
156        self.child.as_deref()
157    }
158
159    /// Steps until the scope is done.
160    ///
161    /// A `Suspend` operation does not stop this, so use
162    /// [`VmScope::run_until_suspended`] when the script is meant to yield.
163    ///
164    /// # Panics
165    ///
166    /// Panics on the same terms as [`VmScope::step`].
167    pub fn run(&mut self, context: &mut Context, registry: &Registry) {
168        while self.step(context, registry).can_progress() {}
169    }
170
171    /// Steps until the scope is done or hits a `Suspend` operation.
172    ///
173    /// Call it again to carry on from where it stopped. Never returns
174    /// [`VmScopeResult::Continue`].
175    ///
176    /// # Panics
177    ///
178    /// Panics on the same terms as [`VmScope::step`].
179    pub fn run_until_suspended(
180        &mut self,
181        context: &mut Context,
182        registry: &Registry,
183    ) -> VmScopeResult {
184        loop {
185            match self.step(context, registry) {
186                VmScopeResult::Continue => {}
187                result => return result,
188            }
189        }
190    }
191
192    /// Runs one operation, or one operation of the deepest child scope.
193    ///
194    /// A scope past its last operation reports
195    /// [`VmScopeResult::Completed`] and does nothing.
196    ///
197    /// # Panics
198    ///
199    /// The operations trust what the frontend produced, so a broken script
200    /// panics rather than failing. It panics when a register or a function
201    /// query matches nothing, when a register index does not exist, when a
202    /// value does not fit on the stack, and when a branch or loop operation
203    /// finds no `bool` on top of the stack.
204    pub fn step(&mut self, context: &mut Context, registry: &Registry) -> VmScopeResult {
205        if let Some(child) = &mut self.child {
206            match child.step(context, registry) {
207                VmScopeResult::Completed => {
208                    self.child = None;
209                }
210                result => return result,
211            }
212        }
213        if self.position == 0
214            && let Some(debugger) = self.debugger.as_ref()
215            && let Ok(mut debugger) = debugger.try_write()
216        {
217            debugger.on_enter_scope(self, context, registry);
218        }
219        let result = if let Some(operation) = self.handle.get(self.position) {
220            if let Some(debugger) = self.debugger.as_ref()
221                && let Ok(mut debugger) = debugger.try_write()
222            {
223                debugger.on_enter_operation(self, operation, self.position, context, registry);
224            }
225            let position = self.position;
226            let result = match operation {
227                ScriptOperation::None => {
228                    self.position += 1;
229                    VmScopeResult::Continue
230                }
231                ScriptOperation::Expression { expression } => {
232                    expression.evaluate(context, registry);
233                    self.position += 1;
234                    VmScopeResult::Continue
235                }
236                ScriptOperation::DefineRegister { query } => {
237                    let handle = registry
238                        .types()
239                        .find(|handle| query.is_valid(handle))
240                        .unwrap_or_else(|| {
241                            panic!("Could not define register for non-existent type: {query:#?}")
242                        });
243                    unsafe {
244                        context
245                            .registers()
246                            .push_register_raw(handle.type_hash(), *handle.layout())
247                    };
248                    self.position += 1;
249                    VmScopeResult::Continue
250                }
251                ScriptOperation::DropRegister { index } => {
252                    let index = context.absolute_register_index(*index);
253                    context
254                        .registers()
255                        .access_register(index)
256                        .unwrap_or_else(|| {
257                            panic!("Could not access non-existent register: {index}")
258                        })
259                        .free();
260                    self.position += 1;
261                    VmScopeResult::Continue
262                }
263                ScriptOperation::PushFromRegister { index } => {
264                    let index = context.absolute_register_index(*index);
265                    let (stack, registers) = context.stack_and_registers();
266                    let mut register = registers.access_register(index).unwrap_or_else(|| {
267                        panic!("Could not access non-existent register: {index}")
268                    });
269                    if !stack.push_from_register(&mut register) {
270                        panic!("Could not push data from register: {index}");
271                    }
272                    self.position += 1;
273                    VmScopeResult::Continue
274                }
275                ScriptOperation::PopToRegister { index } => {
276                    let index = context.absolute_register_index(*index);
277                    let (stack, registers) = context.stack_and_registers();
278                    let mut register = registers.access_register(index).unwrap_or_else(|| {
279                        panic!("Could not access non-existent register: {index}")
280                    });
281                    if !stack.pop_to_register(&mut register) {
282                        panic!("Could not pop data to register: {index}");
283                    }
284                    self.position += 1;
285                    VmScopeResult::Continue
286                }
287                ScriptOperation::MoveRegister { from, to } => {
288                    let from = context.absolute_register_index(*from);
289                    let to = context.absolute_register_index(*to);
290                    let (mut source, mut target) = context
291                        .registers()
292                        .access_registers_pair(from, to)
293                        .unwrap_or_else(|| {
294                            panic!("Could not access non-existent registers pair: {from} and {to}")
295                        });
296                    source.move_to(&mut target);
297                    self.position += 1;
298                    VmScopeResult::Continue
299                }
300                ScriptOperation::CallFunction { query } => {
301                    let handle = registry
302                        .functions()
303                        .find(|handle| query.is_valid(handle.signature()))
304                        .unwrap_or_else(|| {
305                            panic!("Could not call non-existent function: {query:#?}")
306                        });
307                    handle.invoke(context, registry);
308                    self.position += 1;
309                    VmScopeResult::Continue
310                }
311                ScriptOperation::BranchScope {
312                    scope_success,
313                    scope_failure,
314                } => {
315                    if context.stack().pop::<bool>().unwrap() {
316                        self.child = Some(Box::new(
317                            Self::new(scope_success.clone(), self.symbol)
318                                .with_debugger(self.debugger.clone()),
319                        ));
320                    } else if let Some(scope_failure) = scope_failure {
321                        self.child = Some(Box::new(
322                            Self::new(scope_failure.clone(), self.symbol)
323                                .with_debugger(self.debugger.clone()),
324                        ));
325                    }
326                    self.position += 1;
327                    VmScopeResult::Continue
328                }
329                ScriptOperation::LoopScope { scope } => {
330                    if !context.stack().pop::<bool>().unwrap() {
331                        self.position += 1;
332                    } else {
333                        self.child = Some(Box::new(
334                            Self::new(scope.clone(), self.symbol)
335                                .with_debugger(self.debugger.clone()),
336                        ));
337                    }
338                    VmScopeResult::Continue
339                }
340                ScriptOperation::PushScope { scope } => {
341                    context.store_registers();
342                    self.child = Some(Box::new(
343                        Self::new(scope.clone(), self.symbol).with_debugger(self.debugger.clone()),
344                    ));
345                    self.position += 1;
346                    VmScopeResult::Continue
347                }
348                ScriptOperation::PopScope => {
349                    context.restore_registers();
350                    self.position = self.handle.len();
351                    VmScopeResult::Completed
352                }
353                ScriptOperation::ContinueScopeConditionally => {
354                    if context.stack().pop::<bool>().unwrap() {
355                        self.position += 1;
356                        VmScopeResult::Continue
357                    } else {
358                        self.position = self.handle.len();
359                        VmScopeResult::Completed
360                    }
361                }
362                ScriptOperation::Suspend => {
363                    self.position += 1;
364                    VmScopeResult::Suspended
365                }
366            };
367            if let Some(debugger) = self.debugger.as_ref()
368                && let Ok(mut debugger) = debugger.try_write()
369            {
370                debugger.on_exit_operation(self, operation, position, context, registry);
371            }
372            result
373        } else {
374            VmScopeResult::Completed
375        };
376        if (!result.can_progress() || self.position >= self.handle.len())
377            && let Some(debugger) = self.debugger.as_ref()
378            && let Ok(mut debugger) = debugger.try_write()
379        {
380            debugger.on_exit_scope(self, context, registry);
381        }
382        result
383    }
384}
385
386impl<SE: ScriptExpression + 'static> ScriptFunctionGenerator<SE> for VmScope<'static, SE> {
387    type Input = Option<VmDebuggerHandle<SE>>;
388    type Output = VmScopeSymbol;
389
390    fn generate_function_body(
391        script: ScriptHandle<'static, SE>,
392        debugger: Self::Input,
393    ) -> Option<(FunctionBody, Self::Output)> {
394        let symbol = VmScopeSymbol::new();
395        Some((
396            FunctionBody::closure(move |context, registry| {
397                Self::new(script.clone(), symbol)
398                    .with_debugger(debugger.clone())
399                    .run(context, registry);
400            }),
401            symbol,
402        ))
403    }
404}
405
406impl<SE: ScriptExpression> Clone for VmScope<'_, SE> {
407    fn clone(&self) -> Self {
408        Self {
409            handle: self.handle.clone(),
410            symbol: self.symbol,
411            position: self.position,
412            child: self.child.as_ref().map(|child| Box::new((**child).clone())),
413            debugger: self.debugger.clone(),
414        }
415    }
416}
417
418/// How a [`VmScopeFuture`] holds the context it runs on.
419pub enum VmScopeFutureContext {
420    /// The future owns the context.
421    Owned(Box<Context>),
422    /// The future holds an exclusive handle to a context owned elsewhere.
423    RefMut(ManagedRefMut<Context>),
424    /// The future holds an unclaimed handle, and takes write access only while
425    /// it steps. Something else can use the context in between.
426    Lazy(ManagedLazy<Context>),
427}
428
429impl From<Box<Context>> for VmScopeFutureContext {
430    fn from(value: Box<Context>) -> Self {
431        Self::Owned(value)
432    }
433}
434
435impl From<Context> for VmScopeFutureContext {
436    fn from(value: Context) -> Self {
437        Self::Owned(Box::new(value))
438    }
439}
440
441impl From<ManagedRefMut<Context>> for VmScopeFutureContext {
442    fn from(value: ManagedRefMut<Context>) -> Self {
443        Self::RefMut(value)
444    }
445}
446
447impl From<ManagedLazy<Context>> for VmScopeFutureContext {
448    fn from(value: ManagedLazy<Context>) -> Self {
449        Self::Lazy(value)
450    }
451}
452
453/// A [`VmScope`] driven by an executor instead of by a loop.
454///
455/// Each poll steps the scope up to `operations_per_poll` times. It reports
456/// [`Poll::Ready`](std::task::Poll::Ready) once the scope is done, and
457/// [`Poll::Pending`](std::task::Poll::Pending) when the scope suspends, when
458/// the poll budget runs out, or when the context cannot be borrowed right now.
459///
460/// The future never wakes itself, so the executor has to poll it again on its
461/// own.
462pub struct VmScopeFuture<'a, SE: ScriptExpression> {
463    /// The scope being stepped.
464    pub scope: VmScope<'a, SE>,
465    /// The context the scope runs on.
466    pub context: VmScopeFutureContext,
467    /// The registry the scope looks types and functions up in.
468    pub registry: RegistryHandle,
469    /// How many operations one poll runs at most.
470    pub operations_per_poll: usize,
471}
472
473impl<'a, SE: ScriptExpression> VmScopeFuture<'a, SE> {
474    /// Wraps `scope` into a future, with no limit on operations per poll.
475    pub fn new(
476        scope: VmScope<'a, SE>,
477        context: impl Into<VmScopeFutureContext>,
478        registry: RegistryHandle,
479    ) -> Self {
480        Self {
481            scope,
482            context: context.into(),
483            registry,
484            operations_per_poll: usize::MAX,
485        }
486    }
487
488    /// Sets how many operations one poll runs at most, builder style.
489    ///
490    /// A small number hands control back to the executor more often, so one
491    /// long script cannot hold up everything else.
492    pub fn operations_per_poll(mut self, value: usize) -> Self {
493        self.operations_per_poll = value;
494        self
495    }
496
497    fn step(&mut self) -> Option<VmScopeResult> {
498        match &mut self.context {
499            VmScopeFutureContext::Owned(context) => {
500                Some(self.scope.step(&mut *context, &self.registry))
501            }
502            VmScopeFutureContext::RefMut(context) => {
503                let mut context = context.write()?;
504                Some(self.scope.step(&mut context, &self.registry))
505            }
506            VmScopeFutureContext::Lazy(context) => {
507                let mut context = context.write()?;
508                Some(self.scope.step(&mut context, &self.registry))
509            }
510        }
511    }
512}
513
514impl<SE: ScriptExpression> Future for VmScopeFuture<'_, SE> {
515    type Output = ();
516
517    fn poll(
518        mut self: std::pin::Pin<&mut Self>,
519        _cx: &mut std::task::Context<'_>,
520    ) -> std::task::Poll<Self::Output> {
521        for _ in 0..self.operations_per_poll {
522            match self.step() {
523                None => return std::task::Poll::Pending,
524                Some(VmScopeResult::Completed) => return std::task::Poll::Ready(()),
525                Some(VmScopeResult::Suspended) => return std::task::Poll::Pending,
526                Some(VmScopeResult::Continue) => {}
527            }
528        }
529        std::task::Poll::Pending
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use crate::scope::*;
536    use intuicio_core::{
537        Visibility,
538        function::{Function, FunctionParameter, FunctionQuery, FunctionSignature},
539        script::{ScriptBuilder, ScriptFunction, ScriptFunctionParameter, ScriptFunctionSignature},
540        types::{TypeQuery, struct_type::NativeStructBuilder},
541    };
542    use intuicio_data::managed::Managed;
543
544    #[test]
545    fn test_async() {
546        fn is_async<T: Send + Sync>() {}
547
548        is_async::<VmScope<()>>();
549        is_async::<VmScopeFuture<()>>();
550        is_async::<VmScopeFutureContext>();
551    }
552
553    #[test]
554    fn test_vm_scope() {
555        let i32_handle = NativeStructBuilder::new::<i32>()
556            .build()
557            .into_type()
558            .into_handle();
559        let mut registry = Registry::default().with_basic_types();
560        registry.add_function(Function::new(
561            FunctionSignature::new("add")
562                .with_input(FunctionParameter::new("a", i32_handle.clone()))
563                .with_input(FunctionParameter::new("b", i32_handle.clone()))
564                .with_output(FunctionParameter::new("result", i32_handle.clone())),
565            FunctionBody::closure(|context, _| {
566                let a = context.stack().pop::<i32>().unwrap();
567                let b = context.stack().pop::<i32>().unwrap();
568                context.stack().push(a + b);
569            }),
570        ));
571        registry.add_function(
572            VmScope::<()>::generate_function(
573                &ScriptFunction {
574                    signature: ScriptFunctionSignature {
575                        meta: None,
576                        name: "add_script".to_owned(),
577                        module_name: None,
578                        type_query: None,
579                        visibility: Visibility::Public,
580                        inputs: vec![
581                            ScriptFunctionParameter {
582                                meta: None,
583                                name: "a".to_owned(),
584                                type_query: TypeQuery::of::<i32>(),
585                            },
586                            ScriptFunctionParameter {
587                                meta: None,
588                                name: "b".to_owned(),
589                                type_query: TypeQuery::of::<i32>(),
590                            },
591                        ],
592                        outputs: vec![ScriptFunctionParameter {
593                            meta: None,
594                            name: "result".to_owned(),
595                            type_query: TypeQuery::of::<i32>(),
596                        }],
597                    },
598                    script: ScriptBuilder::<()>::default()
599                        .define_register(TypeQuery::of::<i32>())
600                        .pop_to_register(0)
601                        .push_from_register(0)
602                        .call_function(FunctionQuery {
603                            name: Some("add".into()),
604                            ..Default::default()
605                        })
606                        .build(),
607                },
608                &registry,
609                None,
610            )
611            .unwrap()
612            .0,
613        );
614        registry.add_type_handle(i32_handle);
615        let mut context = Context::new(10240, 10240);
616        let (result,) = registry
617            .find_function(FunctionQuery {
618                name: Some("add".into()),
619                ..Default::default()
620            })
621            .unwrap()
622            .call::<(i32,), _>(&mut context, &registry, (40, 2), true);
623        assert_eq!(result, 42);
624        assert_eq!(context.stack().position(), 0);
625        assert_eq!(context.registers().position(), 0);
626        let (result,) = registry
627            .find_function(FunctionQuery {
628                name: Some("add_script".into()),
629                ..Default::default()
630            })
631            .unwrap()
632            .call::<(i32,), _>(&mut context, &registry, (40, 2), true);
633        assert_eq!(result, 42);
634        assert_eq!(context.stack().position(), 0);
635        assert_eq!(context.registers().position(), 0);
636    }
637
638    #[test]
639    fn test_vm_scope_future() {
640        enum Expression {
641            Literal(i32),
642            Increment,
643        }
644
645        impl ScriptExpression for Expression {
646            fn evaluate(&self, context: &mut Context, _registry: &Registry) {
647                match self {
648                    Expression::Literal(value) => {
649                        context.stack().push(*value);
650                    }
651                    Expression::Increment => {
652                        let value = context.stack().pop::<i32>().unwrap();
653                        context.stack().push(value + 1);
654                    }
655                }
656            }
657        }
658
659        let mut context = Managed::new(Context::new(10240, 10240));
660        let registry = RegistryHandle::default();
661
662        let script = ScriptBuilder::<Expression>::default()
663            .expression(Expression::Literal(42))
664            .suspend()
665            .expression(Expression::Increment)
666            .build();
667        let scope = VmScope::new(script, VmScopeSymbol::new());
668        let mut future = VmScopeFuture::new(scope, context.lazy(), registry);
669        let mut future = std::pin::Pin::new(&mut future);
670        let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
671        assert_eq!(context.write().unwrap().stack().position(), 0);
672
673        assert_eq!(future.as_mut().poll(&mut cx), std::task::Poll::Pending);
674        assert_eq!(
675            context.write().unwrap().stack().position(),
676            if cfg!(feature = "typehash_debug_name") {
677                28
678            } else {
679                12
680            }
681        );
682        assert_eq!(context.write().unwrap().stack().pop::<i32>().unwrap(), 42);
683        context.write().unwrap().stack().push(1);
684
685        assert_eq!(future.as_mut().poll(&mut cx), std::task::Poll::Ready(()));
686        assert_eq!(context.write().unwrap().stack().pop::<i32>().unwrap(), 2);
687    }
688}