Anatomy of a Script
Entire Script
structure is split into package, modules, functions/strucs/enums and at it's core: operations.
Example:
- Package:
- module:
test
- struct:
Foo
- field:
a
:bool
- field:
- enum:
Bar
- variant:
A
- variant:
B
- field:
a
:bool
- field:
- variant:
- function:
main
- output:
result
:i32
- operations:
- literal:
40
:i32
- literal:
2
:i32
- call function:
lib::add
- literal:
- output:
- struct:
- module:
Operations
Intuicio scripts by default have a very limited set of operations, because of being an universal interface between frontends and backends, these operations have to be the lowest denominator across most possible frontend and backend designs, therefore operations it haves are:
-
Expression
This is an additional set of possible operations defined by frontends. It accepts types that implement
ScriptExpression
that allows to evaluate additional operation using only context and registry. Common example of script expression is pushing literals onto stack - stack manipulation is not part of the script operations set, because Intuicio is completely generic over the data it moves, and specific frontends most likely do have very specific assumptions about the data they allow to use, so it's better to leave this kind of operations to be provided by them.Example of custom script expression:
#![allow(unused)] fn main() { pub enum CustomExpression { Literal(i32), StackDrop, } impl ScriptExpression for CustomExpression { fn evaluate(&self, context: &mut Context, _: &Registry) { match self { Self::Literal(value) => { context.stack().push(*value); } Self::StackDrop => { context.stack().drop(); } } } } }
-
Define register
Allocates space for new register of given type defined by
TypeQuery
. Remember to always define register before it gets used! Registers gets removed when scope where they were defined gets dropped. -
Drop register
Drops data behind given register.
-
Push from register
Takes data from given register and pushes it onto stack.
-
Pop to register
Takes data from stack and moves it to given register.
-
Move register
Moves data between two registers.
-
Call function
Uses associated
FunctionQuery
to find and invoke function from registry. -
Branch scope
Takes
bool
from stack and if it'strue
, it will execute Success operations set, otherwise Failure operations set if present. -
Loop scope
Executes set of operations in an loop so that before start of iteration it takes
bool
from stack, then if it'strue
, set of operations gets executed. Important note here is to remember to always pushbool
value (telling if next iteration should be executed), before we exit child scope. -
Push scope
Pushes new set of operations for execution, returning further execution to its parent only when child scope stops its execution.
-
Pop scope
Closest analogue can be
return
keyword, which forces scope to stop further operations execution. -
Continue scope conditionally
Does what pop scope does, but first takes
bool
from the stack and if it isfalse
, scope gets dropped.
Usually we leave producing script operations to frontends, but one can create them at any time by any means. One can even create them at runtime and call newly created function in place, without adding it to registry:
let function = Function::new( function_signature! { registry => mod test fn main() -> (result: i32) }, VmScope::<AsmExpression>::generate_function_body( ScriptBuilder::<AsmExpression>::default() .literal(AsmExpression::Literal(AsmLiteral::I32(2))) .literal(AsmExpression::Literal(AsmLiteral::I32(40))) .call_function(FunctionQuery { name: Some("add".into()), module_name: Some("lib".into()), ..Default::default() }) .build(), ®istry, None, ) .unwrap() .0 ); function.invoke(&mut context, ®istry); assert_eq!(context.stack().pop::<i32>().unwrap(), 42);
This example shows precisely how short is the path between scripts and backends - backends constructing script functions directly from script operations, and in case of VmScope
every time we call function generated by it, we directly execute these operations.