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 onerrorId) instead of aRuntimeErrorwhen reached outsideexecute()andcreateStepper(), for example direct.fncalls onBUILTIN_METHODSentries. Argument-binding errors frominvokeCallable(RILL-R001,RILL-R044,RILL-R045) still throwRuntimeError, so hosts calling it directly handle both.
Atom registration happens through
ctx.registerErrorCode(name, kind)onExtensionFactoryCtxat extension factory init time, not at the top-level export surface.
Migration note:
KvExtensionContractandFsExtensionContracttypes moved to@rcrsr/rill-ext-kvand@rcrsr/rill-ext-fsin 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 type | value 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.
| Field | Type | Default | Description |
|---|---|---|---|
timezone | number | 0 (UTC) | Numeric hour offset for .local_* properties on datetime values |
nowMs | number | undefined | Fixed 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
| Category | Description |
|---|---|
keyword | Language keywords and collection keyword prefixes |
operator | All binary and unary operators |
string | String literals |
number | Numeric literals |
bool | Boolean literals (true, false) |
comment | Line comments |
variableName | Variables ($), identifiers, _ |
functionName | Method names after a dot (METHOD_NAME, e.g. len in .len) |
punctuation | Structural characters (., ,, :) |
bracket | Delimiters ((, ), {, }, [, ]) |
meta | Atom 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:
| Token | Category | Syntax |
|---|---|---|
LIST_LBRACKET | keyword | list[ |
DICT_LBRACKET | keyword | dict[ |
TUPLE_LBRACKET | keyword | tuple[ |
ORDERED_LBRACKET | keyword | ordered[ |
DESTRUCT_LANGLE | keyword | destruct< |
SLICE_LANGLE | keyword | slice< |
2 tokens removed:
| Token | Was | Replaced by |
|---|---|---|
STAR_LT | Destruct sigil (*<) | DESTRUCT_LANGLE |
SLASH_LT | Slice 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
| Method | Returns | Description |
|---|---|---|
get(code) | ErrorDefinition | undefined | Look up an error by code |
has(code) | boolean | Whether an error code is registered |
size | number | Total count of registered error definitions |
entries() | IterableIterator<[string, ErrorDefinition]> | Iterate all [code, definition] pairs |
Error Code Ranges
| Range | Category |
|---|---|
RILL-L001 – RILL-L005 | Lexer errors |
RILL-P001 – RILL-P022 | Parse errors (non-contiguous) |
RILL-R001 – RILL-R083 | Runtime errors (non-contiguous) |
RILL-C001 – RILL-C004 | Check 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
| Field | Type | Description |
|---|---|---|
ast | ScriptNode | The parsed AST, possibly containing RecoveryErrorNode or PartialExpressionNode entries |
errors | ParseError[] | Parse errors collected during recovery; empty when success is true |
success | boolean | true 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/Return | Type | Description |
|---|---|---|
root | ASTNode | Subtree to search, typically a ScriptNode from parse() |
offset | number | 0-based absolute character offset, matching SourceLocation.offset |
| Returns | ASTNode | null | The 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);
});| Parameter | Type | Description |
|---|---|---|
root | ASTNode | Subtree to traverse, typically a ScriptNode from parse() |
visit | (node: ASTNode) => void | Callback 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>;
}| Field | Type | Description |
|---|---|---|
name | string | Parameter name |
type | TypeStructure | undefined | Structural type descriptor. undefined = any type (no validation) |
defaultValue | RillValue | undefined | Default value when argument missing. undefined = required parameter |
annotations | Record<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>;| Parameter | Type | Description |
|---|---|---|
args | Record<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 |
ctx | RuntimeContextLike | Runtime context for the current execution. Provides access to variables, abort signal, and callbacks |
location | SourceLocation | undefined | Source 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;
}| Field | Type | Description |
|---|---|---|
params | readonly RillParam[] | Parameter descriptors. Runtime validates arguments before calling fn |
fn | CallableFn | The function implementation. The runtime marshals call-site arguments into Record<string, RillValue> keyed by parameter name before invoking fn. See CallableFn |
annotations | Record<string, RillValue> | undefined | Key-value metadata. annotations.description replaces the old description field. Optional; defaults to {} |
returnType | RillTypeValue | Declared 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;
}): RillStreamBuilds a RillStream from an AsyncIterable of chunk values and a resolution callback.
| Parameter | Type | Description |
|---|---|---|
chunks | AsyncIterable<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 | () => void | Optional cleanup function. Runs exactly once when consumption ends — see Stream disposal below |
chunkType | TypeStructure | Optional structural type descriptor for chunk values |
retType | TypeStructure | Optional 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 atake(n)larger than the stream). - The operator stops early (
take(n), or abreakinseq/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 RillStreamReturns 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>;| Parameter | Type | Description |
|---|---|---|
resource | string | The resource path after the scheme prefix (e.g., "myModule" from use<host:myModule>) |
config | unknown | undefined | Per-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 };| Variant | Fields | Description |
|---|---|---|
{ kind: 'value' } | value: RillValue | Supplies a pre-built rill value (e.g., an extension dict) |
{ kind: 'source' } | text: string | Supplies 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:
| Field | Type | Required | Description |
|---|---|---|---|
[moduleId] | string | Yes (at least one) | Maps a module identifier to a file path (relative to the config file’s directory) |
Error codes:
| Code | Trigger |
|---|---|
RILL-R050 | Module identifier not found in config |
RILL-R051 | File read failure (I/O error or missing file) |
RILL-R059 | Config 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:
| Field | Type | Required | Description |
|---|---|---|---|
[extensionId] | RillValue | Yes (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:
| Code | Trigger |
|---|---|
RILL-R052 | Extension identifier not found in config |
RILL-R053 | Member 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>;
}| Error | Raised when |
|---|---|
RILL-R086 | A "*" rule declares in or out transforms |
RILL-R087 | A transform reference cannot be resolved to a mounted callable |
RILL-R088 | A call is denied by policy (catchable with guard or ??) |
RILL-R089 | A policy transform re-enters itself |
RILL-R090 | An 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>;| Member | Type | Description |
|---|---|---|
registerErrorCode | (name: string, kind: string) => void | Register a domain atom (e.g. "PAYMENT_FAILED", "domain"). Available as #NAME in scripts. |
signal | AbortSignal | Fires when the runtime disposes. Use to cancel long-running factory setup. |
Note: The function name
registerErrorCodeis intentionally unchanged per TD-NOD-5.codehere is an English noun meaning “error code”, not a reference to the atom primitive type. TheExtensionFactoryCtxmethod returnsvoid; the underlying registry creates aRillAtomaccessible at runtime as#NAMEand via the standaloneregisterErrorCodeexport 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): RillValueWraps a JavaScript error as an invalid rill value. Returns a :atom-tagged invalid value the script can test with .? and .!.
| Parameter | Type | Description |
|---|---|---|
error | unknown | The caught JavaScript error or any throwable |
meta | InvalidateMeta | Metadata: { 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.
| Parameter | Type | Description |
|---|---|---|
thunk | () => Promise<T> | Async operation to attempt |
detector | (e: unknown) => InvalidMeta | null | Maps 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 | undefinedThe 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();
}| Behavior | Detail |
|---|---|
| Idempotent | Safe to call multiple times; subsequent calls are no-ops |
| Signal propagation | Fires AbortSignal on all registered extension factory contexts |
| In-flight timeout | Uses 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
- Host Integration: Embedding guide and runtime configuration
- Extensions: Reusable function packages
- Modules: Module convention
- Host API Types: TypeStructure, TypeDefinition, TypeProtocol
- Language Service API: Editor-tooling providers and a static checker with 42 rule codes