@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.
Files changed (83) hide show
  1. package/dist/ast-nodes.d.ts +14 -4
  2. package/dist/ast-unions.d.ts +1 -1
  3. package/dist/constants.d.ts +1 -1
  4. package/dist/constants.js +1 -0
  5. package/dist/error-registry.js +228 -0
  6. package/dist/ext/crypto/index.js +5 -5
  7. package/dist/ext/exec/index.js +3 -3
  8. package/dist/ext/fetch/index.js +4 -4
  9. package/dist/ext/fetch/request.js +1 -1
  10. package/dist/ext/fs/index.js +101 -114
  11. package/dist/ext/fs/sandbox.d.ts +18 -0
  12. package/dist/ext/fs/sandbox.js +33 -0
  13. package/dist/ext/kv/index.js +12 -12
  14. package/dist/ext/kv/store.d.ts +1 -1
  15. package/dist/ext/kv/store.js +1 -1
  16. package/dist/generated/version-data.d.ts +1 -1
  17. package/dist/generated/version-data.js +2 -2
  18. package/dist/highlight-map.js +1 -0
  19. package/dist/index.d.ts +1 -15
  20. package/dist/index.js +1 -14
  21. package/dist/lexer/operators.js +1 -0
  22. package/dist/parser/helpers.js +1 -0
  23. package/dist/parser/parser-expr.js +44 -5
  24. package/dist/parser/parser-literals.js +111 -4
  25. package/dist/parser/parser-shape.js +2 -2
  26. package/dist/parser/parser-use.js +19 -2
  27. package/dist/parser/parser.d.ts +2 -0
  28. package/dist/parser/parser.js +2 -0
  29. package/dist/runtime/core/callable.d.ts +5 -6
  30. package/dist/runtime/core/callable.js +10 -17
  31. package/dist/runtime/core/context.d.ts +2 -2
  32. package/dist/runtime/core/context.js +8 -8
  33. package/dist/runtime/core/eval/base.d.ts +2 -2
  34. package/dist/runtime/core/eval/base.js +2 -0
  35. package/dist/runtime/core/eval/evaluator.d.ts +1 -1
  36. package/dist/runtime/core/eval/index.d.ts +2 -2
  37. package/dist/runtime/core/eval/mixins/closures.js +367 -27
  38. package/dist/runtime/core/eval/mixins/collections.js +81 -6
  39. package/dist/runtime/core/eval/mixins/control-flow.js +1 -1
  40. package/dist/runtime/core/eval/mixins/conversion.js +17 -12
  41. package/dist/runtime/core/eval/mixins/core.js +15 -2
  42. package/dist/runtime/core/eval/mixins/expressions.js +3 -2
  43. package/dist/runtime/core/eval/mixins/extraction.js +2 -3
  44. package/dist/runtime/core/eval/mixins/list-dispatch.js +1 -1
  45. package/dist/runtime/core/eval/mixins/literals.js +14 -3
  46. package/dist/runtime/core/eval/mixins/types.js +30 -1
  47. package/dist/runtime/core/eval/mixins/variables.js +3 -1
  48. package/dist/runtime/core/execute.d.ts +1 -1
  49. package/dist/runtime/core/field-descriptor.d.ts +1 -1
  50. package/dist/runtime/core/introspection.d.ts +2 -2
  51. package/dist/runtime/core/introspection.js +2 -1
  52. package/dist/runtime/core/resolvers.d.ts +1 -1
  53. package/dist/runtime/core/signals.d.ts +6 -1
  54. package/dist/runtime/core/signals.js +9 -0
  55. package/dist/runtime/core/types/constructors.d.ts +54 -0
  56. package/dist/runtime/core/types/constructors.js +201 -0
  57. package/dist/runtime/core/types/guards.d.ts +42 -0
  58. package/dist/runtime/core/types/guards.js +88 -0
  59. package/dist/runtime/core/types/index.d.ts +18 -0
  60. package/dist/runtime/core/types/index.js +19 -0
  61. package/dist/runtime/core/types/operations.d.ts +98 -0
  62. package/dist/runtime/core/types/operations.js +804 -0
  63. package/dist/runtime/core/{type-registrations.d.ts → types/registrations.d.ts} +12 -22
  64. package/dist/runtime/core/{type-registrations.js → types/registrations.js} +94 -92
  65. package/dist/runtime/core/{types.d.ts → types/runtime.d.ts} +8 -8
  66. package/dist/runtime/core/{type-structures.d.ts → types/structures.d.ts} +21 -3
  67. package/dist/runtime/core/values.d.ts +13 -102
  68. package/dist/runtime/core/values.js +26 -722
  69. package/dist/runtime/ext/builtins.js +9 -8
  70. package/dist/runtime/ext/extensions.d.ts +2 -2
  71. package/dist/runtime/ext/extensions.js +2 -1
  72. package/dist/runtime/ext/test-context.d.ts +2 -2
  73. package/dist/runtime/ext/test-context.js +3 -2
  74. package/dist/runtime/index.d.ts +8 -22
  75. package/dist/runtime/index.js +10 -16
  76. package/dist/signature-parser.d.ts +1 -1
  77. package/dist/token-types.d.ts +1 -0
  78. package/dist/token-types.js +1 -0
  79. package/package.json +1 -1
  80. /package/dist/runtime/core/{markers.d.ts → types/markers.d.ts} +0 -0
  81. /package/dist/runtime/core/{markers.js → types/markers.js} +0 -0
  82. /package/dist/runtime/core/{types.js → types/runtime.js} +0 -0
  83. /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, isTypeValue, isTuple, isOrdered, paramToFieldDef, inferStructure, structureMatches, formatStructure, anyTypeValue, structureToTypeValue, } from '../../values.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
- * ClosuresMixin implementation.
54
+ * Create a rendezvous channel for stream chunk handoff.
50
55
  *
51
- * Evaluates callable operations: host functions, closures, methods, invocations.
52
- * Handles parameter binding, type checking, and callable contexts.
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
- * Depends on:
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
- return await this.invokeScriptCallable(callable, args, callLocation);
240
+ result = await this.invokeScriptCallable(callable, args, callLocation);
117
241
  }
118
242
  else {
119
- return await this.invokeFnCallable(callable, args, callLocation, functionName);
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, isRillIterator, isVector } from '../../values.js';
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 iterators.
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 iterator).
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 iterator, got vector', node.span.start);
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 (isRillIterator(input)) {
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 iterator, got ${inferType(input)}`, node.span.start);
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 '../../values.js';
27
+ import { inferType } from '../../types/registrations.js';
28
28
  import { createChildContext } from '../../context.js';
29
29
  import { BreakSignal, ReturnSignal } from '../../signals.js';
30
30
  /**