@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.
Files changed (97) hide show
  1. package/README.md +37 -21
  2. package/dist/ast-nodes.d.ts +14 -4
  3. package/dist/ast-unions.d.ts +1 -1
  4. package/dist/constants.d.ts +1 -1
  5. package/dist/constants.js +1 -0
  6. package/dist/error-registry.js +228 -0
  7. package/dist/ext/crypto/index.d.ts +3 -3
  8. package/dist/ext/crypto/index.js +62 -59
  9. package/dist/ext/exec/index.d.ts +3 -3
  10. package/dist/ext/exec/index.js +15 -9
  11. package/dist/ext/fetch/index.d.ts +3 -3
  12. package/dist/ext/fetch/index.js +17 -12
  13. package/dist/ext/fetch/request.js +1 -1
  14. package/dist/ext/fs/index.d.ts +3 -3
  15. package/dist/ext/fs/index.js +256 -266
  16. package/dist/ext/fs/sandbox.d.ts +18 -0
  17. package/dist/ext/fs/sandbox.js +33 -0
  18. package/dist/ext/kv/index.d.ts +3 -3
  19. package/dist/ext/kv/index.js +198 -196
  20. package/dist/ext/kv/store.d.ts +1 -1
  21. package/dist/ext/kv/store.js +2 -1
  22. package/dist/ext-parse-bridge.d.ts +10 -0
  23. package/dist/ext-parse-bridge.js +10 -0
  24. package/dist/generated/introspection-data.d.ts +1 -1
  25. package/dist/generated/introspection-data.js +385 -296
  26. package/dist/generated/version-data.d.ts +1 -1
  27. package/dist/generated/version-data.js +2 -2
  28. package/dist/highlight-map.js +1 -0
  29. package/dist/index.d.ts +1 -4
  30. package/dist/index.js +1 -5
  31. package/dist/lexer/operators.js +1 -0
  32. package/dist/parser/helpers.js +1 -0
  33. package/dist/parser/parser-expr.js +44 -5
  34. package/dist/parser/parser-literals.js +111 -4
  35. package/dist/parser/parser-shape.js +2 -2
  36. package/dist/parser/parser-types.js +12 -0
  37. package/dist/parser/parser-use.js +26 -3
  38. package/dist/parser/parser.d.ts +2 -0
  39. package/dist/parser/parser.js +2 -0
  40. package/dist/runtime/core/callable.d.ts +24 -13
  41. package/dist/runtime/core/callable.js +71 -38
  42. package/dist/runtime/core/context.d.ts +2 -13
  43. package/dist/runtime/core/context.js +80 -79
  44. package/dist/runtime/core/eval/base.d.ts +2 -2
  45. package/dist/runtime/core/eval/base.js +2 -0
  46. package/dist/runtime/core/eval/evaluator.d.ts +1 -1
  47. package/dist/runtime/core/eval/index.d.ts +3 -3
  48. package/dist/runtime/core/eval/index.js +11 -0
  49. package/dist/runtime/core/eval/mixins/closures.js +381 -41
  50. package/dist/runtime/core/eval/mixins/collections.js +81 -6
  51. package/dist/runtime/core/eval/mixins/control-flow.js +1 -1
  52. package/dist/runtime/core/eval/mixins/conversion.js +61 -115
  53. package/dist/runtime/core/eval/mixins/core.js +17 -4
  54. package/dist/runtime/core/eval/mixins/expressions.js +36 -27
  55. package/dist/runtime/core/eval/mixins/extraction.js +2 -3
  56. package/dist/runtime/core/eval/mixins/list-dispatch.js +1 -1
  57. package/dist/runtime/core/eval/mixins/literals.js +17 -6
  58. package/dist/runtime/core/eval/mixins/types.js +73 -54
  59. package/dist/runtime/core/eval/mixins/variables.js +12 -8
  60. package/dist/runtime/core/execute.d.ts +1 -1
  61. package/dist/runtime/core/field-descriptor.d.ts +3 -3
  62. package/dist/runtime/core/field-descriptor.js +2 -1
  63. package/dist/runtime/core/introspection.d.ts +2 -2
  64. package/dist/runtime/core/introspection.js +7 -6
  65. package/dist/runtime/core/resolvers.d.ts +1 -1
  66. package/dist/runtime/core/signals.d.ts +6 -1
  67. package/dist/runtime/core/signals.js +9 -0
  68. package/dist/runtime/core/types/constructors.d.ts +54 -0
  69. package/dist/runtime/core/types/constructors.js +201 -0
  70. package/dist/runtime/core/types/guards.d.ts +42 -0
  71. package/dist/runtime/core/types/guards.js +88 -0
  72. package/dist/runtime/core/types/index.d.ts +18 -0
  73. package/dist/runtime/core/types/index.js +19 -0
  74. package/dist/runtime/core/types/markers.d.ts +12 -0
  75. package/dist/runtime/core/types/markers.js +7 -0
  76. package/dist/runtime/core/types/operations.d.ts +98 -0
  77. package/dist/runtime/core/types/operations.js +804 -0
  78. package/dist/runtime/core/types/registrations.d.ts +126 -0
  79. package/dist/runtime/core/types/registrations.js +751 -0
  80. package/dist/runtime/core/{types.d.ts → types/runtime.d.ts} +22 -10
  81. package/dist/runtime/core/types/structures.d.ts +146 -0
  82. package/dist/runtime/core/types/structures.js +12 -0
  83. package/dist/runtime/core/values.d.ts +29 -209
  84. package/dist/runtime/core/values.js +56 -968
  85. package/dist/runtime/ext/builtins.js +88 -68
  86. package/dist/runtime/ext/extensions.d.ts +31 -125
  87. package/dist/runtime/ext/extensions.js +2 -94
  88. package/dist/runtime/ext/test-context.d.ts +28 -0
  89. package/dist/runtime/ext/test-context.js +155 -0
  90. package/dist/runtime/index.d.ts +12 -12
  91. package/dist/runtime/index.js +13 -5
  92. package/dist/signature-parser.d.ts +2 -2
  93. package/dist/signature-parser.js +14 -14
  94. package/dist/token-types.d.ts +1 -0
  95. package/dist/token-types.js +1 -0
  96. package/package.json +1 -1
  97. /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, UNVALIDATED_METHOD_RECEIVERS, } from '../../context.js';
47
- import { inferType, isTypeValue, isTuple, isOrdered, paramToFieldDef, inferStructuralType, structuralTypeMatches, formatStructuralType, anyTypeValue, rillTypeToTypeValue, } from '../../values.js';
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
- * 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.
@@ -215,17 +344,58 @@ function createClosuresMixin(Base) {
215
344
  validateParamType(param, value, callLocation) {
216
345
  if (param.type === undefined)
217
346
  return;
218
- if (!structuralTypeMatches(value, param.type)) {
219
- const expectedType = formatStructuralType(param.type);
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 formatStructuralType(structure)
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 formatStructuralType(receiver.structure);
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 UNVALIDATED_METHOD_RECEIVERS handle their own receiver validation
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 (!UNVALIDATED_METHOD_RECEIVERS.has(node.name)) {
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
- // UNVALIDATED_METHOD_RECEIVERS: dispatch to the method in ANY type dict so the
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
- // UNVALIDATED_METHOD_RECEIVERS handle their own receiver validation.
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: inferStructuralType(value),
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 rillTypeToTypeValue({ type: 'ordered', fields: [] });
1071
+ return structureToTypeValue({ kind: 'ordered', fields: [] });
732
1072
  }
733
- const fields = value.params.map((param) => paramToFieldDef(param.name, param.type ?? { type: 'any' }, param.defaultValue));
734
- return rillTypeToTypeValue({ type: 'ordered', fields });
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'] = formatStructuralType(param.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) {