Host API Reference

Host API Reference

Complete Example

Extension Factory Pattern (Recommended)

import {
  parse,
  execute,
  createRuntimeContext,
  toCallable,
  RuntimeHaltSignal,
  type ExtensionFactoryResult,
} from '@rcrsr/rill';

function createMyExtension(config: { model: string }): ExtensionFactoryResult {
  return {
    value: {
      prompt: toCallable({
        params: [{ name: 'text', type: { kind: 'string' } }],
        fn: async (args) => await callLLM(args.text, config.model),
        annotations: { description: 'Send a prompt to the LLM' },
        returnType: { kind: 'string' },
      }),
    },
  };
}

const ext = createMyExtension({ model: 'claude-3-5-sonnet' });
const controller = new AbortController();

const ctx = createRuntimeContext({
  variables: { llm: ext.value },
  callbacks: { onLog: (value) => console.log('[log]', value) },
  timeout: 30000,
  signal: controller.signal,
});

try {
  const ast = parse('llm.prompt("Hello")');
  const result = await execute(ast, ctx);
  console.log('Result:', result.result);
} catch (err) {
  if (err instanceof RuntimeHaltSignal) {
    console.log('Cancelled');
  } else {
    throw err;
  }
} finally {
  await ext.dispose?.();
}

Direct Registration (Legacy)

import {
  parse,
  execute,
  createRuntimeContext,
  callable,
  structureToTypeValue,
  RuntimeHaltSignal,
  type RillValue,
} from '@rcrsr/rill';

const script = `
  $config.greeting -> prompt() => $response
  $response
`;

const controller = new AbortController();

const ctx = createRuntimeContext({
  variables: {
    config: {
      greeting: 'Say hello in French',
    },
    utils: {
      // Property-style callable (computed property)
      timestamp: callable(() => Date.now(), true),
      // Untyped callable: params is undefined, args is RillValue[] (no marshaling)
      format: callable((args) => {
        const [template, ...values] = args as unknown as RillValue[];
        return String(template).replace(/\{\}/g, () =>
          String(values.shift() ?? '')
        );
      }),
    },
  },

  functions: {
    prompt: {
      params: [{ name: 'text', type: { kind: 'string' } }],
      fn: async (args, ctx, location) => {
        console.log(`[prompt at line ${location?.line}]`);
        return await callLLM(args.text);
      },
      annotations: { description: 'Send a prompt to the LLM and return the response' },
      returnType: structureToTypeValue({ kind: 'string' }),
    },
  },

  callbacks: {
    onLog: (value) => console.log('[log]', value),
  },

  observability: {
    onStepStart: (e) => console.log(`Step ${e.index + 1}...`),
    onStepEnd: (e) => console.log(`Done (${e.durationMs}ms)`),
  },

  timeout: 30000,
  signal: controller.signal,
});

try {
  const ast = parse(script);
  const result = await execute(ast, ctx);
  console.log('Result:', result.result);
} catch (err) {
  if (err instanceof RuntimeHaltSignal) {
    console.log('Cancelled');
  } else {
    throw err;
  }
}

API Reference

Exports

// Parsing
export { parse, parseWithRecovery, parseTypeRef, createParserState, tokenize, LexerError };
export { KEYWORDS };
export type { ParseResult, RecoveryErrorNode, ErrorNode, ParseOptions };

// Execution
export { execute, createRuntimeContext, createStepper, generateManifest };
export type { RuntimeContext, RuntimeOptions, ExecutionResult };
export type { ExecutionStepper, StepResult };

// Execution context (advanced use)
export { createChildContext, getVariable, hasVariable };
export { getCallStack, pushCallFrame, popCallFrame };
export { invokeCallable };
export type { ControlFlowContext, DispatchContext, LifecycleContext, MetadataContext, ResolverContext, ScopeContext };
export type { FieldComparisonCallbacks, CallFrame };

// Resolvers
export { moduleResolver, extResolver, contextResolver };
export type { SchemeResolver, ResolverResult };

// Dispatch-boundary policy
export { createConfigFilterResolver, resolvePolicy, getExtensionIdentity };
export type { Filter, FilterResolver, ExtensionIdentity };
export type { PolicyConfig, ExtensionMethodPolicy, MethodPolicyRule, ResolvedPolicy };

// Callable types
export { callable, isCallable, isScriptCallable, isRuntimeCallable, isApplicationCallable, hydrateFieldDefaults };
export type { RillCallable, ScriptCallable, ApplicationCallable, CallableFn };

// Host function types
export type { RillParam, RillFunction };
export type { TypeStructure };
export type { TypeDefinition, TypeProtocol };
export type { RillTypeValue };
export type { MarshalOptions };
export { marshalArgs };

// Stream helpers
export { createRillStream, isRillStream };
export type { RillStream };

// Value types
export type { RillValue };
export type { RillAtomValue, RillDatetime, RillDuration, RillFieldDef, RillIterator, RillTuple, RillVector };
export type { TraceKind };

// Value conversion
export { toNative };
export type { NativeResult, NativeValue, NativeArray, NativePlainObject };

// Value construction and inspection
export { copyValue, createOrdered, createTuple, createVector };
export {
  getStatus,
  isAtom,
  isDatetime,
  isDuration,
  isInvalid,
  isIterator,
  isStream,
  isTuple,
  isTypeValue,
  isVacant,
  isVector,
};
export {
  commonType,
  compareStructuredFields,
  formatRillLiteral,
  formatStructure,
  inferStructure,
  structureEquals,
  structureMatches,
};
export { BUILT_IN_TYPES, deepEquals, deserializeValue, formatValue, inferType, serializeValue };
export { anyTypeValue, isEmpty, structureToTypeValue };
export { formatHalt };

// Field descriptors
export { buildFieldDescriptor };
export type { ConfigFieldDescriptor };

// Introspection
export { getFunctions, getLanguageReference, getDocumentationCoverage, introspectHandlerFromAST };
export type { FunctionMetadata, ParamMetadata, DocumentationCoverageResult, HandlerMetadataStatic, HandlerParamStatic };

// Version information
export { VERSION, VERSION_INFO };
export type { VersionInfo };

// Callbacks
export type { RuntimeCallbacks, ObservabilityCallbacks };
export type { StepStartEvent, StepEndEvent, HostCallEvent, FunctionReturnEvent };
export type { CaptureEvent, ErrorEvent };

// Errors
export { RillError, RuntimeError, ParseError, TimeoutError };
export { formatRillError, formatRillErrorJson, createError, renderMessage, getHelpUrl };
export { ERROR_HANDLING_PATTERNS };
export type { ErrorHandlingExample, ErrorHandlingPattern, ErrorExample, ErrorRegistry };
export type { ErrorCategory, ErrorDefinition, ErrorSeverity };
export type { FormatErrorOptions, FormatErrorJsonOptions, SourceMap };

// Utilities
export { isDict };
export type { SourceLocation, SourceSpan };

// Control flow (for advanced use)
export { BreakSignal, ReturnSignal, YieldSignal, ControlSignal, RuntimeHaltSignal };

// Syntax highlighting
export { TOKEN_HIGHLIGHT_MAP };
export type { HighlightCategory };

// Error registry
export { ERROR_REGISTRY };

// Language and tooling metadata
export { BUILTIN_FUNCTIONS, BUILTIN_METHODS, KEYWORDS, TOKEN_TYPES, VALID_TYPE_NAMES };
export type { Token, TokenType };

// Extension types
export type { ExtensionFactoryResult, ExtensionFactory, ExtensionEvent, ExtensionManifest, ExtensionConfigSchema };
export type { ExtensionFactoryCtx };

// Extension utilities
export { toCallable, createTestContext, emitExtensionEvent };
export { ExtensionBindingError };

// Atom registry (read-only access at top level)
export { resolveAtom, atomName };
export type { InvalidateMeta, TraceFrame };

// AST node types
export { isPipeChainNode };
export type {
  AnnotatedExprNode,
  AnnotatedStatementNode,
  AnnotationAccessNode,
  AnnotationArg,
  ArithHead,
  AssertNode,
  ASTNode,
  AtomLiteralNode,
  BinaryExprNode,
  BinaryOp,
  BlockNode,
  BodyNode,
  BoolLiteralNode,
  BracketAccess,
  BreakNode,
  CaptureNode,
  ChainTerminator,
  ClosureCallNode,
  ClosureNode,
  ClosureParamNode,
  ClosureSigLiteralNode,
  ConditionalNode,
  DestructNode,
  DestructPatternNode,
  DestructureNode,
  DictEntryNode,
  DictKeyComputed,
  DictKeyVariable,
  DictLiteralNode,
  DictNode,
  DoWhileLoopNode,
  ExistenceCheck,
  ExpressionNode,
  FieldAccess,
  FieldAccessAlternatives,
  FieldAccessAnnotation,
  FieldAccessBlock,
  FieldAccessComputed,
  FieldAccessLiteral,
  FieldAccessVariable,
  FrontmatterNode,
  GroupedExprNode,
  GuardBlockNode,
  HostCallNode,
  HostRefNode,
  InterpolationNode,
  InvokeNode,
  ListLiteralNode,
  ListSpreadNode,
  LiteralNode,
  MethodCallNode,
  NamedArgNode,
  NumberLiteralNode,
  OrderedLiteralNode,
  PartialExpressionNode,
  PassNode,
  PipeChainNode,
  PipeInvokeNode,
  PipeTargetNode,
  PostfixExprNode,
  PrimaryNode,
  PropertyAccess,
  RetryBlockNode,
  ReturnNode,
  ScriptNode,
  SimplePrimaryNode,
  SliceBoundNode,
  SliceNode,
  SpreadArgNode,
  StatementNode,
  StatusProbeNode,
  StringLiteralNode,
  TimeoutBlockNode,
  TupleLiteralNode,
  TypeAssertionNode,
  TypeCheckNode,
  TypeConstructorNode,
  TypeNameExprNode,
  UnaryExprNode,
  UseExprNode,
  UseIdentifier,
  VariableNode,
  WhileLoopNode,
  YieldNode,
};
export type { NodeType };
export type { FieldArg, RillTypeName, TypeRef };

// AST traversal
export { nodeAtPosition, walkAst };

Runtime error sites migrated to the halt builders throw a fatal RuntimeHaltSignal (catchable: false, code on errorId) instead of a RuntimeError when reached outside execute() and createStepper(), for example direct .fn calls on BUILTIN_METHODS entries. Argument-binding errors from invokeCallable (RILL-R001, RILL-R044, RILL-R045) still throw RuntimeError, so hosts calling it directly handle both.

Atom registration happens through ctx.registerErrorCode(name, kind) on ExtensionFactoryCtx at extension factory init time, not at the top-level export surface.

Migration note: KvExtensionContract and FsExtensionContract types moved to @rcrsr/rill-ext-kv and @rcrsr/rill-ext-fs in the rill-ext repository.

NativeResult

toNative(value: RillValue): NativeResult converts any rill value to a host-consumable structure.

interface NativeResult {
  /** Base rill type name — matches RillTypeName, or "iterator" for lazy sequences */
  rillTypeName: string;
  /** Full structural 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. */
  value: NativeValue;
}

type NativeValue =
  | string | number | boolean | null
  | NativeArray | NativePlainObject;

A dict with a number or boolean key adds a reserved __rill_typed_keys field to its NativePlainObject. This field is the lossless carrier for those keys. See Resolver Registration for the field’s shape and a conversion example.

Descriptor shapes for non-native types

Rill typevalue shape
closure{ signature: string }
vector{ model: string, dimensions: number }
type{ name: string, signature: string }
iterator{ done: boolean }
stream{ done: boolean }
datetime{ unix: number, iso: string }
duration{ months: number, ms: number }

See Host Integration for conversion examples and migration guidance.


createRuntimeContext

createRuntimeContext(options: RuntimeOptions): RuntimeContext builds the execution context passed to execute(). See Host Integration for the full RuntimeOptions field reference.

Datetime Fields

Two fields control datetime behavior in scripts.

FieldTypeDefaultDescription
timezonenumber0 (UTC)Numeric hour offset for .local_* properties on datetime values
nowMsnumberundefinedFixed unix millisecond timestamp for deterministic now() calls

timezone accepts numeric offsets only (e.g., -5 for UTC-5, 5.5 for UTC+5:30). IANA timezone name resolution (e.g., "America/New_York") requires datetime-extension.

nowMs pins now() to a fixed timestamp. Scripts that call now() without nowMs return the wall-clock time at execution.

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

// Fixed timestamp for deterministic test execution
const ctx = createRuntimeContext({
  timezone: -5,
  nowMs: new Date('2024-06-15T12:00:00Z').getTime(),
});

// Scripts using now() return 2024-06-15T12:00:00Z
// Scripts using .local_date return date in UTC-5 (2024-06-15)

TOKEN_HIGHLIGHT_MAP

TOKEN_HIGHLIGHT_MAP maps each TokenType to a HighlightCategory. Use this to build syntax highlighters for rill scripts.

import { TOKEN_HIGHLIGHT_MAP, tokenize } from '@rcrsr/rill';

const tokens = tokenize(source);
for (const token of tokens) {
  const category = TOKEN_HIGHLIGHT_MAP.get(token.type);
  if (category) applyHighlight(token, category);
}

HighlightCategory Values

CategoryDescription
keywordLanguage keywords and collection keyword prefixes
operatorAll binary and unary operators
stringString literals
numberNumeric literals
boolBoolean literals (true, false)
commentLine comments
variableNameVariables ($), identifiers, _
functionNameMethod names after a dot (METHOD_NAME, e.g. len in .len)
punctuationStructural characters (., ,, :)
bracketDelimiters ((, ), {, }, [, ])
metaAtom literals (#NAME) and frontmatter delimiters (---)

Token Changes in Explicit-Literal Syntax

The explicit-literal-syntax initiative (replacing sigil-based forms with keyword-prefixed forms) changed which tokens exist:

6 tokens added:

TokenCategorySyntax
LIST_LBRACKETkeywordlist[
DICT_LBRACKETkeyworddict[
TUPLE_LBRACKETkeywordtuple[
ORDERED_LBRACKETkeywordordered[
DESTRUCT_LANGLEkeyworddestruct<
SLICE_LANGLEkeywordslice<

2 tokens removed:

TokenWasReplaced by
STAR_LTDestruct sigil (*<)DESTRUCT_LANGLE
SLASH_LTSlice sigil (/<)SLICE_LANGLE

Highlighters that mapped the removed tokens must update to the replacement tokens.

AT remains in TOKEN_HIGHLIGHT_MAP as operator for the @ while-loop operator. Only the chain-sigil use of @ was removed; the token itself was not.

ERROR_REGISTRY

ERROR_REGISTRY provides structured access to all error definitions. Use this to build diagnostic tools or format error messages with links.

import { ERROR_REGISTRY, getHelpUrl } from '@rcrsr/rill';

const entry = ERROR_REGISTRY.get('RILL-P008');
console.log(entry?.description);  // "Bare bracket at expression start"
console.log(getHelpUrl('RILL-P008', '0.4.1'));  // "https://github.com/rcrsr/rill/blob/v0.4.1/docs/ref-errors.md#rill-p008"

getHelpUrl(errorId, version) is a standalone function, not a method on ERROR_REGISTRY. It returns https://github.com/rcrsr/rill/blob/v{version}/docs/ref-errors.md#{lowercased-errorId}, or '' if errorId or version fails validation.

Methods

MethodReturnsDescription
get(code)ErrorDefinition | undefinedLook up an error by code
has(code)booleanWhether an error code is registered
sizenumberTotal count of registered error definitions
entries()IterableIterator<[string, ErrorDefinition]>Iterate all [code, definition] pairs

Error Code Ranges

RangeCategory
RILL-L001 – RILL-L005Lexer errors
RILL-P001 – RILL-P022Parse errors (non-contiguous)
RILL-R001 – RILL-R083Runtime errors (non-contiguous)
RILL-C001 – RILL-C004Check errors

Ranges list outer bounds only; gaps exist within each range. See Error Reference for the authoritative list of assigned codes and full error descriptions and resolution strategies.

parseWithRecovery

parseWithRecovery(source: string): ParseResult parses rill source for IDE and tooling scenarios. Use this instead of parse() when you need diagnostics for incomplete or invalid source, such as while a user is still typing.

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

const result = parseWithRecovery(source);
if (!result.success) {
  console.log('Errors:', result.errors);
}
// result.ast may contain RecoveryErrorNode or PartialExpressionNode entries
FieldTypeDescription
astScriptNodeThe parsed AST, possibly containing RecoveryErrorNode or PartialExpressionNode entries
errorsParseError[]Parse errors collected during recovery; empty when success is true
successbooleantrue when parsing completed without errors

parseWithRecovery never throws. Malformed or pathological input, including deeply nested brackets that would otherwise exceed stack-depth limits, returns { ast, errors, success: false } instead of raising an exception. Callers do not need a try/catch around this function.

See RecoveryErrorNode and PartialExpressionNode for the recovery-specific AST node shapes.

nodeAtPosition

nodeAtPosition(root: ASTNode, offset: number): ASTNode | null finds the deepest AST node at a source offset. Use this to build hover, go-to-definition, or completion tooling.

import { nodeAtPosition, parse } from '@rcrsr/rill';

const ast = parse(source);
const node = nodeAtPosition(ast, 42);
if (node !== null) {
  console.log('Node at offset 42:', node.type);
}
Parameter/ReturnTypeDescription
rootASTNodeSubtree to search, typically a ScriptNode from parse()
offsetnumber0-based absolute character offset, matching SourceLocation.offset
ReturnsASTNode | nullThe deepest node whose span contains offset, or null if none does

nodeAtPosition uses half-open span containment: offset === span.start.offset is inside, offset === span.end.offset is not. An out-of-range offset returns null. The search descends through span-less field-access and dict-key segments to reach their AST node children, so a match can surface a node nested inside those segments rather than the segment itself. A VariableNode span covers only the leading $name token. nodeAtPosition still returns it when offset falls on an access-chain segment like .field or [0].

See Field access and dict key spans for the span rules governing those segments.

walkAst

walkAst(root: ASTNode, visit: (node: ASTNode) => void): void performs a pre-order depth-first traversal of an AST, calling visit once per node. Use this to build linters, analyzers, or codegen tools that need to inspect every node.

import { walkAst, parse } from '@rcrsr/rill';

const ast = parse(source);
const types: string[] = [];
walkAst(ast, (node) => {
  types.push(node.type);
});
ParameterTypeDescription
rootASTNodeSubtree to traverse, typically a ScriptNode from parse()
visit(node: ASTNode) => voidCallback invoked once for every node, including recovery nodes

walkAst visits every reachable ASTNode, including RecoveryErrorNode and PartialExpressionNode entries. Span-less field-access and dict-key segments are descended through but never passed to visit directly.

See AST Node Types for the full set of node shapes visit may receive.


BUILTIN_FUNCTIONS

BUILTIN_FUNCTIONS is a readonly string[] listing every built-in function name. Use this to drive editor completion lists for rill scripts.

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

const completions = BUILTIN_FUNCTIONS.map((name) => ({ label: name }));
registerCompletionProvider(completions);

Editor tooling (LSP completion, syntax highlighting) reads this list instead of vendoring a separate copy. The array is frozen at module load; BUILTIN_FUNCTIONS.push(...) throws.

KEYWORDS

KEYWORDS is a readonly string[] listing every language keyword. Use this alongside BUILTIN_FUNCTIONS to build completion and highlighting name lists.

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

const keywordSet = new Set(KEYWORDS);
const isKeyword = (token: string) => keywordSet.has(token);

Editor tooling (LSP completion, syntax highlighting) reads this list instead of vendoring a separate copy. The array is frozen at module load; KEYWORDS.push(...) throws.

RillParam

RillParam is the unified parameter descriptor for host functions and script closures.

interface RillParam {
  readonly name: string;
  readonly type: TypeStructure | undefined;
  readonly defaultValue: RillValue | undefined;
  readonly annotations: Record<string, RillValue>;
}
FieldTypeDescription
namestringParameter name
typeTypeStructure | undefinedStructural type descriptor. undefined = any type (no validation)
defaultValueRillValue | undefinedDefault value when argument missing. undefined = required parameter
annotationsRecord<string, RillValue>Key-value metadata. Empty object when no annotations. annotations.description is the parameter description

CallableFn

CallableFn is the function signature for typed host function implementations.

type CallableFn = (
  args: Record<string, RillValue>,
  ctx: RuntimeContextLike,
  location?: SourceLocation
) => RillValue | Promise<RillValue>;
ParameterTypeDescription
argsRecord<string, RillValue>Named arguments keyed by parameter name. The runtime marshals positional call-site arguments into this record using the declared params list before invoking fn
ctxRuntimeContextLikeRuntime context for the current execution. Provides access to variables, abort signal, and callbacks
locationSourceLocation | undefinedSource location of the call site. Present when the call originates from a rill script; undefined in programmatic calls

Returns: RillValue or Promise<RillValue>. RillStream is a valid RillValue return. Use createRillStream to build a stream from an AsyncIterable. See Stream Helpers for construction details. A return value that is not a RillValue—including one nested inside a returned array or plain object—halts at the call boundary with RILL-R085. A returned non-finite number (NaN, Infinity, -Infinity) halts catchably with #INVALID_INPUT instead. See Return Values for the full contract.

Migration note: The args parameter changed from RillValue[] (positional) to Record<string, RillValue> (named). Replace args[0] with args.paramName for each parameter. Untyped callables created via callable() (where params is undefined) bypass marshaling and still receive RillValue[]; their internal type cast is unchanged.

Typed dict keys

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

const fn: CallableFn = (args) => {
  const someDict = args.someDict; // dict[1: "a", "1": "b"]
  Object.keys(someDict); // -> ["1"], the number key 1 is missing
  return getTypedKeyEntries(someDict); // -> [{ key: 1, value: "a" }]
};

A dict value in args is a live RillValue; its number- and boolean-keyed entries do not appear in Object.keys/Object.entries. See getTypedKeyEntries(), which returns each such entry with its original number/boolean key type, without converting the dict.


RillFunction

RillFunction is the single registration path for all host functions.

interface RillFunction {
  readonly params: readonly RillParam[];
  readonly fn: CallableFn;
  readonly annotations?: Record<string, RillValue>;
  readonly returnType: RillTypeValue;
}
FieldTypeDescription
paramsreadonly RillParam[]Parameter descriptors. Runtime validates arguments before calling fn
fnCallableFnThe function implementation. The runtime marshals call-site arguments into Record<string, RillValue> keyed by parameter name before invoking fn. See CallableFn
annotationsRecord<string, RillValue> | undefinedKey-value metadata. annotations.description replaces the old description field. Optional; defaults to {}
returnTypeRillTypeValueDeclared return type. Required. Use anyTypeValue as the equivalent of “no constraint”. Runtime does NOT validate return values at call time

Tuple Parameter Hydration

When a host function parameter type is { kind: 'tuple' } and type.fields contains fields with defaultValue !== undefined, the runtime fills missing trailing positions with deep copies of those defaults before invoking fn.

This happens at Stage 2.5 of marshalArgs, after argument validation and before fn is called.

const fn: RillFunction = {
  params: [{
    name: 'coords',
    type: {
      kind: 'tuple',
      fields: [
        { type: { kind: 'number' } },
        { type: { kind: 'number' }, defaultValue: 0 },  // default: 0
      ],
    },
  }],
  fn: (args) => args.coords,  // caller passes tuple[1] → receives tuple[1, 0]
  returnType: structureToTypeValue({ kind: 'tuple' }),
};

Hydration applies only to trailing elements. A missing non-trailing element with no default raises RILL-R009. Hydration does not apply to list, dict, ordered, or closure types.


Stream Helpers

RillStream is a valid RillValue. A host function returns a stream to deliver incremental results to rill scripts.


createRillStream

createRillStream(options: {
  chunks: AsyncIterable<RillValue>;
  resolve: () => Promise<RillValue>;
  dispose?: () => void;
  chunkType?: TypeStructure;
  retType?: TypeStructure;
}): RillStream

Builds a RillStream from an AsyncIterable of chunk values and a resolution callback.

ParameterTypeDescription
chunksAsyncIterable<RillValue>Async source of chunk values. Each yielded value is one stream step
resolve() => Promise<RillValue>Called once when all chunks are consumed. Returns the final resolved value
dispose() => voidOptional cleanup function. Runs exactly once when consumption ends — see Stream disposal below
chunkTypeTypeStructureOptional structural type descriptor for chunk values
retTypeTypeStructureOptional structural type descriptor for the resolved return value

Stream disposal

dispose runs exactly once, whichever way a collection operator stops reading the stream:

  • The operator drains every chunk (seq, fan, skip, stop_when, or a take(n) larger than the stream).
  • The operator stops early (take(n), or a break in seq/acc).
  • The consuming expression halts.

dispose runs before the consuming expression returns, so a host that opens a socket or file per stream releases it on every path. It never runs more than once.

A stream stepped by hand with .first() and .next() is disposed only when a step reaches the end of the chunks. A hand-stepped chain abandoned before that point is not disposed; the host remains responsible for that resource. A stream assigned inside a block is disposed on the enclosing block’s scope exit even if no expression ever consumed it; only a stream that is the script’s own top-level result, with no enclosing block, is left for the host to dispose.

import { createRillStream } from '@rcrsr/rill';
import type { RillValue } from '@rcrsr/rill';

const fn: RillFunction = {
  params: [{ name: 'prompt', type: { kind: 'string' } }],
  fn: async (args) => {
    async function* generateChunks(): AsyncIterable<RillValue> {
      for await (const token of streamLLM(args.prompt as string)) {
        yield token as RillValue;
      }
    }
    return createRillStream({
      chunks: generateChunks(),
      resolve: async () => 'done' as RillValue,
    });
  },
  returnType: structureToTypeValue({ kind: 'stream', chunk: { kind: 'string' }, ret: { kind: 'string' } }),
};

isRillStream

isRillStream(value: RillValue): value is RillStream

Returns true when value is a RillStream. Use this to detect stream results from execute().

import { isRillStream } from '@rcrsr/rill';
import type { RillStream } from '@rcrsr/rill';

execute() Stream Result Path

When a rill script returns a stream, execute() returns a RillStream inside result.result. Use isRillStream to detect it, then consume chunks by calling step.next.fn.

import { parse, execute, createRuntimeContext, isRillStream } from '@rcrsr/rill';
import type { RillStream } from '@rcrsr/rill';

const ast = parse('use<ext:app> => $app\n$app.stream_fn("prompt text")');
const ctx = createRuntimeContext({ /* ... */ });
const result = await execute(ast, ctx);

if (isRillStream(result.result)) {
  let step = result.result as RillStream;

  // Advance through chunks; initial stream object has no value — call next first
  step = (await step.next.fn({}, ctx)) as RillStream;
  while (!step.done) {
    console.log('chunk:', step.value);
    step = (await step.next.fn({}, ctx)) as RillStream;
  }

  // Invoke the resolution callback via the hidden resolve property
  const resolveFn = (step as unknown as Record<string, () => Promise<unknown>>)[
    '__rill_stream_resolve'
  ];
  const finalValue = resolveFn ? await resolveFn() : undefined;
  console.log('resolved:', finalValue);
}

step.done is false while chunks remain. The initial stream object has no value; call step.next.fn({}, ctx) to get the first step. When step.done is true, all chunks are consumed. The resolution callback is stored as __rill_stream_resolve on the initial stream object.


generateManifest

generateManifest(ctx: RuntimeContext): string

Generates a valid rill manifest file listing all registered host functions as closure type signatures.

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

const manifest = generateManifest(ctx);
// Returns a rill file suitable for static analysis or LLM context

The output format is a rill dict literal. The dict is the last expression and becomes the script’s result value:

[
  "greet": |^("who") name: string|:string,
  "fetch": |url: string|:dict,
  "now": ||:number
]

An empty function map returns [:]. Functions with params: undefined (created via the callable() helper) are excluded.

Every entry is a closure-signature literal that evaluates to a type value, never a real closure body. This holds for zero-param functions ("now") and functions with annotated params ("greet") alike.

Parameter defaults are never emitted, because not every RillValue round-trips through a rill literal; every signature carries an explicit return type (:any when unset) so the generated manifest always parses.


SchemeResolver

SchemeResolver is the function type for custom use<scheme:...> resource resolvers.

type SchemeResolver = (
  resource: string,
  config?: unknown
) => ResolverResult | Promise<ResolverResult>;
ParameterTypeDescription
resourcestringThe resource path after the scheme prefix (e.g., "myModule" from use<host:myModule>)
configunknown | undefinedPer-scheme configuration data from RuntimeOptions.configurations

Returns: ResolverResult or a promise that resolves to one. See ResolverResult for variant shapes.

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

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

ResolverResult

ResolverResult is the discriminated union returned by a SchemeResolver.

type ResolverResult =
  | { kind: 'value'; value: RillValue }
  | { kind: 'source'; text: string };
VariantFieldsDescription
{ kind: 'value' }value: RillValueSupplies a pre-built rill value (e.g., an extension dict)
{ kind: 'source' }text: stringSupplies rill source text to be parsed and executed
import type { ResolverResult, RillValue } from '@rcrsr/rill';

// Return a pre-built value
const valueResult: ResolverResult = {
  kind: 'value',
  value: { search: mySearchFn } as RillValue,
};

// Return source text for rill to parse
const sourceResult: ResolverResult = {
  kind: 'source',
  text: `|x: string| x -> .upper`,
};

moduleResolver

moduleResolver is a built-in SchemeResolver that maps module identifiers to file paths and returns their source text.

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

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
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>

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


extResolver

extResolver is a built-in SchemeResolver that maps extension identifiers to pre-built rill values.

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

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, or a path descends into a non-dict value (callable, tuple, ordered, vector, stream)
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>

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


Dispatch-Boundary Policy

Access control and input/output transforms for extension methods resolved through use<scheme:resource>. Configured with the filterResolver option on createRuntimeContext. See Dispatch-Boundary Policy in the host integration guide for how the rules match and why a policy needs "*" to fail closed.

resolvePolicy

function resolvePolicy(
  config: PolicyConfig,
  extensions: ReadonlyMap<string, RillValue>
): ResolvedPolicy;

Resolves the "extension.method" strings in in/out rules to the callables they name, once, at setup. Halts with RILL-R087 on a reference that names no mounted callable, and RILL-R086 on a "*" rule carrying transforms.

createConfigFilterResolver

function createConfigFilterResolver(policy: ResolvedPolicy): FilterResolver;

Builds a FilterResolver over a resolved policy. A factory rather than a bare function so the policy stays in the closure, out of reach of the extension code it governs. The returned resolver implements policesExtension, which is true for an extension with rules or a "*" default.

getExtensionIdentity

function getExtensionIdentity(
  callable: RillCallable
): ExtensionIdentity | undefined;

Where a callable was resolved from, or undefined for anything that did not come through use<>. Under a configured filterResolver, values derived from a branded call’s result also carry an identity when the resolver polices that extension (see Dispatch-Boundary Policy), with () appended to the calling method’s path. This is the authorization key a custom FilterResolver should use: it is fixed when the extension resolves and does not move when a script renames or rebinds the variable holding it.

Types

interface FilterResolver {
  (
    callable: RillCallable,
    resolvedPath: string | undefined,
    ctx: RuntimeContext
  ): Filter | null;

  readonly policesExtension?: (extension: string) => boolean;
}

interface Filter {
  readonly access: 'allow' | 'deny';
  readonly inTransforms: readonly RillCallable[];
  readonly outTransforms: readonly RillCallable[];
}

interface ExtensionIdentity {
  readonly extension: string;
  readonly method: string;
}

interface MethodPolicyRule {
  readonly access: 'allow' | 'deny';
  readonly in?: readonly string[];
  readonly out?: readonly string[];
}

type ExtensionMethodPolicy = Record<string, MethodPolicyRule>;
type PolicyConfig = Record<string, ExtensionMethodPolicy>;

interface ResolvedPolicy {
  readonly rules: ReadonlyMap<string, ReadonlyMap<string, Filter>>;
  readonly defaults: ReadonlyMap<string, Filter>;
}
ErrorRaised when
RILL-R086A "*" rule declares in or out transforms
RILL-R087A transform reference cannot be resolved to a mounted callable
RILL-R088A call is denied by policy (catchable with guard or ??)
RILL-R089A policy transform re-enters itself
RILL-R090An extension value exceeds the branding budget at use<> resolution (catchable; the halt means no value binds)

Factory-Scope Context (ExtensionFactoryCtx)

ExtensionFactoryCtx is passed as the second argument to async extension factories. It provides pre-execution registration and a lifecycle abort signal.

interface ExtensionFactoryCtx {
  registerErrorCode(name: string, kind: string): void;
  readonly signal: AbortSignal;
}

type ExtensionFactory<TConfig> = (
  config: TConfig,
  ctx: ExtensionFactoryCtx
) => ExtensionFactoryResult | Promise<ExtensionFactoryResult>;
MemberTypeDescription
registerErrorCode(name: string, kind: string) => voidRegister a domain atom (e.g. "PAYMENT_FAILED", "domain"). Available as #NAME in scripts.
signalAbortSignalFires when the runtime disposes. Use to cancel long-running factory setup.

Note: The function name registerErrorCode is intentionally unchanged per TD-NOD-5. code here is an English noun meaning “error code”, not a reference to the atom primitive type. The ExtensionFactoryCtx method returns void; the underlying registry creates a RillAtom accessible at runtime as #NAME and via the standalone registerErrorCode export from @rcrsr/rill.

async function createPaymentExtension(
  config: PaymentConfig,
  ctx: ExtensionFactoryCtx
): Promise<ExtensionFactoryResult> {
  ctx.registerErrorCode('PAYMENT_FAILED', 'domain');
  ctx.registerErrorCode('CARD_DECLINED', 'domain');

  const client = await PaymentClient.connect(config.endpoint, {
    signal: ctx.signal,
  });

  return {
    value: {
      charge: toCallable({ /* ... */ }),
    },
    dispose: () => client.close(),
  };
}

See Extensions for registering error codes and returning invalid values.


RuntimeContext Helpers

Three helpers on RuntimeContext support error-aware extension functions.

ctx.invalidate

invalidate(error: unknown, meta: InvalidateMeta): RillValue

Wraps a JavaScript error as an invalid rill value. Returns a :atom-tagged invalid value the script can test with .? and .!.

ParameterTypeDescription
errorunknownThe caught JavaScript error or any throwable
metaInvalidateMetaMetadata: { code, provider, raw? } identifying the failure
fn: async (args, ctx) => {
  try {
    return await sdkClient.fetch(args.url as string);
  } catch (e) {
    return ctx.invalidate(e, { code: 'UNAVAILABLE', provider: 'my-ext' });
  }
},

ctx.catch

catch<T>(
  thunk: () => Promise<T>,
  detector: (e: unknown) => InvalidMeta | null
): Promise<T | RillValue>

Runs thunk. On throw, calls detector(e). When detector returns InvalidMeta, returns an invalid value. When detector returns null, returns an invalid value with code #R999.

ParameterTypeDescription
thunk() => Promise<T>Async operation to attempt
detector(e: unknown) => InvalidMeta | nullMaps thrown errors to InvalidMeta, or null to return an invalid value with code #R999
fn: async (args, ctx) => {
  return ctx.catch(
    () => sdkClient.fetch(args.url as string),
    (e) => {
      if (e instanceof TimeoutError) return { code: 'TIMEOUT', provider: 'my-ext' };
      if (e instanceof AuthError) return { code: 'AUTH', provider: 'my-ext' };
      return null; // re-throw unknown errors
    }
  );
},

ctx.signal

readonly signal: AbortSignal | undefined

The AbortSignal for the current execution, or undefined when no cancellation source is configured. Pass to async I/O operations to propagate cancellation from the script runtime.

fn: async (args, ctx) => {
  const response = await fetch(args.url as string, {
    signal: ctx.signal,
  });
  return response.text();
},

dispose() Lifecycle

dispose() is a top-level async function that shuts down the runtime. It is idempotent.

function dispose(): Promise<void>

Calling dispose() fires the AbortSignal on all in-flight extension calls. In-flight work receives abort via AbortSignal.any() combined with an internal timeout. After the timeout, dispose() resolves regardless.

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

const ctx = createRuntimeContext({ /* ... */ });

try {
  const result = await execute(parse(script), ctx);
  console.log(result.result);
} finally {
  await dispose();
}
BehaviorDetail
IdempotentSafe to call multiple times; subsequent calls are no-ops
Signal propagationFires AbortSignal on all registered extension factory contexts
In-flight timeoutUses AbortSignal.any() plus a grace-period timeout

Type System API

TypeStructure, TypeDefinition, TypeProtocol, RillFieldDef, and related structural type exports are documented in Host API Types Reference.


See Also