@rcrsr/rill 0.17.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/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.js +5 -5
- package/dist/ext/exec/index.js +3 -3
- package/dist/ext/fetch/index.js +4 -4
- package/dist/ext/fetch/request.js +1 -1
- package/dist/ext/fs/index.js +101 -114
- package/dist/ext/fs/sandbox.d.ts +18 -0
- package/dist/ext/fs/sandbox.js +33 -0
- package/dist/ext/kv/index.js +12 -12
- package/dist/ext/kv/store.d.ts +1 -1
- package/dist/ext/kv/store.js +1 -1
- 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 -15
- package/dist/index.js +1 -14
- 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-use.js +19 -2
- package/dist/parser/parser.d.ts +2 -0
- package/dist/parser/parser.js +2 -0
- package/dist/runtime/core/callable.d.ts +5 -6
- package/dist/runtime/core/callable.js +10 -17
- package/dist/runtime/core/context.d.ts +2 -2
- package/dist/runtime/core/context.js +8 -8
- 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 +2 -2
- package/dist/runtime/core/eval/mixins/closures.js +367 -27
- 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 +17 -12
- package/dist/runtime/core/eval/mixins/core.js +15 -2
- package/dist/runtime/core/eval/mixins/expressions.js +3 -2
- 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 +14 -3
- package/dist/runtime/core/eval/mixins/types.js +30 -1
- package/dist/runtime/core/eval/mixins/variables.js +3 -1
- package/dist/runtime/core/execute.d.ts +1 -1
- package/dist/runtime/core/field-descriptor.d.ts +1 -1
- package/dist/runtime/core/introspection.d.ts +2 -2
- package/dist/runtime/core/introspection.js +2 -1
- 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/operations.d.ts +98 -0
- package/dist/runtime/core/types/operations.js +804 -0
- package/dist/runtime/core/{type-registrations.d.ts → types/registrations.d.ts} +12 -22
- package/dist/runtime/core/{type-registrations.js → types/registrations.js} +94 -92
- package/dist/runtime/core/{types.d.ts → types/runtime.d.ts} +8 -8
- package/dist/runtime/core/{type-structures.d.ts → types/structures.d.ts} +21 -3
- package/dist/runtime/core/values.d.ts +13 -102
- package/dist/runtime/core/values.js +26 -722
- package/dist/runtime/ext/builtins.js +9 -8
- package/dist/runtime/ext/extensions.d.ts +2 -2
- package/dist/runtime/ext/extensions.js +2 -1
- package/dist/runtime/ext/test-context.d.ts +2 -2
- package/dist/runtime/ext/test-context.js +3 -2
- package/dist/runtime/index.d.ts +8 -22
- package/dist/runtime/index.js +10 -16
- package/dist/signature-parser.d.ts +1 -1
- package/dist/token-types.d.ts +1 -0
- package/dist/token-types.js +1 -0
- package/package.json +1 -1
- /package/dist/runtime/core/{markers.d.ts → types/markers.d.ts} +0 -0
- /package/dist/runtime/core/{markers.js → types/markers.js} +0 -0
- /package/dist/runtime/core/{types.js → types/runtime.js} +0 -0
- /package/dist/runtime/core/{type-structures.js → types/structures.js} +0 -0
|
@@ -44,37 +44,160 @@
|
|
|
44
44
|
import { RillError, RuntimeError } from '../../../../types.js';
|
|
45
45
|
import { isCallable, isScriptCallable, isApplicationCallable, isDict, marshalArgs, } from '../../callable.js';
|
|
46
46
|
import { getVariable, pushCallFrame, popCallFrame, UNVALIDATED_METHOD_PARAMS, } from '../../context.js';
|
|
47
|
-
import { inferType
|
|
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.
|
|
@@ -221,11 +350,52 @@ function createClosuresMixin(Base) {
|
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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) });
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* @internal
|
|
22
22
|
*/
|
|
23
23
|
import { RuntimeError } from '../../../../types.js';
|
|
24
|
-
import { inferType
|
|
24
|
+
import { inferType } from '../../types/registrations.js';
|
|
25
|
+
import { isIterator, isStream, isVector } from '../../types/guards.js';
|
|
25
26
|
import { createChildContext, getVariable } from '../../context.js';
|
|
26
27
|
import { BreakSignal } from '../../signals.js';
|
|
27
28
|
import { isCallable, isDict, marshalArgs } from '../../callable.js';
|
|
@@ -35,7 +36,7 @@ const DEFAULT_MAX_ITERATIONS = 10000;
|
|
|
35
36
|
* CollectionsMixin implementation.
|
|
36
37
|
*
|
|
37
38
|
* Evaluates collection operators: each, map, fold, filter.
|
|
38
|
-
* Handles iteration over lists, strings, dicts, and
|
|
39
|
+
* Handles iteration over lists, strings, dicts, iterators, and streams.
|
|
39
40
|
*
|
|
40
41
|
* Depends on:
|
|
41
42
|
* - EvaluatorBase: ctx, checkAborted(), getNodeLocation()
|
|
@@ -55,17 +56,18 @@ const DEFAULT_MAX_ITERATIONS = 10000;
|
|
|
55
56
|
* - getIterableElements(input) -> Promise<RillValue[]> (helper)
|
|
56
57
|
* - evaluateIteratorBody(body, element, accumulator) -> Promise<RillValue> (helper)
|
|
57
58
|
* - expandIterator(iterator, limit?) -> Promise<RillValue[]> (helper)
|
|
59
|
+
* - expandStream(stream, node, limit?) -> Promise<RillValue[]> (helper)
|
|
58
60
|
*/
|
|
59
61
|
function createCollectionsMixin(Base) {
|
|
60
62
|
return class CollectionsEvaluator extends Base {
|
|
61
63
|
/**
|
|
62
|
-
* Get elements from an iterable value (list, string, dict, or
|
|
64
|
+
* Get elements from an iterable value (list, string, dict, iterator, or stream).
|
|
63
65
|
* Throws RuntimeError if value is not iterable.
|
|
64
66
|
*/
|
|
65
67
|
async getIterableElements(input, node) {
|
|
66
68
|
// Vector guard [EC-32]
|
|
67
69
|
if (isVector(input)) {
|
|
68
|
-
throw new RuntimeError('RILL-R003', 'Collection operators require list, string, dict, or
|
|
70
|
+
throw new RuntimeError('RILL-R003', 'Collection operators require list, string, dict, iterator, or stream, got vector', node.span.start);
|
|
69
71
|
}
|
|
70
72
|
if (Array.isArray(input)) {
|
|
71
73
|
return input;
|
|
@@ -73,8 +75,12 @@ function createCollectionsMixin(Base) {
|
|
|
73
75
|
if (typeof input === 'string') {
|
|
74
76
|
return [...input];
|
|
75
77
|
}
|
|
78
|
+
// Check for stream BEFORE iterator (streams satisfy iterator shape)
|
|
79
|
+
if (isStream(input)) {
|
|
80
|
+
return this.expandStream(input, node);
|
|
81
|
+
}
|
|
76
82
|
// Check for iterator protocol BEFORE generic dict handling
|
|
77
|
-
if (
|
|
83
|
+
if (isIterator(input)) {
|
|
78
84
|
return this.expandIterator(input, node);
|
|
79
85
|
}
|
|
80
86
|
if (isDict(input)) {
|
|
@@ -85,7 +91,7 @@ function createCollectionsMixin(Base) {
|
|
|
85
91
|
value: input[key],
|
|
86
92
|
}));
|
|
87
93
|
}
|
|
88
|
-
throw new RuntimeError('RILL-R002', `Collection operators require list, string, dict, or
|
|
94
|
+
throw new RuntimeError('RILL-R002', `Collection operators require list, string, dict, iterator, or stream, got ${inferType(input)}`, node.span.start);
|
|
89
95
|
}
|
|
90
96
|
/**
|
|
91
97
|
* Expand an iterator to a list of values.
|
|
@@ -119,6 +125,75 @@ function createCollectionsMixin(Base) {
|
|
|
119
125
|
}
|
|
120
126
|
return elements;
|
|
121
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Expand a stream to a list of chunk values.
|
|
130
|
+
* Consumes async chunks by repeatedly calling the stream's next callable.
|
|
131
|
+
* Respects iteration limits to prevent unbounded expansion.
|
|
132
|
+
*
|
|
133
|
+
* On BreakSignal, calls the stream's dispose callable (if present)
|
|
134
|
+
* before re-throwing (NFR-STREAM-2).
|
|
135
|
+
*/
|
|
136
|
+
async expandStream(stream, node, limit = DEFAULT_MAX_ITERATIONS) {
|
|
137
|
+
const elements = [];
|
|
138
|
+
let current = stream;
|
|
139
|
+
let count = 0;
|
|
140
|
+
let expectedType;
|
|
141
|
+
try {
|
|
142
|
+
while (!current.done && count < limit) {
|
|
143
|
+
this.checkAborted();
|
|
144
|
+
const val = current['value'];
|
|
145
|
+
if (val !== undefined) {
|
|
146
|
+
const actualType = inferType(val);
|
|
147
|
+
if (expectedType === undefined) {
|
|
148
|
+
expectedType = actualType;
|
|
149
|
+
}
|
|
150
|
+
else if (actualType !== expectedType) {
|
|
151
|
+
throw new RuntimeError('RILL-R004', `Stream chunk type mismatch: expected ${expectedType}, got ${actualType}`, node.span.start);
|
|
152
|
+
}
|
|
153
|
+
elements.push(val);
|
|
154
|
+
}
|
|
155
|
+
count++;
|
|
156
|
+
// Invoke next() to advance the stream
|
|
157
|
+
const nextClosure = current['next'];
|
|
158
|
+
if (nextClosure === undefined || !isCallable(nextClosure)) {
|
|
159
|
+
throw new RuntimeError('RILL-R002', 'Stream .next must be a closure', node.span.start);
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
+
const nextStep = await this.invokeCallable(nextClosure, [], this.ctx, node.span.start);
|
|
163
|
+
if (typeof nextStep !== 'object' || nextStep === null) {
|
|
164
|
+
throw new RuntimeError('RILL-R002', 'Stream .next must return a stream step', node.span.start);
|
|
165
|
+
}
|
|
166
|
+
current = nextStep;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
if (e instanceof BreakSignal) {
|
|
171
|
+
// Dispose stream resources before re-throwing (IR-14)
|
|
172
|
+
// Access __rill_stream_dispose on the original stream object
|
|
173
|
+
// (step objects don't carry dispose; it lives on the root stream)
|
|
174
|
+
const disposeFn = stream['__rill_stream_dispose'];
|
|
175
|
+
if (typeof disposeFn === 'function') {
|
|
176
|
+
try {
|
|
177
|
+
disposeFn();
|
|
178
|
+
}
|
|
179
|
+
catch (disposeErr) {
|
|
180
|
+
// Propagate dispose errors as RILL-R002 (EC-15)
|
|
181
|
+
if (disposeErr instanceof RuntimeError)
|
|
182
|
+
throw disposeErr;
|
|
183
|
+
throw new RuntimeError('RILL-R002', disposeErr instanceof Error
|
|
184
|
+
? disposeErr.message
|
|
185
|
+
: String(disposeErr), node.span.start);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
if (count >= limit) {
|
|
193
|
+
throw new RuntimeError('RILL-R010', `Stream expansion exceeded ${limit} iterations`, node.span.start, { limit, iterations: count });
|
|
194
|
+
}
|
|
195
|
+
return elements;
|
|
196
|
+
}
|
|
122
197
|
/**
|
|
123
198
|
* Evaluate collection body for a single element.
|
|
124
199
|
* Handles all body forms: closure, block, grouped, variable, postfix, spread.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* @internal
|
|
25
25
|
*/
|
|
26
26
|
import { RuntimeError } from '../../../../types.js';
|
|
27
|
-
import { inferType } from '../../
|
|
27
|
+
import { inferType } from '../../types/registrations.js';
|
|
28
28
|
import { createChildContext } from '../../context.js';
|
|
29
29
|
import { BreakSignal, ReturnSignal } from '../../signals.js';
|
|
30
30
|
/**
|