Reference
Hosting the language
What a host implements to run DriftScript: a capability described as data, a function to back it, and a target that declares what it provides. Generic, and the same three steps whatever the host is.
A host is anything that supplies the drift/* side of the language: a renderer, a simulation, a server, a tool. It is three steps, and none of them requires the language to know anything about you. The language package imports nothing from any host, so the coupling is a table you write and a lookup it performs.
One: describe what you provide
A capability is data before it is a function. It carries a module, a name, a signature, the effects it has, and whether a deterministic function may call it. Nothing here is an implementation, which is what lets a language server read what a host provides from a process that cannot load the host at all.
typescript
import { defineCapability } from 'driftscript';
const moveTo = defineCapability({
module: 'game/actor',
name: 'moveTo',
signature: 'fn(actor: Actor, x: f32, y: f32) -> bool',
params: [
{ name: 'actor', type: 'Actor' },
{ name: 'x', type: 'f32' },
{ name: 'y', type: 'f32' },
],
returns: 'bool',
effects: ['world'],
deterministic: true,
doc: 'Walk an actor toward a point. Answers whether a path was found.',
});
Two: back it with a function
A binding is a lookup. The implementation map pairs each capability name with the function that does the work, and that function belongs to whichever part of your host owns the subsystem: if a binding needs you to write new behaviour, the behaviour belongs there rather than here.
typescript
const implementations = {
'game/actor': {
moveTo: (actor, x, y) => world.navigate(actor, x, y),
},
};
Three: declare a target, and hand both to your bundler
A target is the list of modules a program may import, and the registry is what effect inference reads. Your bundler plugin takes them together: without the target nothing is refused, and without the registry no effect is inferred, so a deterministic annotation in that build is a claim nothing checked. Both are the right default for a first look at the language and the wrong state for a build that ships.
typescript
import { createRegistry, defineTarget } from 'driftscript';
import { driftScript } from 'driftscript/vite';
const registry = createRegistry([moveTo]);
const manifest = defineTarget({
name: 'my-host',
provides: ['game/actor'],
});
export default {
plugins: [driftScript({ registry, manifest })],
};
Four: bind it, and call what it exports
A compiled module arrives with a hole where each capability was, and binding fills it from the map in step two. A module importing nothing is bound as a no-op, so this is one call whether or not a given file reached for your host at all.
typescript
import { bindHost } from 'driftscript';
const behaviour = await import('./behaviour.drs.js');
bindHost(behaviour, implementations);
behaviour.tick(dt);
What you inherit for nothing
std/* is the language, not the host: duration arithmetic, the scalar functions and the collections work in a target that provides no capabilities at all, and a target may not decline any of it. So a program built only from the standard library runs on your host on the day you have written none of the above.
What a second host changes about the language
Nothing. The prefix is yours, the modules under it are yours, and the standard library arrives unchanged. Two hosts that both provide a module named the same thing are two different targets, and a script written against one is refused by the other in words that name the module and the target rather than failing somewhere in the middle of a frame.