• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / py-slang / 30992841357

05 Aug 2026 09:20AM UTC coverage: 86.022% (-0.09%) from 86.108%
30992841357

push

github

web-flow
Improve Error Handling for module functions (#398)

* feat: add details

* fix: errors

* fix: tests

* Fix module error call context

* Track module error locations in PVML

* Preserve module callback type metadata

* chore: format files and fix tests

* Harden module error context

* chore: format files

---------

Co-authored-by: Martin Henz <henz@nus.edu.sg>

4749 of 5961 branches covered (79.67%)

Branch coverage included in aggregate %.

174 of 243 new or added lines in 26 files covered. (71.6%)

2 existing lines in 2 files now uncovered.

10611 of 11895 relevant lines covered (89.21%)

165669.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.34
/src/conductor/GenericDataHandler.ts
1
import type { IEvaluator, IInterfacableEvaluator } from "@sourceacademy/conductor/runner";
2
import {
27✔
3
  ArrayIdentifier,
4
  ClosureIdentifier,
5
  DataType,
6
  ExternCallable,
7
  IDataHandler,
8
  IFunctionSignature,
9
  OpaqueIdentifier,
10
  PairIdentifier,
11
  TypedValue,
12
} from "@sourceacademy/conductor/types";
13
import { isSameType } from "@sourceacademy/conductor/util";
27✔
14
import { ExprNS } from "../ast-types";
15
import {
27✔
16
  InvalidArityError,
17
  InvalidArrayCreationError,
18
  InvalidIdentifierError,
19
  InvalidIndexError,
20
  InvalidLengthError,
21
  InvalidOpaqueUpdateError,
22
  InvalidTypeError,
23
} from "./errors";
24
const DEFAULT_VALUES = {
27✔
25
  [DataType.NUMBER]: { type: DataType.NUMBER, value: 0 },
26
  [DataType.CONST_STRING]: { type: DataType.CONST_STRING, value: "" },
27
  [DataType.BOOLEAN]: { type: DataType.BOOLEAN, value: false },
28
  [DataType.VOID]: { type: DataType.VOID, value: undefined },
29
  [DataType.EMPTY_LIST]: { type: DataType.EMPTY_LIST, value: null },
30
  [DataType.INTEGER]: { type: DataType.INTEGER, value: 0n },
31
};
32

33
/**
34
 * A conductor `IDataHandler` implementation with no engine-specific logic:
35
 * pairs, arrays, closures and opaques are all just bookkeeping over plain
36
 * Maps keyed by an incrementing id, and the list helpers (`list`/`is_list`/
37
 * `list_to_vec`/`accumulate`/`length`) walk that pair structure generically.
38
 * The only place an engine's own semantics enter the picture is the
39
 * `ExternCallable` passed to `closure_make` (authored by that engine's own
40
 * module-interop layer) and the arguments/results flowing through
41
 * `closure_call`/`closure_call_unchecked` — this class never inspects them.
42
 *
43
 * Originally written inline in PyCseEvaluatorBase (see PyCseEvaluator.ts);
44
 * extracted so every evaluator that talks to conductor modules (CSE, py2js,
45
 * and eventually WASM/PVML) shares one implementation instead of
46
 * re-deriving the same identifier-table bookkeeping per engine. An evaluator
47
 * holds one instance (`private dataHandler = new GenericDataHandler(variant)`) and
48
 * hands it to both `context.evaluator` (or the engine's equivalent) and
49
 * `conductor.registerPlugin(ModuleLoaderRunnerPlugin, conductor, dataHandler)`.
50
 */
51
export class GenericDataHandler implements IDataHandler {
27✔
52
  hasDataInterface = true as const;
186✔
53
  private pairMap = new Map<
186✔
54
    PairIdentifier,
55
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
56
  >();
57
  private arrayMap = new Map<
186✔
58
    ArrayIdentifier<DataType>,
59
    { type: DataType; elements: TypedValue<DataType>[] }
60
  >();
61
  private closureMap = new Map<
186✔
62
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
    ClosureIdentifier<any>,
64
    {
65
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
      sig: IFunctionSignature<any, any>;
67
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
      func: ExternCallable<any, any>;
69
      dependsOn?: (TypedValue<DataType> | null)[];
70
      isVararg?: boolean;
71
    }
72
  >();
73
  private opaqueMap = new Map<OpaqueIdentifier, { value: unknown; immutable: boolean }>();
186✔
74
  private uniqueId = 0;
186✔
75

76
  private currentCall: ExprNS.Call | undefined = undefined;
186✔
77
  private currentSource: string | undefined = undefined;
186✔
78

79
  private getTypeName<T extends DataType>(type: T, value?: TypedValue<T>): string {
80
    switch (type) {
22!
81
      case DataType.NUMBER:
82
        return "'int' or 'float'";
11✔
83
      case DataType.CONST_STRING:
84
        return "'str'";
6✔
85
      case DataType.BOOLEAN:
NEW
86
        return "'bool'";
×
87
      case DataType.VOID:
88
      case DataType.EMPTY_LIST:
NEW
89
        return "'NoneType'";
×
90
      case DataType.INTEGER:
NEW
91
        return "'int'";
×
92
      case DataType.ARRAY:
NEW
93
        return this.variant >= 3 ? "'list'" : "'pair'";
×
94
      case DataType.LIST:
95
        return "'llist'";
5✔
96
      case DataType.PAIR:
NEW
97
        return "'pair'";
×
98
      case DataType.CLOSURE:
NEW
99
        return "'function'";
×
100
      case DataType.ANY:
NEW
101
        return "'any'";
×
102
      case DataType.OPAQUE: {
NEW
103
        if (value === undefined) {
×
NEW
104
          return "'opaque'";
×
105
        }
NEW
106
        const opaque = this.opaqueMap.get(value.value as OpaqueIdentifier);
×
NEW
107
        if (!opaque) {
×
NEW
108
          return "'opaque'";
×
109
        }
NEW
110
        const name = opaque.value;
×
NEW
111
        if (typeof name === "object" && name !== null && name.constructor.name !== "Object") {
×
NEW
112
          return name.constructor.name;
×
113
        }
NEW
114
        return "'opaque'";
×
115
      }
116
    }
117
  }
118

119
  constructor(public readonly variant: number) {}
186✔
120

121
  getCurrentModuleName(): string | undefined {
NEW
122
    return undefined;
×
123
  }
124

125
  setCurrentCall(call: ExprNS.Call | undefined): void {
126
    this.currentCall = call;
20✔
127
  }
128

129
  /**
130
   * Records a generated evaluator's call site.  Unlike the CSE evaluator,
131
   * compiled evaluators cannot retain the AST object at runtime, but the
132
   * diagnostic code only needs its source span.
133
   */
134
  setCurrentCallLocation(start: number, end: number): void {
135
    const source = this.currentSource;
373✔
136
    if (!source || start < 0 || end < start || end > source.length) {
373✔
137
      this.currentCall = undefined;
55✔
138
      return;
55✔
139
    }
140
    const lineStart = source.lastIndexOf("\n", start - 1) + 1;
318✔
141
    const line = source.slice(0, lineStart).split("\n").length;
318✔
142
    this.currentCall = {
318✔
143
      startToken: { indexInSource: start, line, column: start - lineStart, lexeme: source[start] },
144
      endToken: {
145
        indexInSource: Math.max(start, end - 1),
146
        line,
147
        column: Math.max(0, end - 1 - lineStart),
148
        lexeme: source.slice(Math.max(start, end - 1), end),
149
      },
150
    } as unknown as ExprNS.Call;
151
  }
152

153
  setCurrentSource(source: string | undefined): void {
154
    this.currentSource = source;
218✔
155
    // A call node is meaningful only within the source it came from.  In
156
    // persistent evaluators, retaining it across chunks/files would point a
157
    // later module-loading error at an unrelated earlier program.
158
    this.currentCall = undefined;
218✔
159
  }
160

161
  pair_make(
162
    head: TypedValue<DataType>,
163
    tail: TypedValue<DataType>,
164
  ): Promise<TypedValue<DataType.PAIR>> {
165
    this.pairMap.set(this.uniqueId++ as PairIdentifier, { head, tail });
22✔
166
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
22✔
167
  }
168
  /**
169
   * Bridges pair_head/pair_tail/pair_sethead/pair_settail/pair_assert onto a DataType.ARRAY value
170
   * too, not just a genuine PAIR: per Martin, a pair is just a 2-element array, and module code is
171
   * free to keep calling pair_head/pair_tail for clarity even once the underlying value it's
172
   * handed is array-backed (e.g. a value pythonToModule built directly as an ARRAY). Reads/writes
173
   * index 0/1 directly; throws the same "Invalid pair identifier" a genuine dangling PAIR would,
174
   * for a dangling/too-short array.
175
   */
176
  private resolvePairView(
177
    p: TypedValue<DataType.PAIR>,
178
  ): { head: TypedValue<DataType>; tail: TypedValue<DataType> } & (
179
    | { kind: "pair"; pair: { head: TypedValue<DataType>; tail: TypedValue<DataType> } }
180
    | { kind: "array"; array: { type: DataType; elements: TypedValue<DataType>[] } }
181
  ) {
182
    if ((p.type as DataType) === DataType.ARRAY) {
29✔
183
      const array = this.arrayMap.get(p.value as unknown as ArrayIdentifier<DataType>);
14✔
184
      if (!array || array.elements.length < 2) {
14✔
185
        throw new InvalidIdentifierError(
1✔
186
          this.currentCall,
187
          this.currentSource,
188

189
          p.value,
190
          "pair",
191
        );
192
      }
193
      return { kind: "array", array, head: array.elements[0], tail: array.elements[1] };
13✔
194
    }
195
    const pair = this.pairMap.get(p.value);
15✔
196
    if (!pair) {
15!
NEW
197
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, p.value, "pair");
×
198
    }
199
    return { kind: "pair", pair, head: pair.head, tail: pair.tail };
15✔
200
  }
201
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
202
    return Promise.resolve(this.resolvePairView(p).head);
15✔
203
  }
204
  pair_sethead(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
205
    const view = this.resolvePairView(p);
1✔
206
    if (view.kind === "array") {
1!
207
      view.array.elements[0] = tv;
1✔
208
    } else {
209
      view.pair.head = tv;
×
210
    }
211
    return Promise.resolve();
1✔
212
  }
213
  pair_tail(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
214
    return Promise.resolve(this.resolvePairView(p).tail);
10✔
215
  }
216
  pair_settail(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
217
    const view = this.resolvePairView(p);
1✔
218
    if (view.kind === "array") {
1!
219
      view.array.elements[1] = tv;
1✔
220
    } else {
221
      view.pair.tail = tv;
×
222
    }
223
    return Promise.resolve();
1✔
224
  }
225
  pair_assert(
226
    p: TypedValue<DataType.PAIR>,
227
    headType?: DataType,
228
    tailType?: DataType,
229
  ): Promise<void> {
230
    const { head, tail } = this.resolvePairView(p);
2✔
231
    if (headType && head.type !== headType) {
2✔
232
      throw new InvalidTypeError(
1✔
233
        this.currentCall,
234
        this.currentSource,
235
        "head of",
236
        this.getTypeName(headType),
237
        this.getTypeName(head.type, head),
238
      );
239
    }
240
    if (tailType && tail.type !== tailType) {
1!
NEW
241
      throw new InvalidTypeError(
×
242
        this.currentCall,
243
        this.currentSource,
244
        "tail of",
245
        this.getTypeName(tailType),
246
        this.getTypeName(tail.type, tail),
247
      );
248
    }
249
    return Promise.resolve();
1✔
250
  }
251
  array_make<T extends DataType>(
252
    t: T,
253
    len: number,
254
    init?: TypedValue<NoInfer<T>>,
255
  ): Promise<TypedValue<DataType.ARRAY, NoInfer<T>>> {
256
    if (init === undefined && !(t in DEFAULT_VALUES)) {
26!
NEW
257
      throw new InvalidArrayCreationError(
×
258
        this.currentCall,
259
        this.currentSource,
260
        this.getTypeName(t),
261
      );
262
    }
263
    const elements = new Array(len).fill(init ?? DEFAULT_VALUES[t as keyof typeof DEFAULT_VALUES]);
26✔
264
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
26✔
265
      type: DataType.ARRAY,
266
      value: this.uniqueId++ as ArrayIdentifier<T>,
267
    };
268
    this.arrayMap.set(arrayValue.value, { type: t, elements });
26✔
269
    return Promise.resolve(arrayValue);
26✔
270
  }
271
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
272
    const array = this.arrayMap.get(a.value);
6✔
273
    if (!array) {
6!
NEW
274
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
275
    }
276
    return Promise.resolve(array.elements.length);
6✔
277
  }
278
  array_get<T extends DataType>(
279
    a: TypedValue<DataType.ARRAY, T>,
280
    idx: number,
281
  ): Promise<TypedValue<NoInfer<T>>>;
282
  array_get(
283
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
284
    idx: number,
285
  ): Promise<TypedValue<DataType>> {
286
    const array = this.arrayMap.get(a.value);
10✔
287

288
    if (!array) {
10!
NEW
289
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
290
    }
291

292
    if (idx < 0 || idx >= array.elements.length) {
10✔
293
      throw new InvalidIndexError(this.currentCall, this.currentSource, idx, array.elements.length);
3✔
294
    }
295

296
    const value = array.elements[idx];
7✔
297

298
    return Promise.resolve(value);
7✔
299
  }
300

301
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
302
    const array = this.arrayMap.get(a.value);
×
303
    if (array === undefined) {
×
NEW
304
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
305
    }
306
    return Promise.resolve(array.type as NoInfer<T>);
×
307
  }
308
  array_set(
309
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
310
    idx: number,
311
    tv: TypedValue<DataType>,
312
  ): Promise<void>;
313
  array_set<T extends DataType>(
314
    a: TypedValue<DataType.ARRAY, T>,
315
    idx: number,
316
    tv: TypedValue<NoInfer<T>>,
317
  ): Promise<void> {
318
    const array = this.arrayMap.get(a.value);
32✔
319

320
    if (!array) {
32!
NEW
321
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
322
    }
323

324
    if (idx < 0 || idx >= array.elements.length) {
32!
NEW
325
      throw new InvalidIndexError(
×
326
        this.currentCall,
327
        this.currentSource,
328
        idx,
329
        array.elements.length,
330
        true,
331
      );
332
    }
333

334
    if (tv.type !== array.type && array.type !== DataType.ANY) {
32!
NEW
335
      throw new InvalidTypeError(
×
336
        this.currentCall,
337
        this.currentSource,
338
        `element at index ${idx}'s`,
339
        this.getTypeName(array.type),
340
        this.getTypeName(tv.type, tv),
341
      );
342
    }
343

344
    array.elements[idx] = tv;
32✔
345

346
    return Promise.resolve();
32✔
347
  }
348
  array_assert<T extends DataType>(
349
    a: TypedValue<DataType.ARRAY>,
350
    type?: T,
351
    length?: number,
352
  ): Promise<void> {
353
    const array = this.arrayMap.get(a.value);
×
354
    if (!array) {
×
NEW
355
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
356
    }
357
    if (type !== undefined && array.type !== type) {
×
NEW
358
      throw new InvalidTypeError(
×
359
        this.currentCall,
360
        this.currentSource,
361
        `array`,
362
        this.getTypeName(type),
363
        this.getTypeName(array.type),
364
      );
365
    }
366
    if (length !== undefined && array.elements.length !== length) {
×
NEW
367
      throw new InvalidLengthError(
×
368
        this.currentCall,
369
        this.currentSource,
370
        "array",
371
        length,
372
        array.elements.length,
373
      );
374
    }
375
    return Promise.resolve();
×
376
  }
377
  closure_make<const Arg extends readonly DataType[], const Ret extends DataType>(
378
    sig: IFunctionSignature<Arg, Ret>,
379
    func: ExternCallable<Arg, Ret>,
380
    dependsOn?: (TypedValue<DataType> | null)[],
381
    isVararg?: boolean,
382
  ): Promise<TypedValue<DataType.CLOSURE, Ret>> {
383
    const closureValue: TypedValue<DataType.CLOSURE, Ret> = {
520✔
384
      type: DataType.CLOSURE,
385
      value: this.uniqueId++ as ClosureIdentifier<Ret>,
386
    };
387
    this.closureMap.set(closureValue.value, { sig, func, dependsOn, isVararg });
520✔
388
    return Promise.resolve(closureValue);
520✔
389
  }
390
  closure_is_vararg(c: TypedValue<DataType.CLOSURE>): Promise<boolean> {
391
    return Promise.resolve(this.closureMap.get(c.value)?.isVararg ?? false);
11✔
392
  }
393
  closure_arity(c: TypedValue<DataType.CLOSURE>): Promise<number> {
394
    return Promise.resolve(this.closureMap.get(c.value)?.sig.args.length ?? 0);
35!
395
  }
396
  async *closure_call<T extends DataType>(
397
    c: TypedValue<DataType.CLOSURE, T>,
398
    args: TypedValue<DataType>[],
399
    returnType: T,
400
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
401
    const closure = this.closureMap.get(c.value);
281✔
402
    if (closure === undefined) {
281!
NEW
403
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, c.value, "closure");
×
404
    }
405
    if (
281!
406
      args.length < closure.sig.args.length ||
841✔
407
      (!closure.isVararg && args.length > closure.sig.args.length)
408
    ) {
NEW
409
      throw new InvalidArityError(
×
410
        this.currentCall,
411
        this.currentSource,
412
        closure.sig.args.length,
413
        args.length,
414
      );
415
    }
416
    for (const [i, arg] of args.entries()) {
281✔
417
      if (i >= closure.sig.args.length) {
225!
NEW
418
        break;
×
419
      }
420
      if (closure.sig.args[i] === DataType.ANY) {
225✔
421
        continue;
13✔
422
      }
423
      // If the argument is a pair or list type and the return type is an array (or vice-versa), skip for now till we remove the pair type.
424
      if (
212✔
425
        (closure.sig.args[i] === DataType.PAIR || closure.sig.args[i] === DataType.LIST) &&
431✔
426
        arg.type === DataType.ARRAY
427
      ) {
428
        continue;
9✔
429
      }
430
      if (closure.sig.args[i] === DataType.ARRAY && arg.type == DataType.PAIR) {
203!
NEW
431
        continue;
×
432
      }
433
      if (!isSameType(arg.type, closure.sig.args[i])) {
203✔
434
        throw new InvalidTypeError(
5✔
435
          this.currentCall,
436
          this.currentSource,
437
          `argument ${i}`,
438
          this.getTypeName(closure.sig.args[i]),
439
          this.getTypeName(arg.type, arg),
440
        );
441
      }
442
    }
443
    const result = yield* closure.func(...args);
276✔
444
    if (
274!
445
      result.type !== returnType &&
514!
446
      returnType !== DataType.ANY &&
447
      !(
448
        (returnType === DataType.PAIR || returnType === DataType.LIST) &&
×
449
        result.type === DataType.ARRAY
450
      ) &&
451
      !(returnType === DataType.ARRAY && result.type === DataType.PAIR)
×
452
    ) {
NEW
453
      throw new InvalidTypeError(
×
454
        this.currentCall,
455
        this.currentSource,
456
        "return",
457
        this.getTypeName(returnType),
458
        this.getTypeName(result.type, result),
459
      );
460
    }
461
    return result as TypedValue<NoInfer<T>>;
274✔
462
  }
463
  async *closure_call_unchecked<T extends DataType>(
464
    c: TypedValue<DataType.CLOSURE, T>,
465
    args: TypedValue<DataType>[],
466
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
467
    const closure = this.closureMap.get(c.value);
9✔
468
    if (closure === undefined) {
9!
NEW
469
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, c.value, "closure");
×
470
    }
471
    return (yield* closure.func(...args)) as TypedValue<NoInfer<T>>;
9✔
472
  }
473
  /**
474
   * Fast path for a closure that provably never needs to leave the current
475
   * synchronous call - e.g. a scalar-in/scalar-out wave function sampled
476
   * 44100x/sec by the sound module. `func` (an `ExternCallable`, normally
477
   * only callable as an AsyncGenerator per conductor's own contract) may
478
   * additionally carry a `.sync` escape hatch: a plain function computing
479
   * the exact same result with no Promise/generator indirection at all. An
480
   * engine's module-interop layer sets `.sync` only when it can prove the
481
   * closure never needs a real host round-trip (see py2js's moduleInterop.ts
482
   * pyClosureFunc); a closure with no such proof (every CSE-machine closure
483
   * today, or a py2js closure that touches something asyncOnly) simply never
484
   * gets one.
485
   *
486
   * Returns `undefined` when the closure has no sync form - the signal for
487
   * "fall back to closure_call_unchecked" - which is unambiguous because a
488
   * TypedValue always wraps a real `{ type, value }` pair, even for
489
   * DataType.VOID; the bare JS value `undefined` is never a legitimate
490
   * closure result.
491
   */
492
  closure_call_sync<T extends DataType>(
493
    c: TypedValue<DataType.CLOSURE, T>,
494
    args: TypedValue<DataType>[],
495
  ): TypedValue<NoInfer<T>> | undefined {
496
    const func = this.closureMap.get(c.value)?.func as
14✔
497
      | (ExternCallable<DataType[], T> & {
498
          sync?: (...a: TypedValue<DataType>[]) => TypedValue<DataType> | undefined;
499
        })
500
      | undefined;
501
    return func?.sync?.(...args) as TypedValue<NoInfer<T>> | undefined;
14✔
502
  }
503
  closure_arity_assert(c: TypedValue<DataType.CLOSURE>, arity: number): Promise<void> {
504
    const closure = this.closureMap.get(c.value);
×
505
    if (!closure) {
×
NEW
506
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, c.value, "closure");
×
507
    }
508
    if (closure.sig.args.length !== arity && !closure.isVararg) {
×
NEW
509
      throw new InvalidArityError(
×
510
        this.currentCall,
511
        this.currentSource,
512
        arity,
513
        closure.sig.args.length,
514
      );
515
    }
516
    return Promise.resolve();
×
517
  }
518
  opaque_make(v: unknown, immutable?: boolean): Promise<TypedValue<DataType.OPAQUE>> {
519
    const opaqueValue: TypedValue<DataType.OPAQUE> = {
8✔
520
      type: DataType.OPAQUE,
521
      value: this.uniqueId++ as OpaqueIdentifier,
522
    };
523
    this.opaqueMap.set(opaqueValue.value, { value: v, immutable: immutable || false });
8✔
524
    return Promise.resolve(opaqueValue);
8✔
525
  }
526
  opaque_get(o: TypedValue<DataType.OPAQUE>): Promise<unknown> {
527
    const opaque = this.opaqueMap.get(o.value);
7✔
528
    if (!opaque) {
7!
NEW
529
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, o.value, "opaque");
×
530
    }
531
    return Promise.resolve(opaque.value);
7✔
532
  }
533
  opaque_update(o: TypedValue<DataType.OPAQUE>, v: unknown): Promise<void> {
534
    const opaque = this.opaqueMap.get(o.value);
×
535
    if (!opaque) {
×
NEW
536
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, o.value, "opaque");
×
537
    }
538
    if (opaque.immutable) {
×
NEW
539
      throw new InvalidOpaqueUpdateError(this.currentCall, this.currentSource, o.value);
×
540
    }
541
    opaque.value = v;
×
542
    return Promise.resolve();
×
543
  }
544
  tie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
545
    throw new Error("Method not implemented.");
×
546
  }
547
  untie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
548
    throw new Error("Method not implemented.");
×
549
  }
550
  async list(...elements: TypedValue<DataType>[]): Promise<TypedValue<DataType.LIST>> {
551
    const list = await elements.reduceRight(
5✔
552
      async (acc, el) => {
553
        return this.pair_make(el, await acc);
13✔
554
      },
555
      Promise.resolve({ type: DataType.EMPTY_LIST, value: null }) as Promise<
556
        TypedValue<DataType.LIST>
557
      >,
558
    );
559
    return list;
5✔
560
  }
561
  /**
562
   * Reads every generic list helper's elements uniformly: an ARRAY is already flat (its elements
563
   * read straight off array_get, no walking needed), while a PAIR/EMPTY_LIST chain is walked node
564
   * by node the old way. Per Martin: a pair is just a 2-element array, not a distinct concept, so
565
   * these helpers treat both shapes as equally valid "list" inputs rather than only recognizing
566
   * the PAIR/EMPTY_LIST chain - this is what lets pythonToModule (CSE/PVML/py2js) freely encode a
567
   * Python list as DataType.ARRAY without breaking a module that calls list_to_vec/is_list/length/
568
   * accumulate on it (sound, midi, ...), with zero changes needed on the module's side. Throws the
569
   * same "Expected a list, got type X" a caller relying on that message already handles.
570
   */
571
  private readListElements(xs: TypedValue<DataType>): TypedValue<DataType>[] {
572
    if (xs.type === DataType.ARRAY) {
26✔
573
      const array = this.arrayMap.get(xs.value);
13✔
574
      if (!array) {
13!
NEW
575
        throw new InvalidIdentifierError(
×
576
          this.currentCall,
577
          this.currentSource,
578

579
          xs.value,
580
          "array",
581
        );
582
      }
583
      return array.elements;
13✔
584
    }
585
    const result: TypedValue<DataType>[] = [];
13✔
586
    let current: TypedValue<DataType> = xs;
13✔
587
    while (current.type !== DataType.EMPTY_LIST) {
13✔
588
      if (current.type !== DataType.PAIR) {
22✔
589
        throw new InvalidTypeError(
5✔
590
          this.currentCall,
591
          this.currentSource,
592

593
          "a list",
594
          this.getTypeName(DataType.LIST),
595
          this.getTypeName(current.type, current),
596
        );
597
      }
598
      const pair = this.pairMap.get(current.value);
17✔
599
      if (!pair) {
17✔
600
        throw new InvalidIdentifierError(
3✔
601
          this.currentCall,
602
          this.currentSource,
603

604
          current.value,
605
          "pair",
606
        );
607
      }
608
      result.push(pair.head);
14✔
609
      current = pair.tail;
14✔
610
    }
611
    return result;
5✔
612
  }
613
  is_list(xs: TypedValue<DataType.LIST>): Promise<boolean> {
614
    try {
5✔
615
      this.readListElements(xs);
5✔
616
      return Promise.resolve(true);
2✔
617
    } catch {
618
      return Promise.resolve(false);
3✔
619
    }
620
  }
621
  list_to_vec(xs: TypedValue<DataType.LIST>): Promise<TypedValue<DataType>[]> {
622
    try {
13✔
623
      return Promise.resolve(this.readListElements(xs));
13✔
624
    } catch (e) {
625
      return Promise.reject(e);
2✔
626
    }
627
  }
628
  async *accumulate<T extends DataType>(
629
    op: TypedValue<DataType.CLOSURE, T>,
630
    initial: TypedValue<T>,
631
    sequence: TypedValue<DataType.LIST>,
632
    resultType: T,
633
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
634
    let acc = initial;
4✔
635
    for (const element of this.readListElements(sequence)) {
4✔
636
      acc = yield* this.closure_call(op, [acc, element], resultType);
6✔
637
    }
638
    return acc;
3✔
639
  }
640
  length(xs: TypedValue<DataType.LIST>): Promise<number> {
641
    return Promise.resolve(this.readListElements(xs).length);
4✔
642
  }
643
}
644

645
/**
646
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
647
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
648
 * evaluator built around `GenericDataHandler` has those two halves on two
649
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
650
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
651
 * combines them into the one object the registration call needs, so
652
 * combining stays a one-line call at each registration site instead of ~20
653
 * lines of per-evaluator forwarding methods duplicated alongside the
654
 * bookkeeping this class already centralizes.
655
 */
656
export function asInterfacableEvaluator(
27✔
657
  evaluator: IEvaluator,
658
  dataHandler: GenericDataHandler,
659
): IInterfacableEvaluator {
660
  return new Proxy(evaluator, {
82✔
661
    get(target, prop, receiver) {
662
      if (prop in dataHandler) {
528✔
663
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
664
        // and write dataHandler, not the proxy — a plain Reflect.get returns
665
        // the method unbound, so calling it here would set `this` to the
666
        // proxy and (absent a `set` trap) silently write to `evaluator`.
667
        const value = Reflect.get(dataHandler, prop, dataHandler);
527✔
668
        return typeof value === "function" ? value.bind(dataHandler) : value;
527!
669
      }
670
      return Reflect.get(target, prop, receiver);
1✔
671
    },
672
  }) as unknown as IInterfacableEvaluator;
673
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc