XScripTH is a statically-typed, command-based scripting language and execution engine built in C# for .NET. It is designed to evaluate scripts through a strict, sequential pipeline that parses text, validates types at compile-time, and executes asynchronous commands.
This document explains the internal mechanics of the engine and provides a comprehensive guide to the syntax structure of the language.
The lifecycle of an XScripTH script goes through four distinct phases before and during execution:
- Parsing (AST Generation)
The
XScriptParserlexes the raw script string into tokens and constructs an Abstract Syntax Tree (AST). It identifies command invocations, nested commands, blocks, and arguments. - Compilation & Compile-Time Execution
The
XScriptCompilertraverses the AST and binds text identifiers to actual C# classes using theCommandRegistry. During this phase, commands that implementICompileTimePhase(such asvar,func, orimport) are executed. This allows the compiler to populate the static symbol table (variables and function signatures) before the script ever runs. - Static Type Checking
Before execution, the
CommandTypeCheckerstatically analyzes the bound program. Every C# command defines its expected inputs and outputs via the[CommandTypes]attribute. The type checker traverses the script to ensure that literals, variables, and nested command outputs strictly match the expected input types of their parent commands. If a type mismatch occurs, compilation fails immediately. - Runtime Execution
The
XScripTHEngineexecutes the compiledICommandInvocationlist. It processes commands sequentially, resolving nested commands first, and maintains anXScriptExecutionContextto handle variable state and child scoping (e.g., insideiforwhileblocks).
XScripTH is purely command-driven. There are no operators like + or = natively in the language grammar; everything is a command that takes arguments (comma-separated) and returns outputs.
Every statement in XScripTH is a command invocation. Commands are invoked by their name, followed by an optional, comma-separated list of arguments, and must be explicitly terminated.
There are two terminators:
;(Await): Blocks the execution pipeline until the command completes.;;(Fire-and-Forget): Dispatches the command asynchronously in the background and immediately moves to the next instruction.
// Synchronous execution
print "Hello World";
// Asynchronous background execution
long-running-task;;
mark;
Arguments are separated by commas ,. The parser supports standard primitives, and infers their underlying .NET types. Numeric literals support C#-style suffixes to enforce specific types during static type checking.
- Strings:
"Hello"(string) - Chars:
'A'(char) - Booleans:
true,false(bool) - Integers:
42(Defaults toint) - Longs:
42lor42L(long) - Floats:
3.14f(float) - Doubles:
3.14d(double) - Decimals:
3.14m(decimal)
// Calling a 'capture' command with mixed literal arguments
capture "text", 'c', 42, 42l, 3.14f, true;
The output of one command can be passed directly as an argument to another command. Nested commands are written inline and must be terminated by a single semicolon ; to indicate the end of the nested expression.
// 'text' executes first. Its output is passed to 'length'.
// The first ';' closes the 'text' command. The second ';' closes the 'length' command.
length text; ;
// Nested commands can be mixed with literal arguments
surround "[", text;, "]";
Variables are strongly typed and resolved during the compile-time phase.
- You declare and assign a variable using the
varcommand. - You reference a variable using the
$prefix.
Because XScripTH evaluates var at compile-time, the type checker automatically knows what type $name is based on what you assigned to it.
// Declare a variable. The type is inferred as 'string'
var $message, "Hello";
// Use the variable
print $message;
// Assigning the output of a command to a variable
var $size, length $message; ;
Blocks { ... } allow you to group commands without executing them immediately. They are treated as an argument type (CommandBlockArgument) and are passed into control flow commands.
If a block needs to resolve to a value, you use the return command inside it.
// A block that echoes a single value
{ return 42; }
// A block containing multiple statements
{
print "Working...";
return true;
}
Functions in XScripTH are named, reusable deferred blocks.
- Declare a function using the
funccommand, providing a string name and a block. - Reference a declared function using the
@prefix.
Functions must be declared before they are referenced, as the compile-time symbol table reads top-to-bottom.
// Declare a function named "say_hello"
func "say_hello", {
return "Hello from function";
};
// Use the function reference as an argument
print @say_hello;
Standard control flow is implemented as built-in commands that accept blocks as inputs.
If Command: Takes a boolean condition and a block to execute if the condition is true.
if true, {
print "Condition met!";
};
While Command:
Takes two blocks: a condition block (which must return a bool) and a body block. The condition block is re-evaluated before every iteration.
var $counter, 0;
// Note: You would need custom commands to manipulate $counter,
// but the structural syntax looks like this:
while { return true; }, {
print "Infinite Loop";
};
XScripTH supports block-level scoping via XScriptExecutionContext. Variables declared inside a { ... } block shadow outer variables but do not leak into the parent scope once the block finishes executing.