Module System

The use<> expression resolves a named resource through a host-registered scheme resolver. The result is the last expression of the loaded script. Modules are plain rill values — dicts, closures, or any other type.

Syntax

use<module:greetings>
use<module:greetings> => $g
use<module:greetings>($name)

The formal grammar:

use-expr = "use" "<" use-id ">" [ ":" type-ref ] ;
use-id   = identifier ":" identifier { "." identifier }   # static form
         | "$" identifier                                  # variable form
         | "(" pipe-chain ")" ;                            # computed form

The scheme (module) and resource (greetings) are separated by :. The host registers a resolver for each scheme.

Module Structure

A module is a rill script whose last expression is its exported value. There is no export: frontmatter. The last expression determines what the caller receives.

|name|"Hello, {$name}!" => $hello
|name|"Goodbye, {$name}!" => $goodbye
dict[hello: $hello, goodbye: $goodbye]

The final dict[...] is the module value. The host executes the script and binds the result.

Consumer Pattern

Load a module and call its members:

use<module:greetings> => $g
$g.hello("World")
# Result: "Hello, World!"

$g is a dict with callable members. Member access follows the same rules as any rill dict.

Re-export via Dict Composition

Combine multiple modules into a single namespace:

use<module:math> => $math
use<module:string> => $str
dict[math: $math, str: $str] => $utils
$utils.math.double(5)

There is no special re-export syntax. Dict composition achieves nested namespaces.


Host Declaration Modules

Some modules are backed by host code rather than rill source. The resolver returns { kind: "value", value: ... } directly:

# Resolver returns a host-constructed dict:
use<http:client> => $http
$http.get("https://api.example.com")

The caller cannot distinguish source-backed from value-backed modules.


moduleResolver Registration

moduleResolver is a built-in scheme resolver exported from @rcrsr/rill. It reads rill source files from the filesystem.

Config shape

type ModuleResolverConfig = {
  [moduleId: string]: string;   // Maps module ID to file path
};

Registration

import {
  createRuntimeContext,
  execute,
  moduleResolver,
  parse,
} from '@rcrsr/rill';

const ctx = createRuntimeContext({
  resolvers: {
    module: moduleResolver,
  },
  configurations: {
    resolvers: {
      module: {
        greetings: './greet.rill',
        utils: './utils.rill',
      },
    },
  },
  parseSource: parse,
});

const ast = parse(source);
const result = await execute(ast, ctx);

parseSource is required when any resolver returns kind: "source" results. moduleResolver always returns source, so it must be provided.

Return value

moduleResolver returns { kind: "source", text } after reading the target file. The runtime parses and executes the text, then binds the last expression as the module value.

Caching

The runtime does not cache module results between execute calls. Implement caching in the host by wrapping the resolver:

const cache = new Map<string, import('@rcrsr/rill').RillValue>();

const cachingResolver: import('@rcrsr/rill').SchemeResolver = async (resource, config) => {
  if (cache.has(resource)) {
    return { kind: 'value', value: cache.get(resource)! };
  }
  const result = await moduleResolver(resource, config);
  // Cache after execution — store the value, not the source
  return result;
};

For same-run deduplication, the runtime tracks in-flight resolution to detect cycles (see Circular Resolution).


Custom Resolvers

Any function matching SchemeResolver works as a resolver. Register multiple schemes for different resource types:

import { createRuntimeContext, parse } from '@rcrsr/rill';
import type { SchemeResolver } from '@rcrsr/rill';

const dbResolver: SchemeResolver = async (resource) => {
  const row = await db.query(`SELECT source FROM modules WHERE id = ?`, [resource]);
  return { kind: 'source', text: row.source };
};

const ctx = createRuntimeContext({
  resolvers: {
    module: moduleResolver,
    db: dbResolver,
  },
  configurations: {
    resolvers: {
      module: { greetings: './greet.rill' },
    },
  },
  parseSource: parse,
});

Scripts use each scheme independently:

use<module:greetings> => $g
use<db:templates> => $t

Circular Resolution

The runtime tracks in-flight resolution keys. If module A loads module B which loads module A, the runtime throws RILL-R055 before infinite recursion occurs.

# module:a contains: use<module:b>
# module:b contains: use<module:a>
# Error: RILL-R055 Circular resolution detected: module:a is already being resolved

See Error Reference for full error details on RILL-R055 and related codes.


Error Reference

CodeTrigger
RILL-R050Module ID not in moduleResolver config
RILL-R051File path in config cannot be read
RILL-R054Scheme name not registered in resolvers
RILL-R055Circular resolution: key already in flight
RILL-P020Missing : after scheme in use<>
RILL-P021Missing resource identifier after :
RILL-P022Missing > to close use<>

See Also