DriftScript
Documentation

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.',
});
The description a hover, a completion list and the linker all read.

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),
  },
};
The map a compiled module is bound against. Your code, unchanged by being reachable.

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 })],
};
Where the two objects from step one actually go. A .drs file is compiled and linked here.

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);
The compiled module, its capabilities filled in, running.

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.