Context

Context values are the program's connections to the outside world - output, input, time, randomness, files, memory - passed down the call stack implicitly instead of as arguments. Each is read with a Context Reference, &name, and can be replaced for a file, a folder, or the whole project with @inject, @injectLocally and @injectGlobally - see Context Reference and Inject for the syntax.

Replacing a value

Most values are typed by an interface from interface - each card below lists the members it requires. Implement that interface on your own type and inject an instance, and everything that reads the value below that point uses yours instead of the builtin one. The common replacements are ready-made - see Components - so a fixed clock for tests is one line:

Anything else is a type of your own that implements the interface:

The interfaces report failures with these types (see Error):

Rules

The exact rules for declaring, requiring and injecting context values. The Context page introduces them.

Requiring

A function needs a context value when it reads one, or calls a function that needs one, where nothing injected it. The compiler follows the calls and warns about each function that doesn't @require what it needs - so a function's attributes list everything it reaches outside itself, however deep the call that reaches it. An inject before a call covers it, and so does a default in the called function's own @require, which then provides the value itself. One @require lists as many as the function needs - @require(&stdout, &clock) - each written like a parameter, with its type when nothing declares it and its default after :=. The policies - &hashSeed and the others above - are read by the builtin operations everywhere, so they're never declared, and @main, @test and @bench functions, which the runtime calls with the builtins provided, declare nothing. An effect attribute that names the value says it as well: @reads(&clock), @writes(&clock) or @effects(&clock) requires &clock with the type it's declared with, so it needs no @require(&clock) beside it. See @require.

A function given as a value is followed too, through what holds it: @require on a parameter, a field or an interface's method says what calling the function there may read. Calling it needs those values, like calling a function that reads them - so timed needs &clock - and a lambda given to it may read them without the function it's written in needing them for the lambda. report still needs &clock above, because it calls timed; the lambda's &stdout isn't on step, so it's report's to declare.

Declaring

Any name can be injected, not only the builtin ones - a database, a request's user, a feature flag. Each has a type, the way every variable does: context name: Type declares it, and every &name it reaches is of that type, and every inject name has to give one. It reaches as far as a definition is seen - with local in front, its own file; with nothing, its folder; with global, the whole project. Reading or injecting a name nothing declares is an error, the same as using a variable nothing defines. Two folders can each declare the same name, but only with the same type: an injection goes by the name alone, so one folder's inject reaches the other folder's reads. See Context Definition.

A value after := is the default: what &name reads where nothing up the call stack injected one, made the first time it's read and shared by every read after that - unlike a struct field's default or a @require default, which are worked out again each time. Without a default, reading a value nobody injected is an error, which !? can replace like any other.

@require declares one for a single function instead: its name, its type, and optionally a default for when nothing up the call stack injected it, made again on each call. A value declared with context - or a builtin, like &stdout - is required by its name alone, @require(&accounts), which takes the declared type; one nothing declares has its type written. See @require.

Injecting for a file, folder or project

A const marked @inject is injected into every call in its file, @injectLocally into every call in its folder, and @injectGlobally into every call in the project - one of each per file, folder and project. Each declares its name too, with the const's type, over the same reach - it needs no context line of its own. This is where a program's shared dependencies live. An inject statement further down the call stack is nearer, so it still wins: a test injects its own database over the folder's. See HTTP Server.

A function can be injected the same way: its value is the function itself, so &name(...) calls whichever one is in effect - the file's, or one a caller injected over it. A definition named in backticks provides a context value by that name, and it's read back in backticks too: &`format price`, inject `format price`, reject `format price`. A context declaration and @require take a quoted name the same way. A context name is never a path, so it can't hold a /.

Blocks

Every block keeps its injections to itself: an inject inside an if ends with the if's block. To inject only sometimes, inject in the block that needs it and choose the value - reading &name keeps what was already there. A loop body is a block of its own, so the injection above lasts one iteration.

Values

&stdout

#stdout

Where the program's normal output goes - by default the process's standard output. Printing is meant to go through it, so injecting an in-memory OutputStream captures a function's output, e.g. to check it in a test.

Implements interface.OutputStream(3)
  • write(bytes: Bytes): IOError ! Undefinedwrites the bytes as they are
  • writeText(text: String): IOError ! Undefinedwrites the text, UTF-8 encoded
  • flush(): IOError ! Undefinedpushes out anything buffered

Ready-made: MemoryOutputStream, NullOutputStream, TeeOutputStream

&stderr

#stderr

Where diagnostics and error messages go - by default the process's standard error, kept apart from &stdout so they don't mix into the program's actual output.

Implements interface.OutputStream(3)
  • write(bytes: Bytes): IOError ! Undefinedwrites the bytes as they are
  • writeText(text: String): IOError ! Undefinedwrites the text, UTF-8 encoded
  • flush(): IOError ! Undefinedpushes out anything buffered

Ready-made: MemoryOutputStream, NullOutputStream, TeeOutputStream

&stdin

#stdin

Where the program reads its input - by default the process's standard input. Inject one over a fixed string to feed a function scripted input.

Implements interface.InputStream(3)
  • read(count: U64): IOError ! Bytesup to count bytes, empty at end of input
  • readLine(): IOError ! String?undefined at end of input
  • readAll(): IOError ! Byteseverything left until end of input

Ready-made: MemoryInputStream

&clock

#clock

The source of the current time: read with &clock.now(), and what sleep waits on. Inject a fixed or manually advanced clock to make time-dependent code deterministic.

Implements interface.Clock(2)
  • now(): Instantthe current time
  • sleep(duration: Duration): Undefinedwaits for duration - with nothing else to run, the program idles

Ready-made: FixedClock, OffsetClock

&random

#random

The source of random numbers, used by every builtin that picks at random (e.g. shuffle). Inject one with a fixed seed to make runs - and tests - repeatable.

Implements interface.Random(3)
  • nextU64(): U64uniform over all of U64
  • nextF64(): F64uniform in [0, 1)
  • between(range: Range<Integer>): Integeruniform within the range

Ready-made: SeededRandom

&env

#env

The process's environment variables. Reads and writes go through this value rather than the real environment, so a test can inject its own set without touching the process's.

Implements interface.Environment(3)
  • get(name: String): String?undefined when not set
  • set(name: String, value: String): Undefinedsets name to value
  • all(): Map<String, String>every variable, by name

Provided for free, built on the members above: settings

Ready-made: MemoryEnvironment

&args

#args

The arguments the program was started with. Plain data rather than an interface - inject any Array<String> to run a @main function with different arguments.

&fs

#fs

Access to the file system: reading, writing and listing files and directories. Code that works with files goes through it, so an in-memory FileSystem lets that code run without touching the disk. It's there while compiling too - the compiler's own - so #comptime(...) code can read files into the program (see Compile-time Code).

Implements interface.FileSystem(9)
  • read(path: String | Path): IOError ! Bytesthe whole file as bytes
  • readText(path: String | Path): IOError ! Stringthe whole file as UTF-8 text
  • write(path: String | Path, bytes: Bytes): IOError ! Undefinedcreates or replaces the file
  • writeText(path: String | Path, text: String): IOError ! Undefinedcreates or replaces the file, UTF-8 encoded
  • list(path: String | Path): IOError ! Array<String>entry names directly inside a directory
  • exists(path: String | Path): Booleana file or directory is there
  • isDirectory(path: String | Path): Booleanit's there and a directory
  • createDirectory(path: String | Path): IOError ! Undefinedcreates it, and any missing parents
  • remove(path: String | Path): IOError ! Undefineddeletes a file or a directory, with everything in it

Provided for free, built on the members above: glob

Ready-made: MemoryFileSystem

&database

#database

The connection #SQL {{{ ... }}} queries run through - a query for statements that select rows, an execute for ones that change them. There's none by default, so running a query is an IOError until one's injected - usually SqliteDatabase, as a const marked @injectLocally, or SqliteDatabase.memory() in a test.

Implements interface.Database(2)
  • query(sql: String, parameters: Array<Unknown>): IOError ! Array<Map<String, Unknown>>the rows selected
  • execute(sql: String, parameters: Array<Unknown>): IOError ! U64how many rows changed

Ready-made: SqliteDatabase

&request

#request

The HTTP request the running route answers - its method, path, params (the {name} segments of the route's path), query string, form fields, headers and body. The server injects it for each request before calling the route; there's none outside one, and a test of a route injects its own, inject request := Request{params := Map.of(("id", "7"))}. See @serve.

&http

#http

Where HttpRequests are sent - &http.send(request), or request.send() - each giving a Promise<IOError ! Response> that settles once the server has answered. By default the JavaScript host's own client: on Bun, a request goes out on a thread of its own while the program waits; in a web page the browser sends it, and the program goes on from its await once the answer comes. Without a JavaScript host (plain WASI, a native executable) every request is an IOError saying so. A test injects MemoryHttpClient, which answers from a Map and keeps what it was sent. See Sending HTTP Requests.

Implements interface.HttpClient(1)
  • send(request: HttpRequest): Promise<IOError ! Response>the server's answer, once it's come - whatever its status, a 404 too; an IOError when none came (no network, a refused connection, a name that doesn't resolve)

Ready-made: MemoryHttpClient

&timeZone

#timezone

The zone local dates and times are in - what Date.today(), DateTime.now() and Instant.toDateTime() use when they're given none. Plain data: by default the host's own zone (UTC on a WASI runtime, which has none); inject TimeZone.fixed(...) or TimeZone.named("Europe/Warsaw") to pin it, in a test as anywhere.

&cancellation

#cancellation

Whether the running work has been asked to stop. Plain data: by default one nothing cancels. Whoever starts some work injects a Cancellation of their own and cancels it - or gives it a deadline - when the work isn't wanted anymore; the work asks between its steps with &cancellation.check()!!.

&snapshots

#snapshots

Where snapshot tests keep the texts they compare against - &snapshots.check("name", text)!! passes when text is the one kept, keeps it the first time, and fails showing the first line that differs otherwise; --update-snapshots keeps the new text instead. By default files in the program's folder's snapshots/; inject MemorySnapshots to test code that checks snapshots. See Testing.

Implements interface.SnapshotStore(3)
  • read(name: String): IOError ! String?the text kept under name - undefined when there's none yet
  • write(name: String, text: String): IOError ! Undefinedkeeps text under name, replacing what was there
  • updating(): Booleanwhether a text that differs replaces the kept one instead of failing - what --update-snapshots turns on

Provided for free, built on the members above: check

Ready-made: FileSnapshots, MemorySnapshots

&logger

#logger

Where structured log entries go: a LogLevel, a message, and a map of named fields. Write one with &logger.info("...") - or debug, warn, error - and add fields as a second argument when there are any. Replace it to send logs somewhere else, filter them by level, or collect them in a test; a replacement only writes log, and gets the other four for free.

Implements interface.Logger(1)
  • log(level: LogLevel, message: String, fields: Map<String, Unknown>): Undefinedone entry - the one method an implementation writes

Provided for free, built on the members above: debug, info, warn, error

Ready-made: MemoryLogger, StreamLogger

&panicHandler

#panichandler

What a panic does - panic, todo, unreachable, Optional.expect, a call past &maxCallDepth - all hand their error to it. The builtin one is AbortingPanicHandler: print the error and its trace to &stderr and end the program once pending defer blocks have run. Its handle never returns, so a replacement ends the program too - or, in a test runner, the test.

Implements interface.PanicHandler(1)
  • handle(error: Error): Neverpanic, todo, unreachable, Optional.expect and a too-deep call end here

Ready-made: AbortingPanicHandler

&heapAllocator

#heapallocator

Where heap memory comes from - what Pointer.allocate and RcPointer.new allocate with. Inject an arena or a tracking allocator to control or measure allocations; it returns OutOfMemoryError when a request can't be met.

Implements interface.Allocator(3)
  • allocate(size: U64): OutOfMemoryError ! Bytesa fresh block of size bytes
  • resize(block: Bytes, size: U64): OutOfMemoryError ! Bytesthe block grown or shrunk to size - its contents kept up to the smaller size, possibly moved
  • free(block: Bytes): Undefinedgives a block back

Ready-made: TrackingAllocator

&stringNormalization

#stringnormalization

The Unicode normal form text ==, hashing and compare look at. Undefined by default - text is compared as it is, codepoint by codepoint. Inject NFC and "é" written as one codepoint equals "e\u{301}", an e and a combining accent; NFKC also equates compatibility forms, like "fi" and "fi". A plain value.

&hashSeed

#hashseed

What every builtin hash starts from - 0xcbf29ce484222325 by default, so hashes, and anything ordered by them, are the same on every run. A server taking keys from untrusted input can inject a random one at startup, so an attacker can't pick keys that all collide. Inject it for a whole program (@injectGlobally), not around part of one: a Map or Set built under one seed can't find its keys under another. A plain value.

&maxCallDepth

#maxcalldepth

How deeply calls may nest - 100_000 by default. A call one deeper panics (see &panicHandler) instead of overflowing the platform's stack, so runaway recursion fails the same way on every target. Raise it for deeply recursive code, lower it in a test to check a function's depth. A plain value.

Components

Ready-made implementations of the interfaces above, reached through the #builtin.implementation namespace - never as a bare name, so they don't crowd your own code. Inject one to pin time or randomness in a test, capture or silence output, fake the file system, or measure memory. Capturing what a function prints, for example:

FixedClock

#fixedclock
Clock

only moves when told to - sleep returns at once, moving the time forward instead of waiting

  • Function new(at: Instant := Instant.epoch()): FixedClocktime stands at at
  • Method set(at: Instant): Undefinedtime jumps to at
  • Method advance(by: Duration): Undefinedtime moves forward by

Replaces &clock - source

OffsetClock

#offsetclock
Clock

another clock shifted by a fixed amount - "run as if it were tomorrow"; sleeps really wait

  • Function new(offset: Duration, inner: Clock := &clock): OffsetClockinner's time plus offset

Replaces &clock - source

SeededRandom

#seededrandom
Random

the same sequence for the same seed, on every platform (SplitMix64) - not for anything secret

  • Function new(seed: U64): SeededRandomthe sequence seed starts

Replaces &random - source

MemoryOutputStream

#memoryoutputstream
OutputStream

keeps everything written, in memory - to check what a function printed

  • Function new(): MemoryOutputStreamempty
  • Method bytes(): Byteseverything written so far
  • Method text(): ParseError ! Stringthe same, as UTF-8
  • Method clear(): Undefinedforgets everything written

Replaces &stdout, &stderr - source

NullOutputStream

#nulloutputstream
OutputStream

throws everything away - to silence a function

  • Function new(): NullOutputStreama stream that keeps nothing

Replaces &stdout, &stderr - source

TeeOutputStream

#teeoutputstream
OutputStream

writes everything to two streams, first then second - to watch output while also keeping it

  • Function new(first: OutputStream, second: OutputStream): TeeOutputStreamwrites go to first, then second

Replaces &stdout, &stderr - source

MemoryInputStream

#memoryinputstream
InputStream

reads from fixed bytes - to feed a function scripted input

  • Function fromBytes(bytes: Bytes): MemoryInputStreamreads the bytes, then end of input
  • Function fromText(text: String): MemoryInputStreamencoded as UTF-8
  • Method remaining(): U64bytes not read yet

Replaces &stdin - source

MemoryEnvironment

#memoryenvironment
Environment

variables held in a Map, apart from the process's own

  • Function new(variables: Map<String, String> := Map.of()): MemoryEnvironmentcopies variables

Replaces &env - source

MemoryFileSystem

#memoryfilesystem
FileSystem

files and directories held in memory - "/"-separated absolute paths, "/" the root. A directory exists once created or once a file is written under it; writing creates the directories on the way

  • Function new(): MemoryFileSystemempty, only the root
  • Function of(files: Map<String, String>): MemoryFileSystempath to text, encoded as UTF-8

Replaces &fs - source

SqliteDatabase

#sqlitedatabase
Database

an SQLite database - a file, created when it doesn't exist, or one held in memory. Needs a JavaScript host (--js, which --run and --test use): the host's own SQLite runs the statements

  • Function open(path: String): IOError ! SqliteDatabasethe database file at path, created when it doesn't exist
  • Function memory(): IOError ! SqliteDatabaseempty, and gone when the program ends

Replaces &database - source

MemoryHttpClient

#memoryhttpclient
HttpClient

answers from a Map instead of the network, at once - and keeps every request it's sent, to check what a function asked for. A request it has no answer for gets a 404

  • Function new(answers: Map<String, Response> := Map.of()): MemoryHttpClientanswers by "METHOD url" - "GET https://api.example.com/todos"
  • Method requests(): Array<HttpRequest>every request sent, in order

Replaces &http - source

FileSnapshots

#filesnapshots
SnapshotStore

a file for each snapshot, ".snap" in folder (characters a file name can't hold become "_"), on the program's own file system even when a test replaces &fs. &snapshots' builtin value is one whose folder is the environment's REFINGERE_SNAPSHOTS - the compiler sets it to the program's folder's snapshots/ when it runs the program - or "snapshots" where the program runs, without it. On a host whose files can't be written - a web page - it keeps the texts for the run instead

  • Function new(folder: String := "snapshots"): FileSnapshotsfiles in folder, relative to where the program runs - updating when the environment's REFINGERE_UPDATE_SNAPSHOTS is 1, which --update-snapshots sets

Replaces &snapshots - source

MemorySnapshots

#memorysnapshots
SnapshotStore

snapshots held in a Map - to test code that checks snapshots

  • Function new(texts: Map<String, String> := Map.of(), update: Boolean := false): MemorySnapshotsstarting with texts, by name - replacing differing texts when update is true
  • Method texts(): Map<String, String>every text it keeps, by name

Replaces &snapshots - source

MemoryLogger

#memorylogger
Logger

keeps every entry, in order - to check what a function logged

  • Function new(): MemoryLoggerno entries yet
  • Method entries(): Array<(level: LogLevel, message: String, fields: Map<String, Unknown>)>every entry, in order
  • Method messages(level: LogLevel?): Array<String>every message, or only those at level
  • Method clear(): Undefinedforgets every entry

Replaces &logger - source

StreamLogger

#streamlogger
Logger

one line per entry to a stream - "WARN disk almost full free=12 path=/tmp" - dropping entries below minimum (Debug < Info < Warn < Error)

  • Function new(stream: OutputStream := &stderr, minimum: LogLevel := LogLevel.Info): StreamLoggerwrites entries at minimum or above to stream

Replaces &logger - source

AbortingPanicHandler

#abortingpanichandler
PanicHandler

the builtin &panicHandler - prints the error and its trace() to &stderr and ends the program, once pending defer blocks have run

  • Function new(): AbortingPanicHandlerprint and end the program - the builtin &panicHandler

Replaces &panicHandler - source

TrackingAllocator

#trackingallocator
Allocator

passes every request on to another allocator, counting as it goes - and, given a limit, refuses with OutOfMemoryError any request that would take more than limit bytes live at once, to test that path

  • Function new(limit: U64? := undefined, inner: Allocator := &heapAllocator): TrackingAllocatorlimit undefined means no limit - every request goes on to inner
  • Method live(): U64bytes handed out and not yet freed
  • Method peak(): U64the most live() has ever been
  • Method allocations(): U64successful allocate calls

Replaces &heapAllocator - source