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
      • enum: Bar
        • variant: A
        • variant: B
          • field: a: bool
      • function: main
        • output: result: i32
        • operations:
          • literal: 40: i32
          • literal: 2: i32
          • call function: lib::add

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's true, 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's true, set of operations gets executed. Important note here is to remember to always push bool 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 is false, 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(),
        &registry,
        None,
    )
    .unwrap()
    .0
);

function.invoke(&mut context, &registry);
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.