@rcrsr/rill 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -21
- package/dist/ast-nodes.d.ts +14 -4
- package/dist/ast-unions.d.ts +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/error-registry.js +228 -0
- package/dist/ext/crypto/index.d.ts +3 -3
- package/dist/ext/crypto/index.js +62 -59
- package/dist/ext/exec/index.d.ts +3 -3
- package/dist/ext/exec/index.js +15 -9
- package/dist/ext/fetch/index.d.ts +3 -3
- package/dist/ext/fetch/index.js +17 -12
- package/dist/ext/fetch/request.js +1 -1
- package/dist/ext/fs/index.d.ts +3 -3
- package/dist/ext/fs/index.js +256 -266
- package/dist/ext/fs/sandbox.d.ts +18 -0
- package/dist/ext/fs/sandbox.js +33 -0
- package/dist/ext/kv/index.d.ts +3 -3
- package/dist/ext/kv/index.js +198 -196
- package/dist/ext/kv/store.d.ts +1 -1
- package/dist/ext/kv/store.js +2 -1
- package/dist/ext-parse-bridge.d.ts +10 -0
- package/dist/ext-parse-bridge.js +10 -0
- package/dist/generated/introspection-data.d.ts +1 -1
- package/dist/generated/introspection-data.js +385 -296
- package/dist/generated/version-data.d.ts +1 -1
- package/dist/generated/version-data.js +2 -2
- package/dist/highlight-map.js +1 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -5
- package/dist/lexer/operators.js +1 -0
- package/dist/parser/helpers.js +1 -0
- package/dist/parser/parser-expr.js +44 -5
- package/dist/parser/parser-literals.js +111 -4
- package/dist/parser/parser-shape.js +2 -2
- package/dist/parser/parser-types.js +12 -0
- package/dist/parser/parser-use.js +26 -3
- package/dist/parser/parser.d.ts +2 -0
- package/dist/parser/parser.js +2 -0
- package/dist/runtime/core/callable.d.ts +24 -13
- package/dist/runtime/core/callable.js +71 -38
- package/dist/runtime/core/context.d.ts +2 -13
- package/dist/runtime/core/context.js +80 -79
- package/dist/runtime/core/eval/base.d.ts +2 -2
- package/dist/runtime/core/eval/base.js +2 -0
- package/dist/runtime/core/eval/evaluator.d.ts +1 -1
- package/dist/runtime/core/eval/index.d.ts +3 -3
- package/dist/runtime/core/eval/index.js +11 -0
- package/dist/runtime/core/eval/mixins/closures.js +381 -41
- package/dist/runtime/core/eval/mixins/collections.js +81 -6
- package/dist/runtime/core/eval/mixins/control-flow.js +1 -1
- package/dist/runtime/core/eval/mixins/conversion.js +61 -115
- package/dist/runtime/core/eval/mixins/core.js +17 -4
- package/dist/runtime/core/eval/mixins/expressions.js +36 -27
- package/dist/runtime/core/eval/mixins/extraction.js +2 -3
- package/dist/runtime/core/eval/mixins/list-dispatch.js +1 -1
- package/dist/runtime/core/eval/mixins/literals.js +17 -6
- package/dist/runtime/core/eval/mixins/types.js +73 -54
- package/dist/runtime/core/eval/mixins/variables.js +12 -8
- package/dist/runtime/core/execute.d.ts +1 -1
- package/dist/runtime/core/field-descriptor.d.ts +3 -3
- package/dist/runtime/core/field-descriptor.js +2 -1
- package/dist/runtime/core/introspection.d.ts +2 -2
- package/dist/runtime/core/introspection.js +7 -6
- package/dist/runtime/core/resolvers.d.ts +1 -1
- package/dist/runtime/core/signals.d.ts +6 -1
- package/dist/runtime/core/signals.js +9 -0
- package/dist/runtime/core/types/constructors.d.ts +54 -0
- package/dist/runtime/core/types/constructors.js +201 -0
- package/dist/runtime/core/types/guards.d.ts +42 -0
- package/dist/runtime/core/types/guards.js +88 -0
- package/dist/runtime/core/types/index.d.ts +18 -0
- package/dist/runtime/core/types/index.js +19 -0
- package/dist/runtime/core/types/markers.d.ts +12 -0
- package/dist/runtime/core/types/markers.js +7 -0
- package/dist/runtime/core/types/operations.d.ts +98 -0
- package/dist/runtime/core/types/operations.js +804 -0
- package/dist/runtime/core/types/registrations.d.ts +126 -0
- package/dist/runtime/core/types/registrations.js +751 -0
- package/dist/runtime/core/{types.d.ts → types/runtime.d.ts} +22 -10
- package/dist/runtime/core/types/structures.d.ts +146 -0
- package/dist/runtime/core/types/structures.js +12 -0
- package/dist/runtime/core/values.d.ts +29 -209
- package/dist/runtime/core/values.js +56 -968
- package/dist/runtime/ext/builtins.js +88 -68
- package/dist/runtime/ext/extensions.d.ts +31 -125
- package/dist/runtime/ext/extensions.js +2 -94
- package/dist/runtime/ext/test-context.d.ts +28 -0
- package/dist/runtime/ext/test-context.js +155 -0
- package/dist/runtime/index.d.ts +12 -12
- package/dist/runtime/index.js +13 -5
- package/dist/signature-parser.d.ts +2 -2
- package/dist/signature-parser.js +14 -14
- package/dist/token-types.d.ts +1 -0
- package/dist/token-types.js +1 -0
- package/package.json +1 -1
- /package/dist/runtime/core/{types.js → types/runtime.js} +0 -0
|
@@ -43,38 +43,161 @@
|
|
|
43
43
|
*/
|
|
44
44
|
import { RillError, RuntimeError } from '../../../../types.js';
|
|
45
45
|
import { isCallable, isScriptCallable, isApplicationCallable, isDict, marshalArgs, } from '../../callable.js';
|
|
46
|
-
import { getVariable, pushCallFrame, popCallFrame, UNVALIDATED_METHOD_PARAMS,
|
|
47
|
-
import { inferType
|
|
46
|
+
import { getVariable, pushCallFrame, popCallFrame, UNVALIDATED_METHOD_PARAMS, } from '../../context.js';
|
|
47
|
+
import { inferType } from '../../types/registrations.js';
|
|
48
|
+
import { isTypeValue, isTuple, isOrdered, isStream, } from '../../types/guards.js';
|
|
49
|
+
import { paramToFieldDef, inferStructure, structureMatches, formatStructure, } from '../../types/operations.js';
|
|
50
|
+
import { createRillStream } from '../../types/constructors.js';
|
|
51
|
+
import { anyTypeValue, structureToTypeValue } from '../../values.js';
|
|
52
|
+
import { YieldSignal, ReturnSignal } from '../../signals.js';
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
54
|
+
* Create a rendezvous channel for stream chunk handoff.
|
|
50
55
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
56
|
+
* Producer (body) calls push() which blocks until consumer calls pull().
|
|
57
|
+
* Consumer (async generator) calls pull() which blocks until producer pushes.
|
|
58
|
+
* close() and error() signal body termination.
|
|
53
59
|
*
|
|
54
|
-
*
|
|
55
|
-
* - EvaluatorBase: ctx, checkAborted(), getNodeLocation(), withTimeout()
|
|
56
|
-
* - evaluateExpression() (from future CoreMixin composition)
|
|
57
|
-
* - evaluateBodyExpression() (from ControlFlowMixin)
|
|
58
|
-
*
|
|
59
|
-
* Methods added:
|
|
60
|
-
* - invokeCallable(callable, args, location) -> Promise<RillValue>
|
|
61
|
-
* - evaluateHostCall(node) -> Promise<RillValue>
|
|
62
|
-
* - evaluateClosureCall(node) -> Promise<RillValue>
|
|
63
|
-
* - evaluateClosureCallWithPipe(node, pipeInput) -> Promise<RillValue>
|
|
64
|
-
* - evaluatePipePropertyAccess(node, pipeInput) -> Promise<RillValue>
|
|
65
|
-
* - evaluateVariableInvoke(node, pipeInput) -> Promise<RillValue>
|
|
66
|
-
* - evaluatePipeInvoke(node, input) -> Promise<RillValue>
|
|
67
|
-
* - evaluateMethod(node, receiver) -> Promise<RillValue>
|
|
68
|
-
* - evaluateInvoke(node, receiver) -> Promise<RillValue>
|
|
69
|
-
* - evaluateArgs(argExprs) -> Promise<RillValue[]> (helper)
|
|
70
|
-
* - invokeFnCallable(callable, args, location) -> Promise<RillValue> (helper)
|
|
71
|
-
* - invokeScriptCallable(callable, args, location) -> Promise<RillValue> (helper)
|
|
72
|
-
* - createCallableContext(callable) -> RuntimeContext (helper)
|
|
73
|
-
* - validateParamType(param, value, location) -> void (helper)
|
|
74
|
-
* - bindArgsToParams(argNodes, callable, callLocation) -> Promise<BoundArgs> (helper)
|
|
60
|
+
* @internal
|
|
75
61
|
*/
|
|
62
|
+
function createStreamChannel() {
|
|
63
|
+
// Pending chunk waiting for consumer
|
|
64
|
+
let pendingChunk;
|
|
65
|
+
// Consumer waiting for a chunk
|
|
66
|
+
let pendingPull;
|
|
67
|
+
// Terminal state
|
|
68
|
+
let closed = false;
|
|
69
|
+
let closedResolution;
|
|
70
|
+
let closedError;
|
|
71
|
+
return {
|
|
72
|
+
async push(value) {
|
|
73
|
+
if (closed)
|
|
74
|
+
return;
|
|
75
|
+
// If consumer is already waiting, deliver immediately
|
|
76
|
+
if (pendingPull) {
|
|
77
|
+
const pull = pendingPull;
|
|
78
|
+
pendingPull = undefined;
|
|
79
|
+
pull.resolve({ value, done: false });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Otherwise, wait for consumer to pull
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
pendingChunk = { value, resume: resolve };
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
async pull() {
|
|
88
|
+
// If there's a pending chunk from the producer, consume it
|
|
89
|
+
if (pendingChunk) {
|
|
90
|
+
const chunk = pendingChunk;
|
|
91
|
+
pendingChunk = undefined;
|
|
92
|
+
chunk.resume(); // unblock producer
|
|
93
|
+
return { value: chunk.value, done: false };
|
|
94
|
+
}
|
|
95
|
+
// If body already completed, return done
|
|
96
|
+
if (closed) {
|
|
97
|
+
if (closedError !== undefined)
|
|
98
|
+
throw closedError;
|
|
99
|
+
return { done: true };
|
|
100
|
+
}
|
|
101
|
+
// Wait for producer to push
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
pendingPull = { resolve, reject };
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
close(_resolution) {
|
|
107
|
+
closed = true;
|
|
108
|
+
closedResolution = _resolution;
|
|
109
|
+
// Wake up waiting consumer
|
|
110
|
+
if (pendingPull) {
|
|
111
|
+
const pull = pendingPull;
|
|
112
|
+
pendingPull = undefined;
|
|
113
|
+
pull.resolve({ done: true });
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
error(err) {
|
|
117
|
+
closed = true;
|
|
118
|
+
closedError = err;
|
|
119
|
+
// Wake up waiting consumer with error
|
|
120
|
+
if (pendingPull) {
|
|
121
|
+
const pull = pendingPull;
|
|
122
|
+
pendingPull = undefined;
|
|
123
|
+
pull.reject(err);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
/** Access cached resolution value after close(). */
|
|
127
|
+
get resolution() {
|
|
128
|
+
return closedResolution ?? null;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
76
132
|
function createClosuresMixin(Base) {
|
|
77
133
|
return class ClosuresEvaluator extends Base {
|
|
134
|
+
/**
|
|
135
|
+
* Active stream channel for the current stream closure body execution.
|
|
136
|
+
* Set during stream closure body execution; null otherwise.
|
|
137
|
+
* Used by evaluateYield to push chunks to the async generator.
|
|
138
|
+
*/
|
|
139
|
+
activeStreamChannel = null;
|
|
140
|
+
/**
|
|
141
|
+
* Expected chunk type for the active stream closure.
|
|
142
|
+
* Set during stream closure body execution; null otherwise.
|
|
143
|
+
* Used by evaluateYield for chunk type validation (FR-STREAM-10).
|
|
144
|
+
*/
|
|
145
|
+
activeStreamChunkType = null;
|
|
146
|
+
/**
|
|
147
|
+
* Stack of active stream lists for IR-14 scope exit cleanup.
|
|
148
|
+
* Each entry represents a scope boundary. Streams are tracked in
|
|
149
|
+
* creation order; disposed in reverse order on scope exit.
|
|
150
|
+
*/
|
|
151
|
+
streamScopeStack = [];
|
|
152
|
+
/**
|
|
153
|
+
* Track a stream in the current scope for cleanup on scope exit (IR-14).
|
|
154
|
+
* Streams with dispose functions get cleaned up when their scope exits.
|
|
155
|
+
*/
|
|
156
|
+
trackStream(stream) {
|
|
157
|
+
const current = this.streamScopeStack[this.streamScopeStack.length - 1];
|
|
158
|
+
if (current) {
|
|
159
|
+
current.push(stream);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Dispose a list of unconsumed streams in reverse creation order (IR-14).
|
|
164
|
+
* Propagates dispose errors as RILL-R002 — does not swallow.
|
|
165
|
+
*/
|
|
166
|
+
async disposeStreams(streams) {
|
|
167
|
+
for (let i = streams.length - 1; i >= 0; i--) {
|
|
168
|
+
const stream = streams[i];
|
|
169
|
+
// Only dispose streams that are not fully consumed
|
|
170
|
+
if (stream.done)
|
|
171
|
+
continue;
|
|
172
|
+
const disposeFn = stream['__rill_stream_dispose'];
|
|
173
|
+
if (typeof disposeFn === 'function') {
|
|
174
|
+
try {
|
|
175
|
+
disposeFn();
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
// Propagate dispose errors — do not swallow (IR-14)
|
|
179
|
+
if (err instanceof RuntimeError)
|
|
180
|
+
throw err;
|
|
181
|
+
throw new RuntimeError('RILL-R002', err instanceof Error ? err.message : String(err));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Wrap evaluateBlock to add scope exit cleanup for streams (IR-14).
|
|
188
|
+
* Pushes a scope boundary, runs the block, then disposes unconsumed streams.
|
|
189
|
+
*/
|
|
190
|
+
async evaluateBlock(node) {
|
|
191
|
+
this.streamScopeStack.push([]);
|
|
192
|
+
try {
|
|
193
|
+
// Call the ControlFlowMixin's evaluateBlock via prototype chain
|
|
194
|
+
return await Object.getPrototypeOf(ClosuresEvaluator.prototype).evaluateBlock.call(this, node);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
const streams = this.streamScopeStack.pop();
|
|
198
|
+
await this.disposeStreams(streams);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
78
201
|
/**
|
|
79
202
|
* Evaluate argument expressions while preserving the current pipeValue.
|
|
80
203
|
* Used by all callable invocations to prepare arguments.
|
|
@@ -112,12 +235,18 @@ function createClosuresMixin(Base) {
|
|
|
112
235
|
pushCallFrame(this.ctx, frame);
|
|
113
236
|
}
|
|
114
237
|
try {
|
|
238
|
+
let result;
|
|
115
239
|
if (callable.kind === 'script') {
|
|
116
|
-
|
|
240
|
+
result = await this.invokeScriptCallable(callable, args, callLocation);
|
|
117
241
|
}
|
|
118
242
|
else {
|
|
119
|
-
|
|
243
|
+
result = await this.invokeFnCallable(callable, args, callLocation, functionName);
|
|
244
|
+
}
|
|
245
|
+
// IR-14: Track returned streams for scope exit cleanup
|
|
246
|
+
if (isStream(result)) {
|
|
247
|
+
this.trackStream(result);
|
|
120
248
|
}
|
|
249
|
+
return result;
|
|
121
250
|
}
|
|
122
251
|
catch (error) {
|
|
123
252
|
// Snapshot call stack onto error before finally pops the frame.
|
|
@@ -215,17 +344,58 @@ function createClosuresMixin(Base) {
|
|
|
215
344
|
validateParamType(param, value, callLocation) {
|
|
216
345
|
if (param.type === undefined)
|
|
217
346
|
return;
|
|
218
|
-
if (!
|
|
219
|
-
const expectedType =
|
|
347
|
+
if (!structureMatches(value, param.type)) {
|
|
348
|
+
const expectedType = formatStructure(param.type);
|
|
220
349
|
const actualType = inferType(value);
|
|
221
350
|
throw new RuntimeError('RILL-R001', `Parameter type mismatch: ${param.name} expects ${expectedType}, got ${actualType}`, callLocation, { paramName: param.name, expectedType, actualType });
|
|
222
351
|
}
|
|
223
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Evaluate yield: validate chunk type and throw YieldSignal (IR-6).
|
|
355
|
+
*
|
|
356
|
+
* When inside a stream closure body (activeStreamChannel is set),
|
|
357
|
+
* pushes the value to the stream channel instead of throwing.
|
|
358
|
+
* When no stream channel is active, throws YieldSignal directly
|
|
359
|
+
* (for nested evaluation contexts that catch it).
|
|
360
|
+
*
|
|
361
|
+
* Validates pipe value against declared chunk type at emission (FR-STREAM-10).
|
|
362
|
+
* Throws RILL-R004 if chunk type does not match declared type.
|
|
363
|
+
*/
|
|
364
|
+
evaluateYield(value, location) {
|
|
365
|
+
// Validate chunk type if constrained
|
|
366
|
+
if (this.activeStreamChunkType !== null) {
|
|
367
|
+
if (!structureMatches(value, this.activeStreamChunkType)) {
|
|
368
|
+
const expected = formatStructure(this.activeStreamChunkType);
|
|
369
|
+
const actual = inferType(value);
|
|
370
|
+
throw new RuntimeError('RILL-R004', `Yielded value type mismatch: expected ${expected}, got ${actual}`, location, { expected, actual });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Push to stream channel if inside a stream closure body
|
|
374
|
+
if (this.activeStreamChannel) {
|
|
375
|
+
return this.activeStreamChannel.push(value);
|
|
376
|
+
}
|
|
377
|
+
// Fallback: throw YieldSignal (caught by stream body wrapper)
|
|
378
|
+
throw new YieldSignal(value);
|
|
379
|
+
}
|
|
224
380
|
/**
|
|
225
381
|
* Invoke script callable with positional arguments.
|
|
226
382
|
* Handles parameter binding, default values, and type checking.
|
|
383
|
+
*
|
|
384
|
+
* Stream closures (returnType.structure.kind === 'stream') are detected
|
|
385
|
+
* and dispatched to invokeStreamClosure for lazy body execution (IR-13).
|
|
227
386
|
*/
|
|
228
387
|
async invokeScriptCallable(callable, args, callLocation) {
|
|
388
|
+
// IR-13: Stream closure detection — dispatch to stream-specific invocation
|
|
389
|
+
if (callable.returnType.structure.kind === 'stream') {
|
|
390
|
+
return this.invokeStreamClosure(callable, args, callLocation);
|
|
391
|
+
}
|
|
392
|
+
return this.invokeRegularScriptCallable(callable, args, callLocation);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Invoke a regular (non-stream) script callable.
|
|
396
|
+
* Extracted from invokeScriptCallable for clarity after stream dispatch.
|
|
397
|
+
*/
|
|
398
|
+
async invokeRegularScriptCallable(callable, args, callLocation) {
|
|
229
399
|
const callableCtx = this.createCallableContext(callable);
|
|
230
400
|
// Marshal positional args to named record (IC-1).
|
|
231
401
|
// Script callables always have params defined.
|
|
@@ -280,6 +450,152 @@ function createClosuresMixin(Base) {
|
|
|
280
450
|
this.ctx = savedCtx;
|
|
281
451
|
}
|
|
282
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Invoke a stream closure: produces a RillStream instead of body result (IR-13).
|
|
455
|
+
*
|
|
456
|
+
* Sets up a rendezvous channel between the closure body and an async generator.
|
|
457
|
+
* The body executes lazily as chunks are consumed:
|
|
458
|
+
* - yield in body → pushes chunk to channel → consumer yields to iterator
|
|
459
|
+
* - return in body → sets resolution value
|
|
460
|
+
* - Body end without return → resolution is null
|
|
461
|
+
*
|
|
462
|
+
* Each call produces a new, independent stream instance (idempotency).
|
|
463
|
+
*
|
|
464
|
+
* Error contracts:
|
|
465
|
+
* - Chunk type mismatch at yield → RILL-R004 (validated by evaluateYield)
|
|
466
|
+
* - Resolution type mismatch → RILL-R004
|
|
467
|
+
*/
|
|
468
|
+
async invokeStreamClosure(callable, args, callLocation) {
|
|
469
|
+
const callableCtx = this.createCallableContext(callable);
|
|
470
|
+
// Marshal positional args to named record (IC-1).
|
|
471
|
+
const record = marshalArgs(args, callable.params, {
|
|
472
|
+
functionName: '<anonymous>',
|
|
473
|
+
location: callLocation,
|
|
474
|
+
});
|
|
475
|
+
// Bind each named value into the callable context.
|
|
476
|
+
for (const [name, value] of Object.entries(record)) {
|
|
477
|
+
callableCtx.variables.set(name, value);
|
|
478
|
+
}
|
|
479
|
+
// IR-4: Block closure pipe sync
|
|
480
|
+
if (callable.params[0]?.name === '$') {
|
|
481
|
+
callableCtx.pipeValue = record['$'];
|
|
482
|
+
}
|
|
483
|
+
// Extract chunk and ret types from the stream structure
|
|
484
|
+
const streamStructure = callable.returnType.structure;
|
|
485
|
+
// Create channel and async generator for lazy body execution
|
|
486
|
+
const channel = createStreamChannel();
|
|
487
|
+
// Start body execution asynchronously.
|
|
488
|
+
// Arrow function captures `this` from invokeStreamClosure scope.
|
|
489
|
+
// The body runs concurrently with consumption, blocking at each yield
|
|
490
|
+
// until the consumer pulls the next chunk.
|
|
491
|
+
const bodyPromise = (async () => {
|
|
492
|
+
const savedCtx = this.ctx;
|
|
493
|
+
const savedChannel = this.activeStreamChannel;
|
|
494
|
+
const savedChunkType = this.activeStreamChunkType;
|
|
495
|
+
this.ctx = callableCtx;
|
|
496
|
+
this.activeStreamChannel = channel;
|
|
497
|
+
this.activeStreamChunkType = streamStructure.chunk ?? null;
|
|
498
|
+
try {
|
|
499
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
500
|
+
const result = await this.evaluateBodyExpression(callable.body);
|
|
501
|
+
// Validate resolution type if declared
|
|
502
|
+
if (streamStructure.ret !== undefined) {
|
|
503
|
+
if (!structureMatches(result, streamStructure.ret)) {
|
|
504
|
+
const expected = formatStructure(streamStructure.ret);
|
|
505
|
+
const actual = inferType(result);
|
|
506
|
+
throw new RuntimeError('RILL-R004', `Stream resolution type mismatch: expected ${expected}, got ${actual}`, callLocation, { expected, actual });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
channel.close(result);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (error instanceof ReturnSignal) {
|
|
513
|
+
// return in stream body sets resolution value
|
|
514
|
+
const result = error.value;
|
|
515
|
+
if (streamStructure.ret !== undefined) {
|
|
516
|
+
if (!structureMatches(result, streamStructure.ret)) {
|
|
517
|
+
const expected = formatStructure(streamStructure.ret);
|
|
518
|
+
const actual = inferType(result);
|
|
519
|
+
channel.error(new RuntimeError('RILL-R004', `Stream resolution type mismatch: expected ${expected}, got ${actual}`, callLocation, { expected, actual }));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
channel.close(result);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
channel.error(error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
this.ctx = savedCtx;
|
|
531
|
+
this.activeStreamChannel = savedChannel;
|
|
532
|
+
this.activeStreamChunkType = savedChunkType;
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
// Create async generator that pulls from the channel
|
|
536
|
+
async function* generateChunks() {
|
|
537
|
+
try {
|
|
538
|
+
while (true) {
|
|
539
|
+
const result = await channel.pull();
|
|
540
|
+
if (result.done)
|
|
541
|
+
return;
|
|
542
|
+
yield result.value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
// Ensure body promise settles to prevent unhandled rejections
|
|
547
|
+
await bodyPromise.catch(() => { });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Build the RillStream (IR-13)
|
|
551
|
+
const stream = createRillStream({
|
|
552
|
+
chunks: generateChunks(),
|
|
553
|
+
resolve: async () => {
|
|
554
|
+
// Wait for body to complete
|
|
555
|
+
await bodyPromise.catch(() => { });
|
|
556
|
+
return channel.resolution;
|
|
557
|
+
},
|
|
558
|
+
chunkType: streamStructure.chunk,
|
|
559
|
+
retType: streamStructure.ret,
|
|
560
|
+
});
|
|
561
|
+
return stream;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Invoke a stream as a function (IR-12).
|
|
565
|
+
* Drains remaining chunks internally, calls resolve, caches result.
|
|
566
|
+
* Subsequent calls return the cached value (idempotent).
|
|
567
|
+
*
|
|
568
|
+
* Drain is necessary for stream closures where the body blocks at
|
|
569
|
+
* yield until the consumer pulls. Without draining, resolve() would
|
|
570
|
+
* hang because the body never completes.
|
|
571
|
+
*/
|
|
572
|
+
async invokeStream(stream) {
|
|
573
|
+
const resolveFn = stream['__rill_stream_resolve'];
|
|
574
|
+
if (typeof resolveFn !== 'function') {
|
|
575
|
+
throw new RuntimeError('RILL-R002', 'Stream has no resolve function');
|
|
576
|
+
}
|
|
577
|
+
// Drain remaining chunks by walking the stream's linked list (AC-4).
|
|
578
|
+
// This unblocks the body (which may be waiting at a yield/push).
|
|
579
|
+
let current = stream;
|
|
580
|
+
while (!current.done) {
|
|
581
|
+
const nextCallable = current['next'];
|
|
582
|
+
if (!nextCallable || !isCallable(nextCallable))
|
|
583
|
+
break;
|
|
584
|
+
try {
|
|
585
|
+
const next = await this.invokeCallable(nextCallable, []);
|
|
586
|
+
if (typeof next !== 'object' ||
|
|
587
|
+
next === null ||
|
|
588
|
+
!isStream(next))
|
|
589
|
+
break;
|
|
590
|
+
current = next;
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Drain errors are expected when stream is already consumed
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return resolveFn();
|
|
598
|
+
}
|
|
283
599
|
/**
|
|
284
600
|
* Evaluate host function call: functionName(args)
|
|
285
601
|
* Looks up function in context and invokes it.
|
|
@@ -437,6 +753,10 @@ function createClosuresMixin(Base) {
|
|
|
437
753
|
throw new RuntimeError('RILL-R002', `Cannot access property on non-dict value at '${fullPath}'`, this.getNodeLocation(node));
|
|
438
754
|
}
|
|
439
755
|
}
|
|
756
|
+
// IR-12: Stream invocation — $s() returns the resolution value
|
|
757
|
+
if (isStream(value)) {
|
|
758
|
+
return this.invokeStream(value);
|
|
759
|
+
}
|
|
440
760
|
if (!isCallable(value)) {
|
|
441
761
|
throw new RuntimeError('RILL-R002', `'${fullPath}' is not callable`, this.getNodeLocation(node), { path: fullPath, actualType: inferType(value) });
|
|
442
762
|
}
|
|
@@ -541,13 +861,13 @@ function createClosuresMixin(Base) {
|
|
|
541
861
|
throw new RuntimeError('RILL-R003', `Method .${node.name} not available on callable (invoke with -> $() first)`, this.getNodeLocation(node), { methodName: node.name, receiverType: 'callable' });
|
|
542
862
|
}
|
|
543
863
|
// IR-3: .name on type values returns the typeName string (method path)
|
|
544
|
-
// IR-4: .signature on type values returns
|
|
864
|
+
// IR-4: .signature on type values returns formatStructure(structure)
|
|
545
865
|
if (isTypeValue(receiver)) {
|
|
546
866
|
if (node.name === 'name') {
|
|
547
867
|
return receiver.typeName;
|
|
548
868
|
}
|
|
549
869
|
if (node.name === 'signature') {
|
|
550
|
-
return
|
|
870
|
+
return formatStructure(receiver.structure);
|
|
551
871
|
}
|
|
552
872
|
}
|
|
553
873
|
const args = await this.evaluateArgs(node.args);
|
|
@@ -617,10 +937,10 @@ function createClosuresMixin(Base) {
|
|
|
617
937
|
throw new RuntimeError('RILL-R009', `Property '${node.name}' not found on type value (available: name, signature)`, this.getNodeLocation(node), { property: node.name, type: 'type value' });
|
|
618
938
|
}
|
|
619
939
|
// RILL-R003: method exists on other types but not this receiver's type.
|
|
620
|
-
// Methods in
|
|
940
|
+
// Methods in unvalidatedMethodReceivers handle their own receiver validation
|
|
621
941
|
// with specific error messages; skip generic RILL-R003 for them and let the
|
|
622
942
|
// method body run its own check (they exist in at least one type dict).
|
|
623
|
-
if (!
|
|
943
|
+
if (!this.ctx.unvalidatedMethodReceivers.has(node.name)) {
|
|
624
944
|
const supportedTypes = [];
|
|
625
945
|
for (const [dictType, dict] of this.ctx.typeMethodDicts) {
|
|
626
946
|
if (dict[node.name] !== undefined) {
|
|
@@ -632,14 +952,14 @@ function createClosuresMixin(Base) {
|
|
|
632
952
|
}
|
|
633
953
|
}
|
|
634
954
|
else {
|
|
635
|
-
//
|
|
955
|
+
// unvalidatedMethodReceivers: dispatch to the method in ANY type dict so the
|
|
636
956
|
// method body can run its own custom receiver validation and error message.
|
|
637
957
|
for (const [, dict] of this.ctx.typeMethodDicts) {
|
|
638
958
|
const fallbackMethod = dict[node.name];
|
|
639
959
|
if (fallbackMethod !== undefined &&
|
|
640
960
|
isApplicationCallable(fallbackMethod)) {
|
|
641
961
|
try {
|
|
642
|
-
//
|
|
962
|
+
// Unvalidated methods handle their own receiver validation.
|
|
643
963
|
// Build named record with receiver so buildMethodEntry extracts it correctly;
|
|
644
964
|
// the method body performs its own receiver type check with a custom error.
|
|
645
965
|
const fbMethodArgs = { receiver };
|
|
@@ -677,6 +997,10 @@ function createClosuresMixin(Base) {
|
|
|
677
997
|
* Calls the receiver value as a closure with the given arguments.
|
|
678
998
|
*/
|
|
679
999
|
async evaluateInvoke(node, receiver) {
|
|
1000
|
+
// IR-12: Stream invocation — $s() returns the resolution value
|
|
1001
|
+
if (isStream(receiver)) {
|
|
1002
|
+
return this.invokeStream(receiver);
|
|
1003
|
+
}
|
|
680
1004
|
if (!isCallable(receiver)) {
|
|
681
1005
|
throw new RuntimeError('RILL-R002', `Cannot invoke non-callable value (got ${inferType(receiver)})`, this.getNodeLocation(node), { actualType: inferType(receiver) });
|
|
682
1006
|
}
|
|
@@ -706,7 +1030,7 @@ function createClosuresMixin(Base) {
|
|
|
706
1030
|
const typeValue = Object.freeze({
|
|
707
1031
|
__rill_type: true,
|
|
708
1032
|
typeName: inferType(value),
|
|
709
|
-
structure:
|
|
1033
|
+
structure: inferStructure(value),
|
|
710
1034
|
});
|
|
711
1035
|
return typeValue;
|
|
712
1036
|
}
|
|
@@ -714,6 +1038,22 @@ function createClosuresMixin(Base) {
|
|
|
714
1038
|
if (isTypeValue(value)) {
|
|
715
1039
|
throw new RuntimeError('RILL-R008', `Annotation access not supported on type values`, location, { annotationKey: key });
|
|
716
1040
|
}
|
|
1041
|
+
// IR-11: Stream reflection — ^chunk and ^output on stream values
|
|
1042
|
+
if (isStream(value)) {
|
|
1043
|
+
if (key === 'chunk') {
|
|
1044
|
+
const chunkType = value['__rill_stream_chunk_type'];
|
|
1045
|
+
if (chunkType === undefined)
|
|
1046
|
+
return anyTypeValue;
|
|
1047
|
+
return structureToTypeValue(chunkType);
|
|
1048
|
+
}
|
|
1049
|
+
if (key === 'output') {
|
|
1050
|
+
const retType = value['__rill_stream_ret_type'];
|
|
1051
|
+
if (retType === undefined)
|
|
1052
|
+
return anyTypeValue;
|
|
1053
|
+
return structureToTypeValue(retType);
|
|
1054
|
+
}
|
|
1055
|
+
throw new RuntimeError('RILL-R003', `annotation not found: ^${key}`, location, { actualType: 'stream' });
|
|
1056
|
+
}
|
|
717
1057
|
// Non-callable values do not support annotation reflection
|
|
718
1058
|
if (!isCallable(value)) {
|
|
719
1059
|
throw new RuntimeError('RILL-R003', `annotation not found: ^${key}`, location, { actualType: inferType(value) });
|
|
@@ -728,10 +1068,10 @@ function createClosuresMixin(Base) {
|
|
|
728
1068
|
if (key === 'input') {
|
|
729
1069
|
// Untyped host callables have params set to undefined at runtime (see callable() factory)
|
|
730
1070
|
if (value.params === undefined) {
|
|
731
|
-
return
|
|
1071
|
+
return structureToTypeValue({ kind: 'ordered', fields: [] });
|
|
732
1072
|
}
|
|
733
|
-
const fields = value.params.map((param) => paramToFieldDef(param.name, param.type ?? {
|
|
734
|
-
return
|
|
1073
|
+
const fields = value.params.map((param) => paramToFieldDef(param.name, param.type ?? { kind: 'any' }, param.defaultValue));
|
|
1074
|
+
return structureToTypeValue({ kind: 'ordered', fields });
|
|
735
1075
|
}
|
|
736
1076
|
// IR-3: ^output reads callable.returnType directly for all kinds
|
|
737
1077
|
if (key === 'output') {
|
|
@@ -894,7 +1234,7 @@ function createClosuresMixin(Base) {
|
|
|
894
1234
|
const paramEntry = {};
|
|
895
1235
|
// Add type field if param has type annotation
|
|
896
1236
|
if (param.type !== undefined) {
|
|
897
|
-
paramEntry['type'] =
|
|
1237
|
+
paramEntry['type'] = formatStructure(param.type);
|
|
898
1238
|
}
|
|
899
1239
|
// Add __annotations field if param has parameter-level annotations
|
|
900
1240
|
if (Object.keys(param.annotations).length > 0) {
|