This document records semantics that SplitScript needs from the LiveSplit
autosplitting runtime but that the current WebAssembly host contract cannot
express cleanly. It is the runtime-facing requirement ledger: language and
compiler work stays in TODO.md, while the implemented import
contract stays in ABI.md.
Requirements discovered while porting ASL scripts belong here when satisfying them requires new or changed host behavior. A compiler workaround should not silently become the permanent design when the missing fact or lifetime is owned by LiveSplit.
- Preserve the existing
envABI for existing Wasm autosplitters. Add new facilities through a versioned, feature-detectable contract rather than changing an existing signature in place. - Describe semantic guarantees before choosing import names or raw encodings.
- Prefer opaque, unforgeable host references over guest-visible integer handles.
- Do not expose manual allocation, copying, or
freeoperations in the SplitScript language when the runtime can own that lifetime safely. - Keep deterministic cleanup for resources whose prompt release is observable. Garbage collection is a safety and ownership mechanism, not a scheduling guarantee.
- Preserve the runtime's responsiveness contract. APIs that enumerate
processes, modules, mappings, or scan memory need bounded or suspending forms
so one
updatecannot monopolize the autosplitting thread. - Every promoted facility needs a runtime conformance fixture covering success, absence/failure, ownership, ordering, and cancellation.
Priority: P0
Status: Proposed runtime direction; import namespace and exact signatures are intentionally undecided.
The current ABI represents SettingsMap, SettingsList, SettingValue, and
attached processes as nonzero integer handles. Most settings operations return
owned handles that the guest must copy and free explicitly. That is appropriate
for a C-shaped ABI, but it is a poor source-language contract: WebAssembly GC
structs do not provide user-defined finalizers, so the compiler cannot attach a
reliable settings_map_free call to an arbitrary GC object's lifetime.
Missed frees would leak host resources. Eager frees would be unsafe when values are aliased, stored in globals, captured by an async continuation, or returned from functions. Requiring script authors to manage these lifetimes would also undermine the scripting-language ergonomics.
Add a parallel, versioned host ABI whose resource-bearing values use nullable
externref rather than guest-forgeable integers. Wasmtime describes externref
as an opaque, GC-managed reference to host data.
The guest can retain and pass the value but cannot inspect or forge its host
representation.
The first candidates are:
- settings maps, lists, and setting values;
- immutable snapshots returned by loading the global settings map;
- copies or persistent updates derived from those snapshots;
- optional manually attached process resources, if SplitScript later permits
attachment outside its compiler-managed
statelifetime.
The high-level SplitScript types remain nominal and typed even though all host
objects share the Wasm externref representation. Each import must validate the
expected host kind. A null reference represents absence or failure only where
the declared SplitScript type is optional or fallible.
The GC-native settings surface should preserve the full semantics of the existing ABI:
- recursive maps, lists, booleans,
i64,f64, and strings; - value and collection copies that do not alias mutable state unexpectedly;
- insertion and indexed access without guest-visible ownership transfer;
- atomic
storeIfUnchanged(old, new)behavior so UI changes are not lost; - no source-visible
copymerely for lifetime management and nofreeat all.
Prefer immutable snapshots and persistent-style update operations unless a mutable design can specify aliasing and concurrent frontend changes just as clearly. UTF-8 strings may continue to cross linear memory initially; replacing every byte-buffer API is not required to gain safe resource ownership.
An externref allows Wasmtime to reclaim its host data after neither Wasm nor
the embedder roots it. This still requires the LiveSplit runtime to scope its
own roots correctly and to allow or trigger collection. Conformance tests must
prove that repeatedly loading and discarding settings does not retain every
historical object.
Collection timing is nondeterministic. Settings values can normally tolerate
that. An attached process owns operating-system resources and should still be
released deterministically by SplitScript's existing process-lifetime region.
If arbitrary attachment is added later, its design needs a scoped lifetime or
an explicit idempotent close; an unreachable externref may provide fallback
cleanup but must not be the only way to detach promptly.
Host objects should not form ownership cycles unless the runtime also provides a way to collect those cycles. The design must document behavior at store shutdown, hot reload, trapped updates, and cancelled async work.
- A script can retain and compose settings maps, lists, and values without any explicit lifetime operation.
- Dropping all guest and embedder references eventually releases the host data.
- Wrong-kind references produce a controlled host error rather than a trap or unsafe downcast.
- Legacy
envmodules continue to run unchanged. - The compiler can select the GC-native ABI by an explicit supported-version or feature contract, not by guessing from a failed instantiation.
Priority: P1
Status: Required semantics known; transport undecided.
The current runtime calls only the Wasm update export. SplitScript can sample
timer_get_state before process attachment on every update, so onStart and
onReset are useful while detached and also observe changes initiated outside
the script. This remains sampled rather than lossless: a start and reset that
both occur between updates collapse to no visible transition. Polling
timer_current_split_index has a more serious ambiguity because it cannot
distinguish an undo followed by another split between two updates. Diagnostic
and bookkeeping onSplit callbacks are therefore intentionally omitted rather
than approximated.
The runtime should eventually expose an ordered, lossless timer-event contract.
This could be callback exports or a sequenced event queue consumed during
update; the semantic requirements matter more than that choice:
- starts, splits, skips, undos, resets, and externally initiated actions are observable exactly once and in order;
- events are delivered even when no process or emulator is attached;
- the split event specifies its segment identity and whether it is observed before or after advancement;
- ordering relative to process attachment, detachment, snapshot refresh, and game-time updates is explicit;
- reentrancy and suspension behavior are defined;
- a sequence number makes missed or duplicated delivery testable.
SplitScript's sampled onStart and onReset actions require no new export and
remain zero-cost when unused. Exact lossless variants and onSplit still need
this host contract; the compiler must not invent exports that the runtime does
not call.
Priority: P1, promoted by port evidence
Status: Same-name PID selection and deterministic module fingerprints are implemented; unknown-name module enumeration remains.
Ports need more stable process identity than an executable name in several cases. Candidate host-owned facilities are:
- Same-name process enumeration now uses the runtime's official
process_list_by_nameandprocess_attach_by_pidABI behind the compiler-ownedselectProcesslifecycle block. Source sees only a temporaryprocess; PIDs, handles, enumeration buffers, and detach ownership remain backend details. Host order is unspecified, so predicates must use stable process evidence rather than relying on start order. - module enumeration for scripts that genuinely inspect unknown names. Known
required names use the waiting
process.module(name)API, while known optional names now use synchronousprocess.loadedModule(name)over the existing address/size imports; - product/file version metadata;
- deterministic executable/module fingerprints use
Module.md5(). The compiler hashes exact whole-file bytes through bounded 512 KiB WASI reads, closes its descriptor before every yield, restarts on observed file changes, and completes as canonical uppercase hexadecimal. COTM 1.1.0/1.1.2 and COTM2 1.2.2/1.3.1 are the concrete evidence: their ASL sources select same-name layouts by known whole-file MD5 values, and COTM documents identical module sizes; - path/name metadata for mapped memory ranges. SplitScript now snapshots the
existing count/index ABI synchronously into GC-owned
MemoryRangevalues with readable, writable, and executable flags; enumerating this cheap host metadata does not need the cooperative scheduling required by memory scans.
Before expanding discovery, settle and document the attachment-name contract:
whether matching uses the full executable filename, how case is handled, and
what name is reported after a match on each host OS. Legacy ASL commonly omits
Windows' .exe, while extensionless names are normal on Linux and macOS. Do
not add a compiler warning until deciding whether the host should normalize
executable identity portably, accept target-specific aliases, or retain exact
literal names. Whichever policy is chosen needs stable attachment fixtures on
all three hosts rather than accidental platform behavior.
Any collection-shaped result should use the GC-managed resource direction from
R1 or, when the operation itself is expensive, a bounded visitor/polling
contract. It should not introduce another
integer handle plus manual free family. Long-running discovery must cooperate
with the runtime's hanging-autosplitter threshold and process cancellation.
Priority: P1, promoted by port evidence
Status: Basic declarations are supported; richer frontend semantics remain to be proven by ports.
The existing host supports booleans, headings, free-form text inputs, choices, file selection, filters, tooltips, and a global settings map. Future ports may require conditional visibility or enablement and repeated/table-shaped settings. Those features need a frontend/runtime contract, not merely new SplitScript syntax. The maintained Axiom Verge port provides concrete semantics: its category checkboxes recursively gate child values in legacy ASL. SplitScript preserves that behavior explicitly in source, but the current host cannot present those checkboxes as the visual and interactive parents of nested controls.
The bulk ASL campaign found finite loops that register 35, 50, or 110 boolean
settings and then look them up by string key. A compile-time repeated/table
declaration can solve those finite cases without runtime mutation and belongs
in TODO.md. Runtime-created settings are justified only when keys or labels
are genuinely discovered from game data or otherwise unbounded. For that case,
the host must define when additions become visible, how ordering and hierarchy
are preserved, what happens to persisted values for temporarily absent keys,
and whether registration is permitted after startup.
Dynamic settings mutation must compose with concurrent changes from the LiveSplit UI. R1's GC-native map API therefore needs to retain the current compare-and-store behavior rather than replacing it with an unconditional overwrite.
Priority: P1, promoted by repeated port evidence
Status: Existing narrow operations are supported; broader semantics and transport are undecided.
The current contract exposes timer state, current split index, whether a
segment was split, timer actions, game-time publication/pause/resume, and
variables. This already covers ordinary start/split/reset, load removal,
and many one-shot guards. It does not expose the nested LiveSplit objects that
legacy scripts use through timer.CurrentTime, timer.CurrentSplit.Name,
timer.Run.Offset, timing-method selection, or attempt/category metadata.
The porting campaign found three different needs that must not be collapsed
into one untyped timer escape hatch:
- process-independent debounce and delay logic, which should use SplitScript's
existing monotonic
Instant.now()and requires no new timer ABI; - read-only observations of timer real time, game time, current segment identity, run metadata, and timing method;
- controlled mutations such as changing run offset or timing method, whose ordering and user-visible effects need an explicit host contract.
A future versioned ABI should first expose typed optional values when LiveSplit
has no current attempt, split, or game time. The current autosplitting contract
does not expose these observations at all, so their SplitScript spelling and
representation remain deliberately undecided. LiveSplit Core's internal
Timer::snapshot() freezes the clock while one calculation observes it; that
does not by itself justify freezing all autosplitter-visible timer state for an
entire update. Do not add a source-level or update-wide snapshot mechanism
without a concrete runtime contract and use case. Mutations must specify
whether subsequent reads observe them immediately, how they interact with
reset/start/split events, and whether user UI changes win. Run-offset units,
sign, precision, persistence, and undo/reset behavior must be explicit. Source
APIs should be least-privilege: reading metadata must not grant arbitrary run
mutation.
Timing-method prompts and message boxes are frontend interactions, not timer state. They require a separate consent-aware UI design and may remain outside the sandbox; a port must not silently claim parity merely because it omitted the prompt.
Acceptance tests should cover absence before a run, active and paused timers,
segment changes, reset, positive and negative offsets, timing-method changes,
and ordering relative to whileAttached, timer-decision actions, game-time
publication, and the eventual exact-event contract in R2.
Priority: P1, promoted by lifecycle-port evidence
Status: Required semantic boundary known; no host export exists.
ASL shutdown runs when the autosplitter is disabled, LiveSplit exits, the
script path changes, or the script is reloaded. It is script-instance teardown,
not process detachment. SplitScript can detect an attached process closing
inside update, but generated Wasm cannot run code after the host simply drops
the store or module instance.
If maintained ports prove teardown side effects necessary, the host should invoke a dedicated export before discarding a SplitScript instance. The contract must define ordering relative to process-exit handling, settings and timer events, debug-watch replacement, traps, cancellation of suspended work, and host shutdown. It must also state whether teardown has a bounded execution budget and whether it may suspend; prompt resource cleanup should normally be compiler/runtime-owned rather than left to user code.
Do not map ASL shutdown to onDetach: the latter runs after each process
closes, while shutdown may run once with no current process
and may observe only partially available historical process state.
Priority: P2 / deliberately deferred
Status: Design direction worth preserving; no implementation commitment until a maintained port or public host API requires user-owned resources.
R1 records a host-GC direction for settings values whose cleanup need not be prompt. A complementary solution may be needed for files, manually attached processes, or other resources whose release is observable and must be deterministic. Core Wasm GC objects have no user-defined finalizer, so merely placing an owned host handle inside a GC struct cannot arrange a reliable close when that struct becomes unreachable.
SplitScript could instead add an affine resource category. A resource leaf
would be an opaque host handle with trusted destruction behavior. Any type that
transitively contains a resource would derive an internal NeedsDrop property
and would not be Copy, much like a Rust aggregate containing a non-Copy
field:
resource File
struct Request {
file: File,
path: String,
}
Request, Request?, Request!, and [Request] would all require
deterministic drop. Assignment would move ownership rather than create an
untracked GC alias, while ordinary method calls and non-consuming parameters
could borrow implicitly. The compiler would generate structural drop glue for
structs, active enum variants, options, results, and initialized collection
elements. It would invoke that glue on normal scope exit, overwrite, return,
throw, break, continue, and async completion or cancellation. Returning
or otherwise transferring a value would suppress the source owner's drop.
The Wasm GC allocation could remain alive until a later collection; only the
external resource is released by deterministic drop. This does not require
solving general GC reachability because the type system maintains one tracked
owner. It does require resource-bearing aggregates to lose ordinary freely
aliased GC-reference semantics. A resource-bearing value must not be hidden
inside another freely copied GC object: the non-Copy/NeedsDrop property has
to propagate through every owning edge.
An initial design should avoid partial field moves, arbitrary resource-bearing GC graphs, and implicit shared ownership. Generated async frames may own moved resources only when every completion and cancellation path runs their drop glue. The host should additionally retain an instance- or attachment-scoped resource table that forcibly closes remaining entries after a trap, cancelled instance, or failed teardown; compiler-generated normal-flow cleanup cannot be the sole leak barrier.
Not every nominal host value should be owned. The process supplied by a normal
state declaration is naturally borrowed from the attachment lifetime, and
the ordinary settings view can remain host-owned. Explicit file opening,
manual attachment, or a settings builder consumed by registration are better
candidates for owned resource values.
Before implementing this direction, settle:
- how moves and implicit borrows appear in diagnostics, function signatures, returns, globals, and editor type information;
- whether
T?/T!receive specialized resource drop glue before user structs and variable-length collections; - how current aliasable arrays, sets, structs, and closures are restricted when they transitively contain a resource;
- the exact non-suspending, non-failing destructor ABI and its behavior during traps, hot reload, and store destruction;
- whether a later explicit shared-resource wrapper is justified, rather than making reference counting and its cycle rules part of every resource; and
- how closely the ABI should mirror Component Model
own/borrowresources, even if SplitScript continues to emit a Core Wasm GC module.
This remains lower priority than APIs needed by maintained autosplitters. Its
purpose is to preserve a plausible deterministic ownership model so the lack
of Wasm GC finalizers does not force source-visible free calls or premature
commitment to nondeterministic cleanup.
Priority: P1 / runtime correctness
Status: The frequency unit and transition timing are proven; invalid-value and scheduler bounds need a host fix.
runtime_set_tick_rate receives updates per second and stores its reciprocal
as the wait duration used after the current update. The selected interval is
persistent, including across process detachments. SplitScript therefore owns
its policy explicitly through setup, onDetach, and onAttach; the host must
not invent a process-lifecycle reset.
The current host rejects non-positive finite inputs but does not reject every
non-finite value. NaN can reach the stored interval and fail later when it is
converted to a duration; positive infinity produces a zero interval and can
turn the runner into a busy loop. Extremely small or large positive values
also need an explicit scheduling policy rather than relying on floating-point
reciprocal edge cases.
The existing import signature can retain its f64 parameter, but the host
implementation should:
- reject
NaN, both infinities, zero, and negative values before publishing a new interval; - define finite minimum and maximum frequencies that keep the resulting wait representable and prevent an accidental busy loop or impractically long sleep;
- leave the previous valid interval unchanged when validation fails;
- guarantee that reading the current interval is total and cannot panic; and
- test the initial 120 Hz interval, 60/100/120 Hz updates, invalid inputs, and persistence across ordinary process detachments.
Until that host validation is tightened, SplitScript documents a positive, finite source contract and emits ordinary calls without duplicating scheduler policy in generated Wasm.
Priority: P2 / blocked-port evidence
Status: Semantic requirement recorded; no raw process-mutation API is proposed.
The Amnesia: A Machine for Pigs and Amnesia: The Dark Descent ASL scripts derive loading state by modifying the game process. They scan executable code, allocate an executable buffer, copy displaced instructions, install jumps that write a synthetic flag, suspend and resume the process around modification, and restore all bytes and allocations during shutdown. Ordinary signatures, reads, layouts, and lifecycle state cover the rest of these scripts but cannot observe the transient events captured by that instrumentation.
A future host solution must provide the loading information without allowing an autosplitter instance to leave the game patched. Its semantic requirements are:
- event observation must preserve the load-begin and load-end transitions that the injected flag currently records, rather than sampling an unrelated approximation;
- ownership must remain attachment-scoped, with deterministic cancellation and cleanup on normal detach, rejected attachment, trap, hot reload, and host shutdown;
- setup and teardown must not block ordinary autosplitter ticks for an unbounded duration;
- partial setup must roll back safely, and cleanup must be idempotent even when the game closes during instrumentation;
- the runtime must define which executable versions and architectures are supported instead of accepting unchecked script-provided machine code; and
- conformance coverage must prove transition ordering, failed setup, process closure at every stage, trap cleanup, reattachment, and byte-for-byte process restoration if code patching remains part of the host implementation.
No temporary SplitScript workaround preserves the synthetic loading signal. The scripts can port their readable fields and other lifecycle decisions, but load removal remains blocked. Exposing arbitrary allocation, memory writes, process suspension, or jump installation directly to untrusted scripts would expand the security and lifecycle surface substantially; a native game-specific loading signal or tightly constrained host-owned instrumentation contract must be evaluated before any ABI spelling is chosen.
When an ASL port exposes a possible runtime gap, add it here before designing a compiler-specific escape hatch. Record:
- the game and exact source behavior that provides the evidence;
- why the current host ABI cannot represent it faithfully;
- the required semantic guarantee, independent of proposed function names;
- ownership, lifetime, cancellation, ordering, and responsiveness constraints;
- the temporary SplitScript workaround, if one exists;
- whether the issue blocks the port, reduces fidelity, or only affects ergonomics;
- the conformance test that would prove a future runtime implementation.
If the gap can be solved entirely by ordinary SplitScript source or its
standard library, it belongs in TODO.md instead. If it changes the generated
module's implemented contract, update ABI.md only when that contract actually
exists.