Host Resolver Registration

Host Resolver Registration

Host Resolver Registration

Register resolvers in createRuntimeContext() to handle use<scheme:resource> import statements. Scripts use the scheme portion to select a resolver, and the resource portion is passed to that resolver along with its configuration.

import { moduleResolver, extResolver, createRuntimeContext } from '@rcrsr/rill';
import { myQdrantExtension } from './qdrant';

const ctx = createRuntimeContext({
  resolvers: {
    host: moduleResolver,
    ext: extResolver,
  },
  configurations: {
    resolvers: {
      host: {
        utils: './utils.rill',
      },
      ext: {
        qdrant: myQdrantExtension,
      },
    },
  },
});

Scripts import resources using the use<scheme:resource> syntax:

use<host:utils>
use<ext:qdrant>
use<ext:qdrant.search>

Two built-in resolvers are available:

ResolverImportPurpose
moduleResolverimport { moduleResolver } from '@rcrsr/rill'Maps module IDs to rill source files
extResolverimport { extResolver } from '@rcrsr/rill'Maps extension IDs to pre-built rill value dicts

RuntimeOptions Fields

These two fields in createRuntimeContext() control resolver registration:

FieldTypeDescription
resolversRecord<string, SchemeResolver> | undefinedScheme-to-resolver map for use<scheme:...> imports
configurations{ resolvers?: Record<string, unknown> } | undefinedPer-scheme config data passed to each resolver

Custom Resolvers

Implement SchemeResolver to handle any custom scheme:

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

const myResolver: SchemeResolver = async (resource, config) => {
  const source = await loadSource(resource);
  return { kind: 'source', text: source };
};

const ctx = createRuntimeContext({
  resolvers: { custom: myResolver },
  configurations: {
    resolvers: {
      custom: { /* your config */ },
    },
  },
});

See SchemeResolver and ResolverResult in the API reference for full type details.

moduleResolver

moduleResolver maps module identifiers to rill source files on disk.

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

const ctx = createRuntimeContext({
  resolvers: { host: moduleResolver },
  configurations: {
    resolvers: {
      host: {
        utils: './utils.rill',
        helpers: './lib/helpers.rill',
      },
    },
  },
});

// Scripts can now use: use<host:utils>

Config shape:

FieldTypeRequiredDescription
[moduleId]stringYes (at least one)Maps a module identifier to a file path (relative to the config file’s directory)

Error codes:

CodeTrigger
RILL-R050Module identifier not found in config
RILL-R051File read failure (I/O error or missing file)
RILL-R059Config is missing or malformed

moduleResolver returns { kind: 'source', text: string } after reading the target file. Paths resolve relative to the config file’s directory.

extResolver

extResolver maps extension identifiers to pre-built rill value dicts.

import { extResolver, createRuntimeContext } from '@rcrsr/rill';
import { qdrantExtension } from './my-qdrant-ext';

const ctx = createRuntimeContext({
  resolvers: { ext: extResolver },
  configurations: {
    resolvers: {
      ext: {
        qdrant: qdrantExtension,
      },
    },
  },
});

// Scripts can now use: use<ext:qdrant> or use<ext:qdrant.search>

Config shape:

FieldTypeRequiredDescription
[extensionId]RillValueYes (at least one)Maps an extension identifier to its full rill value dict

Member access: A resource of "qdrant.search" returns only the search member from the qdrant extension dict. A resource of "qdrant" returns the full dict.

Error codes:

CodeTrigger
RILL-R052Extension identifier not found in config
RILL-R053Member path not found within the extension dict

extResolver returns { kind: 'value', value: RillValue }.

ext::fn() Compatibility

The ext::fn() calling pattern is retained for compatibility but is not recommended for new code. In strict checker mode, ext::fn() calls are flagged. Use use<ext:...> imports instead.


Value Types

rill uses several internal container types that host code may encounter in observability callbacks or return values.

RillOrdered

RillOrdered is the container produced by dict spread (*dict). It preserves insertion order and carries named keys.

interface RillOrdered {
  __rill_ordered: true;
  entries: [string, RillValue][];
}
// Created by: ordered[a: 1, b: 2]

toNative() converts RillOrdered to a plain object — the NativeResult.value field holds { key: value, ... } with insertion-order keys.

RillTuple

RillTuple holds positional values produced by tuple expressions. The entries field is a plain array, not a Map:

interface RillTuple {
  __rill_tuple: true;
  entries: RillValue[];    // positional values, 0-indexed
}

toNative() converts RillTuple to a native array — the NativeResult.value field holds the entries as a plain array.

RillTypeValue

^type expressions return a RillTypeValue. The structure field carries the full structural type:

interface RillTypeValue {
  __rill_type: true;
  name: string;                    // coarse name: "list", "dict", "number", etc.
  structure: TypeStructure;        // full structural type
}

TypeStructure uses a kind discriminator field. The name and primitive fields no longer exist:

type TypeStructure =
  | { kind: 'number' }
  | { kind: 'string' }
  | { kind: 'bool' }
  | { kind: 'vector' }
  | { kind: 'type' }
  | { kind: 'any' }
  | { kind: 'dict';    fields?: Record<string, TypeStructure> }
  | { kind: 'list';    element?: TypeStructure }
  | { kind: 'closure'; params?: [string, TypeStructure][]; ret?: TypeStructure }
  | { kind: 'tuple';   elements?: TypeStructure[] }
  | { kind: 'ordered'; fields?: [string, TypeStructure][] }

Breaking change from previous versions: { kind: 'primitive', name: 'string' } is now { kind: 'string' }. { type: 'any' } (pre-rename) is now { kind: 'any' }. { type: 'dict', fields: F } (pre-rename) is now { kind: 'dict', fields?: F }. Switch on structure.kind, not structure.type.

The structural type formats as a human-readable string via formatStructure:

Expression:>string output
[1, 2, 3] -> ^type"list(number)"
[a: 1, b: "x"] -> ^type"dict(a: number, b: string)"
tuple[1, "x"] -> ^type"tuple(number, string)"
ordered[a: 1, b: 2] -> ^type"ordered(a: number, b: number)"

Dict fields are sorted alphabetically in the formatted output.

Callable Introspection: .^input and .^output

All callable kinds expose their parameter and return type shapes via .^input and .^output.

.^input returns a RillOrdered value directly. Each entry is a [paramName, RillTypeValue] pair, preserving parameter declaration order. .^input works on all callable kinds — script closures, application callables, and runtime callables. Untyped host callables return an empty ordered dict, not false.

// Script closure returned from execute():
// |x: number, y: string| x -> :>string

const closure = result.result; // RillCallable (ScriptCallable)
// $fn.^input -> ordered[x: ^number, y: ^string]
// Host side: RillOrdered with entries [["x", RillTypeValue], ["y", RillTypeValue]]
// entries[0][1].structure -> { kind: 'number' }
// entries[1][1].structure -> { kind: 'string' }
// Parameterized closure: |x: list(string), y: number| { $x }
// entries[0][1].structure -> { kind: 'list', element: { kind: 'string' } }
// entries[1][1].structure -> { kind: 'number' }
//
// Use structure.element to inspect the list's element type:
// if (entries[0][1].structure.kind === 'list') {
//   const elementType = entries[0][1].structure.element; // { kind: 'string' }
// }

Behavioral change (v0.x): .^input now returns an ordered dict for all closure kinds. Previously, untyped host callables returned false as a sentinel. They now return an empty ordered dict. Code that checked result === false must be updated to check result.entries.length === 0 instead.

.^output returns a RillTypeValue with the closure’s declared return type. When no return type is declared, the fallback structure is { kind: 'any' }:

// Closure with declared return: |x: number| :string -> ...
// $fn.^output -> ^string
// Host side: RillTypeValue with structure { kind: 'string' }

// Closure with no declared return type:
// $fn.^output -> ^any
// Host side: RillTypeValue with structure { kind: 'any' }

Both accessors use structure.kind (not structure.type) to discriminate the structural type.


Value Conversion

toNative() converts a RillValue to a structured result suitable for host consumption.

import { toNative } from '@rcrsr/rill';

const nativeResult = toNative(executionResult.result);
console.log(nativeResult.rillTypeName);      // e.g. "string", "list", "ordered"
console.log(nativeResult.rillTypeSignature); // e.g. "string", "list(number)"
console.log(nativeResult.value);             // JS-native value, always populated

NativeResult

interface NativeResult {
  /** Base rill type name — "string", "number", "bool", "list", "dict",
   *  "tuple", "ordered", "closure", "vector", "type", or "iterator" */
  rillTypeName: string;
  /** Full structural type signature from formatStructure,
   *  e.g. "list(number)", "dict(a: number, b: string)", "|x: number| :string" */
  rillTypeSignature: string;
  /** JS-native representation. Always populated — never undefined.
   *  Non-native types produce descriptor objects (see Descriptor shapes below). */
  value: NativeValue;
}

type NativeValue = string | number | boolean | null | NativeValue[] | { [key: string]: NativeValue };

Conversion table

Rill valuerillTypeNamevalue
null (rill null / empty string)"string"null
string"string"string
number"number"number
bool"bool"boolean
list"list"array
dict"dict"plain object
tuple"tuple"array of entry values
ordered"ordered"plain object with insertion-order keys
closure"closure"descriptor: { signature: string }
vector"vector"descriptor: { model: string, dimensions: number }
type value"type"descriptor: { name: string, signature: string }
iterator"iterator"descriptor: { done: boolean }

value is always a NativeValue — it is never undefined. JavaScript null is a valid NativeValue (rill null maps to JS null).

Descriptor shapes

Non-native rill types produce descriptor objects in value instead of primitive values:

closurevalue is { signature: string }:

const result = toNative(closureValue);
// result.rillTypeName      -> "closure"
// result.rillTypeSignature -> "|x: number| :string"
// result.value             -> { signature: "|x: number| :string" }

signature is identical to rillTypeSignature — both come from formatStructure.

vectorvalue is { model: string, dimensions: number }:

const result = toNative(vectorValue);
// result.rillTypeName      -> "vector"
// result.rillTypeSignature -> "vector"
// result.value             -> { model: "text-embedding-3-small", dimensions: 1536 }

type valuevalue is { name: string, signature: string }:

const result = toNative(typeValue);
// result.rillTypeName      -> "type"
// result.rillTypeSignature -> "list(number)"
// result.value             -> { name: "list", signature: "list(number)" }

name is the coarse type name; signature is the full structural signature from formatStructure.

iteratorvalue is { done: boolean }:

const result = toNative(iteratorValue);
// result.rillTypeName      -> "iterator"
// result.rillTypeSignature -> "iterator"
// result.value             -> { done: false }

Migration from previous NativeResult

The NativeResult interface was redesigned. Hosts consuming toNative() must update field access.

BeforeAfterAction
result.kindresult.rillTypeNameRename field access
result.typeSigresult.rillTypeSignatureRename field access
result.nativeresult.valueRename field access
result.native === null guardNot needed — value is always populatedRemove null checks
Non-native types return native: nullNon-native types return descriptor objectsRead descriptor fields instead

serializeValue

The built-in json function (used inside scripts as value -> json) throws RILL-R004 for non-serializable types (closure, iterator, vector, type value, tuple, ordered). Use toNative() at the host boundary for safe, non-throwing conversion with type metadata.


Introspection

Discover available functions, access language documentation, and check runtime version at runtime.

getFunctions()

Enumerate all callable functions registered in the runtime context:

import { createRuntimeContext, getFunctions } from '@rcrsr/rill';

const ctx = createRuntimeContext({
  functions: {
    greet: {
      params: [
        { name: 'name', type: { kind: 'string' }, annotations: { description: 'Person to greet' } },
      ],
      description: 'Generate a greeting message',
      fn: (args) => `Hello, ${args[0]}!`,
    },
  },
});

const functions = getFunctions(ctx);
// [
//   {
//     name: 'greet',
//     description: 'Generate a greeting message',
//     params: [{ name: 'name', type: 'string', description: 'Person to greet', defaultValue: undefined }]
//   },
//   ... built-in functions
// ]

Returns FunctionMetadata[] combining:

  1. Host functions (with full parameter metadata)
  2. Built-in functions
  3. Script closures (reads ^(doc: "...") annotation for description)

getLanguageReference()

Access the bundled rill language reference for LLM prompt context:

import { getLanguageReference } from '@rcrsr/rill';

const reference = getLanguageReference();
// Returns complete language reference text (syntax, operators, types, etc.)

// Use in LLM system prompts:
const systemPrompt = `You are a rill script assistant.

${reference}

Help the user write rill scripts.`;

VERSION and VERSION_INFO

Access runtime version information for logging, diagnostics, or version checks:

import { VERSION, VERSION_INFO } from '@rcrsr/rill';

// VERSION: Semver string for display
console.log(`Running rill ${VERSION}`);  // "Running rill 0.5.0"

// VERSION_INFO: Structured components for programmatic comparison
if (VERSION_INFO.major === 0 && VERSION_INFO.minor < 4) {
  console.warn('rill version too old, upgrade required');
}

// Log full version info
console.log('Runtime:', {
  version: VERSION,
  major: VERSION_INFO.major,
  minor: VERSION_INFO.minor,
  patch: VERSION_INFO.patch,
  prerelease: VERSION_INFO.prerelease,
});
ConstantTypeUse
VERSIONstringSemver string for display in logs and error messages
VERSION_INFOVersionInfoStructured major/minor/patch/prerelease for programmatic comparison

Version Comparison Example:

import { VERSION_INFO } from '@rcrsr/rill';

function checkCompatibility(): boolean {
  const required = { major: 0, minor: 4, patch: 0 };

  if (VERSION_INFO.major !== required.major) {
    return false; // Breaking change
  }

  if (VERSION_INFO.minor < required.minor) {
    return false; // Missing features
  }

  return true;
}

if (!checkCompatibility()) {
  throw new Error(`Requires rill >= 0.4.0, found ${VERSION}`);
}

getDocumentationCoverage()

Analyze documentation coverage of functions in a runtime context:

import { createRuntimeContext, getDocumentationCoverage } from '@rcrsr/rill';

const ctx = createRuntimeContext({
  functions: {
    documented: {
      params: [{ name: 'x', type: { kind: 'string' }, annotations: { description: 'Input value' } }],
      description: 'A documented function',
      fn: (args) => args[0],
    },
    undocumented: {
      params: [{ name: 'x', type: { kind: 'string' } }],
      fn: (args) => args[0],
    },
  },
});

const result = getDocumentationCoverage(ctx);
// { total: 2, documented: 1, percentage: 50 }

A function counts as documented when:

  • Has non-empty description (after trim)
  • All parameters have non-empty descriptions (after trim)

Empty context returns { total: 0, documented: 0, percentage: 100 }.

Introspection Types

interface FunctionMetadata {
  readonly name: string;        // Function name (e.g., "math::add")
  readonly description: string; // Human-readable description
  readonly params: readonly ParamMetadata[];
  readonly returnType: string;  // Return type (default: 'any')
}

interface ParamMetadata {
  readonly name: string;                    // Parameter name
  readonly type: string;                    // Type constraint (e.g., "string")
  readonly description: string;             // Parameter description
  readonly defaultValue: RillValue | undefined; // Default if optional
}

interface DocumentationCoverageResult {
  readonly total: number;       // Total function count
  readonly documented: number;  // Functions with complete documentation
  readonly percentage: number;  // Percentage (0-100), rounded to 2 decimals
}

interface VersionInfo {
  readonly major: number;        // Major version (breaking changes)
  readonly minor: number;        // Minor version (new features)
  readonly patch: number;        // Patch version (bug fixes)
  readonly prerelease?: string;  // Prerelease tag if present
}

See Also

DocumentDescription
Host IntegrationFull runtime configuration and embedding guide
Host API ReferenceSchemeResolver, ResolverResult, and all exports
ModulesModule convention for rill source files
ExtensionsBuilding reusable extension packages