(rontolisp) docs

The Clock and Randomness

Two values a program cannot work out for itself: what time it is, and a number nobody can predict. Both come from outside, which is why they are the pair whose behaviour depends on the backend — and the pair a module with no host has to have an answer for.

Where the values come from

build(random n)rontolisp:random-bytesthe clock
interpreter, JVMthe JVM's generatorworksthe machine's clock
WASM (default, Preview 1)the host's WASI random_getworksWASI clock_time_get
WASM --componentthe host's wasi:randomworkswasi:clocks
WASM --no-wasia built-in generator, same sequence every instancesignalswhat the host wrote through __ronto_set_time; signals until it does
WASM --no-wasi --host-randomthe host's env.random_getworksas above

Only the --no-wasi rows need a decision from you; everywhere else both values are the host's own, and the rest of this page is about that last case.

Randomness

Common Lisp's random is a pseudo-random draw from *random-state*, not an entropy API: nothing in its contract promises unpredictability, and an image may start from a fixed state. rontolisp:random-bytes is the separate API that does promise it, and it is available only where a real entropy source is.

The result type follows the limit — an integer limit yields an integer, a float limit a float — so (random 1) is always 0.

The clock

get-universal-time reports seconds since 1900-01-01 GMT; get-internal-real-time and get-internal-run-time report milliseconds, and only their differences are meaningful. All three return an integer on every backend.

encode-universal-time and decode-universal-time convert between that integer and calendar fields with pure arithmetic, so they behave identically everywhere — with one deliberate deviation: a missing time zone means GMT, not the machine's local zone, because no backend-portable source of the local zone exists (WASI exposes no timezone at all).

Waiting is the clock's other half. sleep parks the thread on the interpreter and the JVM, waits on the real host timer under --component (costing no CPU), and busy-waits on the clock on WASM Preview 1, whose imports include a clock but no timer. On --no-wasi it signals — see below.

A module with no host — --no-wasi

A --no-wasi module imports nothing, so neither value has anywhere to come from. What it does about that follows the rule the whole flag follows: a stub answers when the answer is true of the module, and refuses when answering would mean inventing a value you could not tell from a real one — and a value the host hands in is not an invention, which is what the two hooks below are for.

Randomness lands on the answering side by itself. The module carries its own generator, which is inside random's contract — make-random-state here answers nil, so no state object is observable and "the sequence repeats" is a property of the contract rather than a claim about the host. The consequence is worth stating plainly: unseeded, every instance of one module produces the same sequence. Because that generator is not entropy, rontolisp:random-bytes signals rather than draw from it.

The clock lands on the refusing side by itself: a reading of 0 is not "no time", it is 1970, and nothing the module could invent would be the time. So until a host sets it, all three built-ins signal a catchable error naming the operator.

A library that reads the clock while it loads has no caller to catch that, so the build names it for you instead of leaving it to the first run — see what the build tells you.

Seeding the generator — __ronto_seed_random

The module cannot import the host's random by default: a core WebAssembly import is not optional, so asking for one would break the very thing the flag is for (instantiating with {}). It exports a hook instead. Call it once, before _initialize, and even a library's load-time (random ...) draws from your seed:

const instance = new WebAssembly.Instance(module, {});
instance.exports.__ronto_seed_random(
  new BigUint64Array(crypto.getRandomValues(new Uint8Array(8)).buffer)[0],
);
instance.exports._initialize();

Skip the call and you get the deterministic sequence, unchanged. The hook is on the core-module shape only — a reactor component (--component --no-wasi) runs its top level at instantiation, so there is no window before the first draw.

Seeding makes the sequence unpredictable per instance but does not re-enable rontolisp:random-bytes: the generator is invertible from a single output, so a seeded stream is not cryptographically strong, and the API that promises entropy keeps saying no rather than handing you something that only looks like a CSPRNG.

Setting the clock — __ronto_set_time

The clock's hook is the same shape, and takes nanoseconds since the Unix epoch:

const instance = new WebAssembly.Instance(module, {});
instance.exports.__ronto_set_time(BigInt(Date.now()) * 1000000n);
instance.exports._initialize();

Calling it before _initialize is what makes a library that timestamps while it loads loadable at all — lack-middleware-session reads the clock from a top-level form, and without this the module dies during initialization rather than at the first request.

The clock does not tick on its own: it holds the value you wrote until you write another. That is less of a restriction than it sounds — a Cloudflare Worker's own clock is frozen for the duration of a request as a timing-attack mitigation — and the natural rhythm is to set it once per request, which is what the Worker examples do. The one thing it cannot support is waiting: (sleep n) signals here, because nothing can make an interval elapse while your call is running.

Like the seed hook, it is on the core-module shape only. A reactor component runs its top level at instantiation, so there is no moment at which a host could set the time first; the clocks signal there, and say so.

Drawing from the host — --host-random

--host-random replaces the built-in generator with a host call, so every draw is the host's entropy — including draws inside a quickloaded library, which never learns where the bytes came from:

rontolisp app.lisp --no-wasi --host-random -o app.wasm

The module then imports exactly one function, env.random_get(buf, len) -> errno. That is preview1's signature, so a host that already has a WASI implementation can forward it unchanged; from JavaScript it is one property:

const instance = await WebAssembly.instantiate(module, {
  env: {
    random_get(ptr, len) {
      crypto.getRandomValues(new Uint8Array(instance.exports.memory.buffer, ptr, len));
      return 0;                                  // errno 0 = success
    },
  },
});
instance.exports._initialize();

Because the entropy really is the host's, rontolisp:random-bytes works here. No __ronto_seed_random is exported — there is no module-local state left to seed. __ronto_set_time is unaffected: the two services are independent, and only one of them has a module-local generator to make redundant.

The zero-import default is unchanged; this is the opt-in, and the module now has an import the host must provide. The tree shaker still drops it if the program never draws. The flag is core-module only: a reactor component imports nothing by contract, and a plain --component build already has wasi:random.

There is no --host-clock counterpart, because the export answers the same question without costing the zero-import property. A live clock that advances during a call would need one; nothing has needed that yet.

Redefining random

A program's own (defun random ...) is called by the interpreter and ignored by the compile backends, which emit the standard operator at the call site and warn that they did — see Redefining a COMMON-LISP function.