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

source-academy / py-slang / 30936215473

04 Aug 2026 05:56PM UTC coverage: 85.931% (-0.1%) from 86.027%
30936215473

Pull #398

github

web-flow
Merge 47f97d19c into 64acbb674
Pull Request #398: Improve Error Handling for module functions

4730 of 5945 branches covered (79.56%)

Branch coverage included in aggregate %.

164 of 233 new or added lines in 26 files covered. (70.39%)

2 existing lines in 2 files now uncovered.

10552 of 11839 relevant lines covered (89.13%)

165961.16 hits per line

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

59.8
/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;
185✔
53
  private pairMap = new Map<
185✔
54
    PairIdentifier,
55
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
56
  >();
57
  private arrayMap = new Map<
185✔
58
    ArrayIdentifier<DataType>,
59
    { type: DataType; elements: TypedValue<DataType>[] }
60
  >();
61
  private closureMap = new Map<
185✔
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 }>();
185✔
74
  private uniqueId = 0;
185✔
75

76
  private currentCall: ExprNS.Call | undefined = undefined;
185✔
77
  private currentSource: string | undefined = undefined;
185✔
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
  constructor(public readonly variant: number) {}
185✔
119

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

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

128
  /**
129
   * Records a generated evaluator's call site.  Unlike the CSE evaluator,
130
   * compiled evaluators cannot retain the AST object at runtime, but the
131
   * diagnostic code only needs its source span.
132
   */
133
  setCurrentCallLocation(start: number, end: number): void {
134
    this.currentCall = {
370✔
135
      startToken: { indexInSource: start },
136
      endToken: { indexInSource: end, lexeme: "" },
137
    } as ExprNS.Call;
138
  }
139

140
  setCurrentSource(source: string | undefined): void {
141
    this.currentSource = source;
209✔
142
    // A call node is meaningful only within the source it came from.  In
143
    // persistent evaluators, retaining it across chunks/files would point a
144
    // later module-loading error at an unrelated earlier program.
145
    this.currentCall = undefined;
209✔
146
  }
147

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

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

275
    if (!array) {
10!
NEW
276
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
277
    }
278

279
    if (idx < 0 || idx >= array.elements.length) {
10✔
280
      throw new InvalidIndexError(this.currentCall, this.currentSource, idx, array.elements.length);
3✔
281
    }
282

283
    const value = array.elements[idx];
7✔
284

285
    return Promise.resolve(value);
7✔
286
  }
287

288
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
289
    const array = this.arrayMap.get(a.value);
×
290
    if (array === undefined) {
×
NEW
291
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
292
    }
293
    return Promise.resolve(array.type as NoInfer<T>);
×
294
  }
295
  array_set(
296
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
297
    idx: number,
298
    tv: TypedValue<DataType>,
299
  ): Promise<void>;
300
  array_set<T extends DataType>(
301
    a: TypedValue<DataType.ARRAY, T>,
302
    idx: number,
303
    tv: TypedValue<NoInfer<T>>,
304
  ): Promise<void> {
305
    const array = this.arrayMap.get(a.value);
32✔
306

307
    if (!array) {
32!
NEW
308
      throw new InvalidIdentifierError(this.currentCall, this.currentSource, a.value, "array");
×
309
    }
310

311
    if (idx < 0 || idx >= array.elements.length) {
32!
NEW
312
      throw new InvalidIndexError(
×
313
        this.currentCall,
314
        this.currentSource,
315
        idx,
316
        array.elements.length,
317
        true,
318
      );
319
    }
320

321
    if (tv.type !== array.type && array.type !== DataType.ANY) {
32!
NEW
322
      throw new InvalidTypeError(
×
323
        this.currentCall,
324
        this.currentSource,
325
        `element at index ${idx}'s`,
326
        this.getTypeName(array.type),
327
        this.getTypeName(tv.type, tv),
328
      );
329
    }
330

331
    array.elements[idx] = tv;
32✔
332

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

563
          xs.value,
564
          "array",
565
        );
566
      }
567
      return array.elements;
13✔
568
    }
569
    const result: TypedValue<DataType>[] = [];
13✔
570
    let current: TypedValue<DataType> = xs;
13✔
571
    while (current.type !== DataType.EMPTY_LIST) {
13✔
572
      if (current.type !== DataType.PAIR) {
22✔
573
        throw new InvalidTypeError(
5✔
574
          this.currentCall,
575
          this.currentSource,
576

577
          "a list",
578
          this.getTypeName(DataType.LIST),
579
          this.getTypeName(current.type, current),
580
        );
581
      }
582
      const pair = this.pairMap.get(current.value);
17✔
583
      if (!pair) {
17✔
584
        throw new InvalidIdentifierError(
3✔
585
          this.currentCall,
586
          this.currentSource,
587

588
          current.value,
589
          "pair",
590
        );
591
      }
592
      result.push(pair.head);
14✔
593
      current = pair.tail;
14✔
594
    }
595
    return result;
5✔
596
  }
597
  is_list(xs: TypedValue<DataType.LIST>): Promise<boolean> {
598
    try {
5✔
599
      this.readListElements(xs);
5✔
600
      return Promise.resolve(true);
2✔
601
    } catch {
602
      return Promise.resolve(false);
3✔
603
    }
604
  }
605
  list_to_vec(xs: TypedValue<DataType.LIST>): Promise<TypedValue<DataType>[]> {
606
    try {
13✔
607
      return Promise.resolve(this.readListElements(xs));
13✔
608
    } catch (e) {
609
      return Promise.reject(e);
2✔
610
    }
611
  }
612
  async *accumulate<T extends DataType>(
613
    op: TypedValue<DataType.CLOSURE, T>,
614
    initial: TypedValue<T>,
615
    sequence: TypedValue<DataType.LIST>,
616
    resultType: T,
617
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
618
    let acc = initial;
4✔
619
    for (const element of this.readListElements(sequence)) {
4✔
620
      acc = yield* this.closure_call(op, [acc, element], resultType);
6✔
621
    }
622
    return acc;
3✔
623
  }
624
  length(xs: TypedValue<DataType.LIST>): Promise<number> {
625
    return Promise.resolve(this.readListElements(xs).length);
4✔
626
  }
627
}
628

629
/**
630
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
631
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
632
 * evaluator built around `GenericDataHandler` has those two halves on two
633
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
634
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
635
 * combines them into the one object the registration call needs, so
636
 * combining stays a one-line call at each registration site instead of ~20
637
 * lines of per-evaluator forwarding methods duplicated alongside the
638
 * bookkeeping this class already centralizes.
639
 */
640
export function asInterfacableEvaluator(
27✔
641
  evaluator: IEvaluator,
642
  dataHandler: GenericDataHandler,
643
): IInterfacableEvaluator {
644
  return new Proxy(evaluator, {
82✔
645
    get(target, prop, receiver) {
646
      if (prop in dataHandler) {
528✔
647
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
648
        // and write dataHandler, not the proxy — a plain Reflect.get returns
649
        // the method unbound, so calling it here would set `this` to the
650
        // proxy and (absent a `set` trap) silently write to `evaluator`.
651
        const value = Reflect.get(dataHandler, prop, dataHandler);
527✔
652
        return typeof value === "function" ? value.bind(dataHandler) : value;
527!
653
      }
654
      return Reflect.get(target, prop, receiver);
1✔
655
    },
656
  }) as unknown as IInterfacableEvaluator;
657
}
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