ad_astra/interpret/
function.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Ad Astra", an embeddable scripting programming       //
3// language platform.                                                         //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/ad-astra/blob/master/EULA.md               //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::mem::take;
36
37use ad_astra_export::export;
38use lady_deirdre::sync::Shared;
39
40use crate::{
41    interpret::{engine::is_trusted, stack::Stack, Assembly},
42    runtime::{
43        ops::{DynamicType, ScriptBinding, ScriptClone, ScriptInvocation},
44        Arg,
45        Cell,
46        Downcast,
47        InvocationMeta,
48        Origin,
49        Provider,
50        RuntimeError,
51        RuntimeResult,
52        ScriptType,
53        TypeHint,
54        Upcast,
55        __intrinsics::FUNCTION_FAMILY,
56    },
57};
58
59/// Assembly code for the Ad Astra Virtual Machine, ready for execution.
60///
61/// You can create this object using the
62/// [compile](crate::analysis::ModuleRead::compile) function, and then run it
63/// using the [ScriptFn::run] function.
64///
65/// The ScriptFn object is cheap to [Clone]. In the case of cloning, each clone
66/// shares the same assembly code memory, but the execution
67/// [context](ScriptFn::set_context) is unique to each clone.
68///
69/// ## Virtual Machine Design Overview
70///
71/// The assembly code design is currently an implementation detail and is
72/// subject to continuous improvements, optimizations, and changes in future
73/// minor versions of Ad Astra. For this reason, the crate API does not provide
74/// direct access to manually alter the assembly code. However, for debugging
75/// purposes, you can print the internals to the terminal using the [Debug]
76/// implementation of the ScriptFn object.
77///
78/// The ScriptFn consists of Ad Astra assembly commands for the main script
79/// module function (the top-level source code of a module itself serves as the
80/// body of a function with zero parameters), as well as the assembly commands
81/// for other script functions from this module.
82///
83/// The runtime executes each assembly command of the script function
84/// sequentially. Some commands can conditionally or unconditionally jump to
85/// other commands in the list.
86///
87/// The commands interact with the stack of the current thread by pulling some
88/// [Cells](Cell) from the stack and pushing new Cells onto the stack.
89/// Therefore, the Virtual Machine is a stack-based machine.
90///
91/// ## Isolation
92///
93/// Each assembly command is evaluated in a virtual environment. If for any
94/// reason a command fails, the Virtual Machine immediately stops execution and
95/// returns a [RuntimeError] from the [ScriptFn::run] function.
96///
97/// You can manually interrupt script function execution using the hook
98/// mechanism. By setting a hook function with the
99/// [set_runtime_hook](crate::interpret::set_runtime_hook) function, you enforce
100/// the Virtual Machine to report every command execution to the hook. The hook,
101/// in turn, can return `false` to signal the Virtual Machine to stop execution
102/// and return from [ScriptFn::run] with a [RuntimeError::Interrupted] error.
103///
104/// ```rust
105/// use ad_astra::interpret::set_runtime_hook;
106///
107/// set_runtime_hook(|_origin| true);
108/// ```
109///
110/// The hook function is configured per OS process thread. By default, the
111/// thread from which you call the [ScriptFn::run] function does not have a
112/// configured hook, meaning that you trust the script to finish its job without
113/// interruptions. In this trusting mode, script functions are executed slightly
114/// faster than with a configured hook, but the downside is that you cannot
115/// revoke control flow back to Rust until the Virtual Machine finishes its job.
116/// This could be an issue, for example, if the script code contains
117/// unconditional infinite loops.
118///
119/// Additionally, the hook function receives an [Origin] object as an argument
120/// that roughly points to the original source code statements and expressions
121/// of the script module that are about to be evaluated. You can use this
122/// feature to organize interactive script evaluation.
123///
124/// ## Source Maps
125///
126/// In addition to the assembly commands, the ScriptFn object also holds a
127/// mapping between the assembly commands and the source ranges from which these
128/// commands were compiled.
129///
130/// The Virtual Machine uses this metadata to provide proper and descriptive
131/// [runtime errors](RuntimeError) if a script execution flow ends with a script
132/// evaluation error.
133///
134/// ## Concurrent Evaluation
135///
136/// Each script function is executed on the current OS thread from which
137/// it was [run](ScriptFn::run).
138///
139/// The Ad Astra base language does not provide a built-in mechanism for
140/// asynchronous script evaluation or thread management. However, you can
141/// organize a multi-threaded execution environment depending on your design
142/// goals using the export system.
143///
144/// For example, you can export a function from Rust to a script that takes
145/// another function as a parameter (e.g., [Fn0](crate::runtime::ops::Fn0)).
146/// In the script, the author can call this exported Rust function, passing
147/// a script-defined function as an argument. The Rust function can then execute
148/// the provided script function in another thread.
149///
150/// ```rust
151/// # use std::thread::spawn;
152/// #
153/// # use ad_astra::{export, runtime::ops::Fn0};
154/// #
155/// # #[export(include)]
156/// # #[export(package)]
157/// # #[derive(Default)]
158/// # struct Package;
159/// #
160/// #[export]
161/// pub fn foo(f: Fn0<()>) {
162///     spawn(move || {
163///         let f = f;
164///
165///         let _ = f();
166///     });
167/// }
168/// ```
169#[derive(Clone)]
170pub struct ScriptFn {
171    pub(super) assembly: Shared<Assembly>,
172    pub(super) closures: Vec<Cell>,
173    pub(super) subroutines: Shared<Vec<ScriptFn>>,
174}
175
176impl Default for ScriptFn {
177    #[inline(always)]
178    fn default() -> Self {
179        Self {
180            assembly: Shared::default(),
181            closures: Vec::new(),
182            subroutines: Default::default(),
183        }
184    }
185}
186
187impl ScriptFn {
188    /// Evaluates the script.
189    ///
190    /// The function returns the evaluation result in the form of a [Cell],
191    /// representing an object returned by the script using the `return 100;`
192    /// statement. If the script does not return any value, the function returns
193    /// [Cell::nil].
194    ///
195    /// If the script encounters a runtime error during execution, the function
196    /// halts script execution immediately and returns a [RuntimeError].
197    ///
198    /// If the script execution is interrupted by the execution hook (configured
199    /// via [set_runtime_hook](crate::interpret::set_runtime_hook)), this
200    /// function returns a [RuntimeError::Interrupted] error.
201    ///
202    /// By default, the current OS thread does not have a script hook, meaning
203    /// that the Virtual Machine will execute the script until the end of the
204    /// script's control flow.
205    #[inline(always)]
206    pub fn run(&self) -> RuntimeResult<Cell> {
207        let assembly = self.assembly.as_ref();
208
209        let parameters = assembly.arity;
210
211        if parameters > 0 {
212            return Err(RuntimeError::ArityMismatch {
213                invocation_origin: Origin::nil(),
214                function_origin: assembly.decl_origin(),
215                parameters,
216                arguments: 0,
217            });
218        }
219
220        match is_trusted() {
221            true => self.execute::<true>()?,
222            false => self.execute::<false>()?,
223        }
224
225        Ok(Stack::pop_1(0))
226    }
227
228    /// Sets the value of the `self` script variable, allowing the module's
229    /// source code to read script input data.
230    ///
231    /// You can create the [Cell] using the [Cell::give] constructor by passing
232    /// a value of any type known to the Ad Astra Runtime (either any built-in
233    /// Rust type or any type exported using the [export](crate::export) macro).
234    ///
235    /// Each clone of the ScriptFn object may have a unique context value, but
236    /// the context can only be set once per ScriptFn instance. Subsequent calls
237    /// to the `set_context` function will not change the previously set
238    /// context.
239    ///
240    /// By default, the ScriptFn instance does not have an evaluation context,
241    /// and the `self` variable is interpreted as "nil" within the script code.
242    ///
243    /// Note that the script's `self` variable is generally mutable if the type
244    /// of the value supports mutations (e.g., number types are mutable). Thus,
245    /// the `self` script variable can serve as both a data input and output
246    /// channel. If the value you set as the context is intended as the script's
247    /// output, consider reading this value after the script
248    /// [evaluation](Self::run) using the [get_context](Self::get_context)
249    /// function.
250    #[inline(always)]
251    pub fn set_context(&mut self, context: Cell) {
252        let Some(this) = self.closures.get_mut(0) else {
253            return;
254        };
255
256        *this = context;
257    }
258
259    /// Provides access to the value of the script's `self` variable, as
260    /// previously set by the [set_context](Self::set_context) function.
261    ///
262    /// By default, if the ScriptFn instance does not have an evaluation context
263    /// value, this function returns [Cell::nil].
264    #[inline(always)]
265    pub fn get_context(&self) -> &Cell {
266        static NIL: Cell = Cell::nil();
267
268        let Some(this) = self.closures.get(0) else {
269            return &NIL;
270        };
271
272        this
273    }
274}
275
276/// A script function.
277#[export(include)]
278#[export(name "fn")]
279#[export(family &FUNCTION_FAMILY)]
280pub(crate) type ScriptFnType = ScriptFn;
281
282impl<'a> Downcast<'a> for ScriptFnType {
283    #[inline(always)]
284    fn downcast(origin: Origin, provider: Provider<'a>) -> RuntimeResult<Self> {
285        let mut type_match = provider.type_match();
286
287        if type_match.is::<ScriptFnType>() {
288            return provider.to_owned().take::<ScriptFnType>(origin);
289        }
290
291        return Err(type_match.mismatch(origin));
292    }
293
294    #[inline(always)]
295    fn hint() -> TypeHint {
296        TypeHint::Type(ScriptFnType::type_meta())
297    }
298}
299
300impl<'a> Upcast<'a> for ScriptFnType {
301    type Output = Box<ScriptFnType>;
302
303    #[inline(always)]
304    fn upcast(_origin: Origin, this: Self) -> RuntimeResult<Self::Output> {
305        Ok(Box::new(this))
306    }
307
308    #[inline(always)]
309    fn hint() -> TypeHint {
310        TypeHint::Type(ScriptFnType::type_meta())
311    }
312}
313
314#[export(include)]
315impl ScriptClone for ScriptFnType {}
316
317#[export(include)]
318impl ScriptInvocation for ScriptFnType {
319    fn invoke(origin: Origin, lhs: Arg, arguments: &mut [Arg]) -> RuntimeResult<Cell> {
320        let function = lhs.data.take::<ScriptFnType>(origin)?;
321        let assembly = function.assembly.as_ref();
322
323        let arguments_count = arguments.len();
324        let parameters_count = assembly.arity;
325
326        if arguments_count != parameters_count {
327            return Err(RuntimeError::ArityMismatch {
328                invocation_origin: origin,
329                function_origin: assembly.decl_origin(),
330                parameters: parameters_count,
331                arguments: arguments_count,
332            });
333        }
334
335        for arg in arguments {
336            let cell = take(&mut arg.data);
337
338            Stack::push(cell);
339        }
340
341        match is_trusted() {
342            true => function.execute::<true>()?,
343            false => function.execute::<false>()?,
344        }
345
346        Ok(Stack::pop_1(0))
347    }
348
349    #[inline(always)]
350    fn hint() -> Option<&'static InvocationMeta> {
351        None
352    }
353}
354
355#[export(include)]
356impl ScriptBinding for ScriptFnType {
357    type RHS = DynamicType;
358
359    fn script_binding(_origin: Origin, mut lhs: Arg, rhs: Arg) -> RuntimeResult<()> {
360        if rhs.data.is_nil() {
361            return Ok(());
362        }
363
364        let function = lhs.data.borrow_mut::<ScriptFnType>(lhs.origin)?;
365
366        let Some(this) = function.closures.get_mut(0) else {
367            return Ok(());
368        };
369
370        if this.is_nil() {
371            *this = rhs.data;
372        }
373
374        Ok(())
375    }
376}