SplitScript primarily targets the env imports used by the ASL v2 prototype
and the LiveSplit sandboxed Auto Splitting Runtime. Features that need a
standardized host facility use narrowly scoped WASI Preview 1 imports. The
monotonic Instant clock uses clock_time_get; whole-file reads use the
environment, preopen, path-open, descriptor-read, and descriptor-close APIs.
Cooperative module fingerprints additionally use descriptor seek and filestat
so each bounded poll can reopen and verify one stable file version.
This is an internal compiler/runtime contract, not part of the source-language
standard library or its editor documentation. src/abi.rs is the source of
truth for import IDs, Wasm signatures, ownership, lifetime rules, effects, and
summaries. The compiler emits its import section from that catalog, and the test
suite verifies the table below against the catalog renderer.
Proposed host capabilities and limitations discovered during real ports are
tracked separately in RUNTIME_EVOLUTION.md. This file
describes only the contract that generated modules implement today.
| Export | Type | Purpose |
|---|---|---|
_start |
() -> () |
Initialize module state and run the source setup block |
update |
() -> () |
Host ABI polling entry point; drives SplitScript lifecycle blocks and timer actions |
memory |
dynamically sized linear memory | Named runtime scratch regions followed by immutable UTF-8/signature data and growable host-string staging |
| Import | WebAssembly type |
|---|---|
timer_get_state |
() -> i32 |
timer_current_split_index |
() -> i64 |
timer_segment_splitted |
(i64) -> i32 |
timer_start |
() -> () |
timer_split |
() -> () |
timer_skip_split |
() -> () |
timer_undo_split |
() -> () |
timer_reset |
() -> () |
timer_set_game_time |
(i64, i32) -> () |
timer_pause_game_time |
() -> () |
timer_resume_game_time |
() -> () |
timer_set_variable |
(i32, i32, i32, i32) -> () |
runtime_set_tick_rate |
(f64) -> () |
clock_time_get |
(i32, i64, i32) -> i32 |
fd_prestat_get |
(i32, i32) -> i32 |
fd_prestat_dir_name |
(i32, i32, i32) -> i32 |
path_open |
(i32, i32, i32, i32, i32, i64, i64, i32, i32) -> i32 |
fd_read |
(i32, i32, i32, i32) -> i32 |
fd_seek |
(i32, i64, i32, i32) -> i32 |
fd_filestat_get |
(i32, i32) -> i32 |
fd_close |
(i32) -> i32 |
process_attach |
(i32, i32) -> i64 |
process_attach_by_pid |
(i64) -> i64 |
process_list_by_name |
(i32, i32, i32, i32) -> i32 |
process_detach |
(i64) -> () |
process_is_open |
(i64) -> i32 |
process_read |
(i64, i64, i32, i32) -> i32 |
process_get_module_address |
(i64, i32, i32) -> i64 |
process_get_module_size |
(i64, i32, i32) -> i64 |
process_get_module_path |
(i64, i32, i32, i32, i32) -> i32 |
process_get_path |
(i64, i32, i32) -> i32 |
process_get_memory_range_count |
(i64) -> i64 |
process_get_memory_range_address |
(i64, i64) -> i64 |
process_get_memory_range_size |
(i64, i64) -> i64 |
process_get_memory_range_flags |
(i64, i64) -> i64 |
runtime_print_message |
(i32, i32) -> () |
runtime_get_os |
(i32, i32) -> i32 |
runtime_get_arch |
(i32, i32) -> i32 |
user_settings_add_bool |
(i32, i32, i32, i32, i32) -> i32 |
user_settings_add_title |
(i32, i32, i32, i32, i32) -> () |
user_settings_add_choice |
(i32, i32, i32, i32, i32, i32) -> () |
user_settings_add_choice_option |
(i32, i32, i32, i32, i32, i32) -> i32 |
user_settings_add_file_select |
(i32, i32, i32, i32) -> () |
user_settings_add_file_select_name_filter |
(i32, i32, i32, i32, i32, i32) -> () |
user_settings_add_file_select_mime_filter |
(i32, i32, i32, i32) -> () |
user_settings_add_text_input |
(i32, i32, i32, i32, i32, i32) -> () |
user_settings_set_tooltip |
(i32, i32, i32, i32) -> () |
settings_map_load |
() -> i64 |
settings_map_free |
(i64) -> () |
settings_map_get |
(i64, i32, i32) -> i64 |
setting_value_free |
(i64) -> () |
setting_value_get_bool |
(i64, i32) -> i32 |
setting_value_get_string |
(i64, i32, i32) -> i32 |
The signed type of timer_current_split_index is a host encoding, not its
source-language type. timer.currentSplitIndex() maps every negative import
result to None and exposes every nonnegative result as Some(u64).
The compiler plans imports from reachable operations. Every autosplitter gets the small lifecycle baseline; optional facilities such as process reads, settings, logging, and the monotonic clock are imported only when reachable source needs them. Import identities and ordering remain deterministic through the ABI catalog.
File.readAllBytes and File.readAllText only request read rights. They require
absolute paths in the runtime's portable WASI namespace. Windows drive paths
map below /mnt/<drive>, while Linux and macOS paths gain the /mnt prefix.
Those mappings describe the currently mounted host filesystem, not a permanent
restriction on other WASI roots. The compiler matches each path to the longest
applicable preopened directory. Every successfully opened descriptor is closed
before the intrinsic returns, including read-error paths. readAllText
validates strict UTF-8 after the complete read; it does not perform lossy
replacement or normalize the contents.
Ordinary scripts import process_attach(name) and preserve the host's efficient
single-candidate attachment path. A script declaring selectProcess instead
imports process_list_by_name and process_attach_by_pid. The generated update
loop first queries the complete candidate count, grows linear memory, and then
requests the full PID list. A process-set change that outgrows that buffer is
retried on a later tick rather than selecting from a truncated list. Candidate
order is deliberately unspecified. Every candidate receives a temporary owned
handle; false or an uncaught selector error detaches it immediately, while
true promotes it to the one compiler-owned attachment lifetime. PIDs and
temporary handles never enter the source language.
_start initializes globals and GC state, registers the complete settings GUI
including nested titles, tooltips, choices, and file filters, loads the initial
settings snapshot, and then invokes the source setup block exactly once.
The LiveSplit runtime retains this export during instantiation and calls it at
the beginning of the first controlled update, when its interrupt handle is
already available. setup is compiled as a synchronous () -> () internal
function and cannot observe a process provider or state snapshot. Every
exported update call loads a settings map before running lifecycle or attached
user code, decodes it into typed GC/global values,
and frees all temporary host handles. Choice strings become payloadless enum
variants and selected paths become GC strings. The preceding tick remains
available as oldSettings.
When process liveness fails, update detaches and clears the process handle,
provider-specific state, attachment-scoped globals, ready flags, and
attachment-lifetime continuations. A source-defined emulator-provider probe can
end the same logical attachment without releasing the still-open host process.
Either path invokes onDetach exactly once after a completed attachment and
returns. The detach action is compiler-generated lifecycle behavior and
requires no additional host callback or ABI import.
These explicit frees are an implementation detail of the current C-shaped
env ABI, not a desired SplitScript ownership model. New host-owned collection
and value APIs should follow the GC-managed externref direction recorded in
RUNTIME_EVOLUTION.md instead of expanding the manual-handle surface.
The compiler derives GC layouts and type indices from the reachable catalog and
source program; consumers must not rely on fixed numeric indices. Eight- and
sixteen-bit fields use packed GC fields, while wider numbers and references use
their native value types. Standard-library structs such as Duration and
Instant, source structs and enums, constructed arrays, wrapper types, state
snapshots, and an attach continuation frame are included only as required.
String backing arrays are internally mutable so decoders and formatters can
construct dynamically sized values, but the source language exposes strings as
immutable values.
Signature needles and masks are parsed by the compiler and stored in static
linear-memory data. A generated scanner reads the target module through
process_read in overlapping 4 KiB chunks. They overlap by the pattern length
minus one so matches spanning a page boundary are not lost.
Suspending lifecycle blocks lower to internal poll functions. onAttach uses
(process: i64) -> i32; a suspending whileAttached keeps its ordinary two
snapshot parameters but uses the same i32 poll result. Zero means pending and
one means the invocation completed. The source-level whileAttached Boolean is
stored separately so readiness cannot be confused with its timer-decision gate.
The continuation frame program counter selects generated entry, poll, and
continuation states; successful polls redispatch within the same call.
The exported update loop checks process liveness before every poll
and consumes the lowered attachment-lifetime cancellation region on logical detach,
providing structured cancellation without exposing continuation management to
the source language.
When a lifecycle block contains suspension, the compiler adds its live values
to a mutable GC continuation struct. Its first field is the resume program
counter; disjoint action layouts occupy deterministic slots because
onAttach and whileAttached can never be live concurrently. Replacing this
frame on process exit cancels the old continuation and clears its state.
State initialization requires one poll in which every required field succeeds.
The resulting GC object initializes both old and current, then the compiler
invokes synchronous onStateReady once and returns without running
whileAttached or timer-decision actions. Later refreshes populate a new GC
state object field by field. Successful results advance, while failed results
copy that field from current; unrelated fields can therefore advance
independently. Detachment clears the ready flag, so a later attachment repeats
this initialization boundary.
The internal synchronous whileAttached function directly returns its i32
gate. A suspending form returns poll readiness and stores that gate in a private
global on completion. At most one invocation exists per attachment and it is
polled once after each state refresh. Pending returns from the exported update
before timer decisions. Ready resets the continuation for the next update, then
applies the stored gate: fallthrough or true continues, while false skips
the remaining decisions. The committed state and settings snapshots remain
advanced in either case.
The module includes a splitscript custom section containing UTF-8 JSON with
the compiler package version, optional full Git revision, GC target, and host
ABI. The same compiler identity is reported by native frontends and the
embedded compiler service, so an artifact can be traced back to its producer.