SplitScript reference / SplitScript for Rust authors
SplitScript for Rust authors
SplitScript shares Rust's expression-oriented control flow, fixed-width
numbers, as casts, exhaustive match, postfix ?, and strong inference.
It removes ownership syntax and exposes autosplitter lifecycle and process
attachment directly, targeting WebAssembly GC rather than native code.
Keep these important spelling and semantic changes in mind:
- Rust
()becomes SplitScriptNone. - A Rust function tail expression becomes an explicit SplitScript
return. - Rust
impl Displaybecomes an inferred default or aType.toStringmethod.
Bindings and inferred capabilities
Use let without mut; ordinary bindings are mutable, while values such as
old snapshots and iteration elements are read-only by their role. Function
parameters and returns may be inferred from all uses. Generic behavior is
reported as capability bounds such as Numeric, Display, or
MemoryReadable, which play a trait-like role but are currently declared by
the standard library rather than user programs.
Irrefutable binding patterns destructure values in initialized let
declarations, function and closure parameters, and runtime for bindings.
This is close to Rust's irrefutable binding sites, including the fact that a
variant pattern becomes irrefutable when every alternative carries Never.
SplitScript has no let ... else; use is or match when another shape
can actually occur. A type annotation after the pattern describes the one
incoming value, not each name introduced by the pattern.
As in Rust, a concrete expected struct type permits { x, y } without writing
the struct name. This is contextual nominal shorthand, not structural typing;
an otherwise unconstrained parameter still needs an annotation or an explicit
Position { x, y } pattern.
User code does not need an impl Display for Type block. Structs and enums
derive a multiline Display representation automatically. Define
fn Type.toString() -> String only to override it; the result type may be
inferred. The derived or custom implementation powers interpolation, as
String, print, and setVariable.
struct Position {
x: i32,
y: i32,
}
fn Position.toString() { return `({self.x}, {self.y})` }
print(Position { x: 3, y: 5 })
fn greater(left, right) {
return left > right
}
print(greater(7u32, 3u32))
Arrays use [T] and [T; N], structs are GC product types, and enums support
payload variants. There are no lifetimes, moves, borrows, or explicit memory
management in source.
Blocks yield values, functions use return
Nested blocks are expression-oriented much like Rust. Their final expression
is their value, so an if branch, match arm, fallback, or argument can
perform local setup before producing a result:
fn levelLabel(isBoss: bool) -> String {
let label = if isBoss {
let kind = "Boss"
`{kind} level`
} else {
"Level"
}
return label
}
Unlike Rust, a function body does not implicitly return its tail expression.
Write return explicitly. This also applies to methods and lifecycle actions;
falling through an action uses that action's documented default. The compiler
recognizes the common Rust spelling, explains the distinction, and offers to
insert return. A semicolon on a value block's final expression is accepted
but warned about and removed by the formatter; it never silently changes the
block to None.
Loop expressions keep Rust's useful part
loop is expression-valued as in Rust. With no reachable break it has
type Never; break value determines its result, and a bare break produces
None. while and runtime for remain statement loops and reject
value-carrying breaks.
fn choose(flag: bool) -> i32 {
return loop {
if flag { break 7 }
break -1
}
}
The important difference is the explicit return: even when the final
expression is a loop, a SplitScript function does not return it implicitly.
Generators create ordinary iterators
SplitScript has synchronous generator functions even though stable Rust does
not have equivalent yield syntax. A call is lazy, and only next() advances
the body:
fn values(end: u32) -> iterator u32 {
let value = 0u32
while value < end {
yield value
value += 1
}
}
for value in values(3) {
print(value)
}
Unlike Rust's impl Iterator<Item = T>, the public iterator T spelling is an
erased cursor type. Arrays, ranges, sets, maps, adapters, and generators all
return it, so an ordinary function can forward any one of them with return.
Only an iterator T body that actually contains yield becomes a generator.
The cursor implements Iterator and Iterable, uses Item and End
instead of Option<T>, and aliases its position when copied. Generators are
synchronous: await, retry, fallible propagation, and value-returning
return require a different protocol and are rejected.
None is the unit type
None is both the language's zero-sized unit value and the absent side of
T?. Functions that only perform effects infer None; void and Rust's ()
are not source syntax. Plain T values promote into T? and T!, while
Some and Ok are used only to distinguish patterns.
fn parseCount(text: String) -> u32! {
return text.parse()
}
fn showCount(text: String) -> None {
let count = parseCount(text) else 0u32
print(count)
}
showCount("4")
Postfix ? propagates to the nearest state-field boundary or T! function.
Use else fallback for local recovery and match for explicit
Ok(value)/Err(message) handling.
Async values and cancellation
An asynchronous value has type async T. Named functions write that as
-> async T; lifecycle blocks infer suspension from await. Unlike an
executor-agnostic Rust future, attachment-owned work is automatically
cancelled when its process closes.
state "game.exe" {}
fn findImage() -> async Module {
return await process.module("GameAssembly.dll")
}
onAttach {
let image = await findImage()
print(image.address)
}
Use retry expression for synchronous fallible work that should be evaluated
again on later ticks until it succeeds. Unlike Rust's function-scoped ?,
retry establishes a local failure boundary, so a block can describe one
complete transaction:
onAttach {
let health = retry {
let player = process.read<address>(0x1000)?
process.read<i32>(player)?
}
print(health)
}
The braces are an ordinary value block, not special retry syntax. ?, a
final Err(...), or throw ends the current attempt and starts the complete
operand again on the next attached update. return, break, and
continue keep their lexical targets. One attempt must be synchronous and
bounded: it cannot evaluate await or another retry. Calling an async
function is allowed because it only constructs an async T; awaiting that
value is not. Use ordinary await when one asynchronous operation already
owns its polling policy.
Autosplitter domains
state owns attachment and memory polling. Its accepted values become the
transactional old and current snapshots. onAttach, onStateReady,
whileAttached, and onDetach describe process-lifetime phases. start,
split, reset, isLoading, and gameTime communicate timer decisions.
state "game.exe" {
level: u32 at 0x1000
}
split {
return old.level != current.level
}
Process reads are fallible and require a concrete MemoryReadable layout.
Prefer state pointer paths for values polled every tick and direct reads or
signature scans for attachment-time discovery. Settings are a typed
declaration DSL, not a map assembled at runtime.
Next step
Open Getting started from the documentation index and build its first autosplitter workflow. Use Search Documentation for exact capabilities, language forms, and migration concepts rather than assuming a Rust trait or runtime API has a one-to-one equivalent.