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}