Guide
Setting it up
The bundler plugin, the one required compiler flag, and driving the runtime from a loop.
What it takes to compile .drs in a project, and the one compiler flag that is not optional. One package from the registry carries all of it.
The bundler plugin
The language ships a Vite plugin. It recognises .drs, compiles to a JavaScript module with a source map, carries the module metadata, and invalidates dependents only when an interface changes rather than on every edit.
A shipping build has something to verify against
A plugin given neither a registry nor a manifest infers no effects and refuses no unprovided module, so @deterministic on a file it built is a claim nothing checked. That configuration is right for a first look and a production build is declined without both, unless it passes verification: none to say it meant it.
A host that runs at a different rate says so
update at 1Hz compiles to a stride, a count of fixed steps to skip, so how many steps a second holds is what turns a rate into one. It defaults to sixty, which is what this language's first host runs. A host whose loop differs passes fixedStepsPerSecond, and the value a module was built with travels in its metadata so a cached module and a running loop can be compared.
One required compiler flag, and why
A TypeScript project consuming the language sets allowImportingTsExtensions. That is not tidiness: a bundler plugin is loaded by the toolchain, which is Node rather than the bundler, so Node's module rules apply to the plugin and to everything it imports. Node's resolver requires an extension, so every relative import inside the language package names its .ts file. Your own code never writes one; the flag only permits them.
Declaring the module shape
A project declares what a .drs module looks like once, anywhere its compiler includes. The generated function exports are deliberately absent from that declaration, because TypeScript cannot know them without compiling the file, so a generated function is reached through a record of unknowns. That is uncomfortable on purpose.
Driving the runtime
A host sets a clock source, loads the module, and ticks the scheduler from its own loop. The clock source comes before the load when a file declares a task, because spawning runs the task up to its first await and an await asks the clock what step it is on.
// A task is asynchronous behaviour with an owner.
//
// `spawn` starts one inside a `scope`, and leaving the scope cancels whatever has not finished.
// That is the rule that stops detached work outliving the thing that created it, and it is
// structural here rather than something every author has to remember.
//
// `begin` is a task rather than a function, and the compiler insists: a scope opened in a function
// would be left on the same tick it was opened, cancelling everything spawned into it.
data Signal {
fired: bool = false
count: u32 = 0
}
task pulse(signal: mut Signal) {
await fixedTime(250ms)
signal.count = signal.count +| 1
signal.fired = true
}
task begin(signal: mut Signal) {
scope effect {
spawn pulse(signal)
await fixedTime(1s)
}
}
The editor
A VSCode extension on the marketplace runs the same compiler the build runs, so a squiggle in the editor and a failure in the build are the same diagnostic with the same code. It gives diagnostics, completion, hover and semantic tokens, and the client itself holds no language logic: the language server is a second package on the registry for anything that speaks the protocol.
What the editor needs from a host
Point the extension at a capability description and completion after a drift prefix knows what your build provides. Without one it still checks everything that is not a host call, which is most of a file. That description is the same JSON the bundler plugin reads, which is the property that keeps an editor and a build from disagreeing.