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

source-academy / py-slang / 29891246868

22 Jul 2026 04:30AM UTC coverage: 86.112% (-0.07%) from 86.179%
29891246868

Pull #307

github

web-flow
Merge bf9c42273 into bda1a2574
Pull Request #307: Cross Python ints as DataType.INTEGER, enforce floats-only module boundary

4384 of 5477 branches covered (80.04%)

Branch coverage included in aggregate %.

18 of 64 new or added lines in 5 files covered. (28.13%)

30 existing lines in 1 file now uncovered.

9443 of 10580 relevant lines covered (89.25%)

178352.89 hits per line

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

49.39
/src/conductor/GenericDataHandler.ts
1
import type { IEvaluator, IInterfacableEvaluator } from "@sourceacademy/conductor/runner";
2
import {
18✔
3
  ArrayIdentifier,
4
  ClosureIdentifier,
5
  DataType,
6
  ExternCallable,
7
  IDataHandler,
8
  IFunctionSignature,
9
  OpaqueIdentifier,
10
  PairIdentifier,
11
  TypedValue,
12
} from "@sourceacademy/conductor/types";
13

14
/**
15
 * Backward-compatibility shim for `DataType.INTEGER`: conductor added it well after
16
 * `DataType.NUMBER` already had a whole ecosystem of modules (sound, midi, repeat, ...) built
17
 * against it, declaring numeric parameters `DataType.NUMBER` and doing raw JS arithmetic on
18
 * `.value` assuming it's always a `number`. `closure_call`/`closure_call_unchecked` never
19
 * validated an argument's actual `DataType` against the closure's declared signature (see this
20
 * class's own doc comment), so once py-slang started tagging a Python `int` as
21
 * `DataType.INTEGER` (a `bigint`) instead of silently widening it to `DataType.NUMBER`, calling
22
 * any such module with a plain integer literal (e.g. `sine_sound(220, 2)`) started throwing
23
 * "Cannot mix BigInt and other types" deep inside the module's own body instead of working like
24
 * it always did.
25
 *
26
 * One-directional by design, per the module-interface contract Martin settled on: values passed
27
 * *into* a module can be floats or ints, and an int is silently downcast to a float (declared
28
 * NUMBER/actual INTEGER narrows to a JS `number`). There is deliberately no reverse direction -
29
 * modules do not get to declare an INTEGER parameter and have py-slang widen a float into a
30
 * `bigint` for them. Keeping modules bigint-free is the point: it's simpler, keeps arithmetic
31
 * fast, and doesn't assume every future language backing a module has bigints at all.
32
 *
33
 * Deliberately does NOT recurse into a `DataType.PAIR`/`LIST` argument to normalize elements
34
 * inside it (an earlier version of this function did, to unbreak a `sum_list`-style module that
35
 * reads raw NUMBER off list elements) - conductor's LIST/PAIR carries no declared element type,
36
 * so `entry(t)`/`left_branch(t)`/etc. on a binary_tree also declare their tree argument
37
 * `DataType.LIST`, indistinguishable at this layer from a *flat list of numbers*. Recursing
38
 * doesn't just fail to help sum_list - it actively walks straight into the tree's own stored
39
 * value and flattens it back to NUMBER before the module ever sees it, silently reintroducing
40
 * exactly the "1.0" bug this was all for. A list-consuming module that wants real integers has
41
 * to become INTEGER-aware itself; there's no safe way to guess it from here.
42
 */
43
function coerceArgsToSignature(
44
  args: TypedValue<DataType>[],
45
  declaredArgTypes: readonly DataType[],
46
): TypedValue<DataType>[] {
47
  return args.map((arg, i) => {
265✔
48
    const declared = declaredArgTypes[i];
206✔
49
    if (declared === DataType.NUMBER && arg.type === DataType.INTEGER) {
206✔
50
      return { type: DataType.NUMBER, value: Number(arg.value) };
82✔
51
    }
52
    return arg;
124✔
53
  });
54
}
55

56
/**
57
 * Enforces the other half of the same contract on the way out: "numerical values coming from
58
 * modules ... are always floats" - unconditionally, not just for modules that happen to declare
59
 * DataType.NUMBER. A module has no business returning DataType.INTEGER at all under this design,
60
 * but this is the boundary that actually guarantees it rather than trusting every module author
61
 * to remember not to.
62
 */
63
function coerceReturnValue<T extends DataType>(value: TypedValue<T>): TypedValue<T> {
64
  if ((value.type as DataType) === DataType.INTEGER) {
261!
UNCOV
65
    return { type: DataType.NUMBER, value: Number(value.value) } as TypedValue<T>;
×
66
  }
67
  return value;
261✔
68
}
69

70
/**
71
 * `closure_call`/`closure_call_unchecked` hand back an `AsyncGenerator` (the module may yield
72
 * control back to the host mid-call), so the return-value coercion can't just wrap a value - it
73
 * has to wrap the generator and coerce whatever it eventually returns, while passing every
74
 * yielded value through untouched.
75
 */
76
async function* coerceGeneratorReturn<T extends DataType>(
77
  gen: AsyncGenerator<void, TypedValue<T>, undefined>,
78
): AsyncGenerator<void, TypedValue<T>, undefined> {
79
  const result = yield* gen;
262✔
80
  return coerceReturnValue(result);
260✔
81
}
82

83
/**
84
 * A conductor `IDataHandler` implementation with no engine-specific logic:
85
 * pairs, arrays, closures and opaques are all just bookkeeping over plain
86
 * Maps keyed by an incrementing id, and the list helpers (`list`/`is_list`/
87
 * `list_to_vec`/`accumulate`/`length`) walk that pair structure generically.
88
 * The only place an engine's own semantics enter the picture is the
89
 * `ExternCallable` passed to `closure_make` (authored by that engine's own
90
 * module-interop layer); `closure_call`/`closure_call_unchecked`/`closure_call_sync` do inspect
91
 * arguments now, but only to run them through `coerceArgsToSignature` above -
92
 * everything else about them still passes straight through unexamined.
93
 *
94
 * Originally written inline in PyCseEvaluatorBase (see PyCseEvaluator.ts);
95
 * extracted so every evaluator that talks to conductor modules (CSE, py2js,
96
 * and eventually WASM/PVML) shares one implementation instead of
97
 * re-deriving the same identifier-table bookkeeping per engine. An evaluator
98
 * holds one instance (`private dataHandler = new GenericDataHandler()`) and
99
 * hands it to both `context.evaluator` (or the engine's equivalent) and
100
 * `conductor.registerPlugin(ModuleLoaderRunnerPlugin, conductor, dataHandler)`.
101
 */
102
export class GenericDataHandler implements IDataHandler {
18✔
103
  hasDataInterface = true as const;
98✔
104
  private pairMap = new Map<
98✔
105
    PairIdentifier,
106
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
107
  >();
108
  private arrayMap = new Map<
98✔
109
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
    ArrayIdentifier<any>,
111
    { type: DataType; elements: TypedValue<DataType>[] }
112
  >();
113
  private closureMap = new Map<
98✔
114
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
    ClosureIdentifier<any>,
116
    {
117
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
      sig: IFunctionSignature<any, any>;
119
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
      func: ExternCallable<any, any>;
121
      dependsOn?: (TypedValue<DataType> | null)[];
122
      isVararg?: boolean;
123
    }
124
  >();
125
  private opaqueMap = new Map<OpaqueIdentifier, { value: unknown; immutable: boolean }>();
98✔
126
  private uniqueId = 0;
98✔
127
  pair_make(
128
    head: TypedValue<DataType>,
129
    tail: TypedValue<DataType>,
130
  ): Promise<TypedValue<DataType.PAIR>> {
131
    this.pairMap.set(this.uniqueId++ as PairIdentifier, { head, tail });
38✔
132
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
38✔
133
  }
134
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
135
    const pair = this.pairMap.get(p.value);
13✔
136
    if (!pair) {
13!
NEW
137
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
138
    }
139
    return Promise.resolve(pair.head);
13✔
140
  }
141
  pair_sethead(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
NEW
142
    const pair = this.pairMap.get(p.value);
×
NEW
143
    if (!pair) {
×
NEW
144
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
145
    }
NEW
146
    pair.head = tv;
×
NEW
147
    return Promise.resolve();
×
148
  }
149
  pair_tail(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
150
    const pair = this.pairMap.get(p.value);
9✔
151
    if (!pair) {
9!
NEW
152
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
153
    }
154
    return Promise.resolve(pair.tail);
9✔
155
  }
156
  pair_settail(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
NEW
157
    const pair = this.pairMap.get(p.value);
×
NEW
158
    if (!pair) {
×
NEW
159
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
160
    }
NEW
161
    pair.tail = tv;
×
NEW
162
    return Promise.resolve();
×
163
  }
164
  pair_assert(
165
    p: TypedValue<DataType.PAIR>,
166
    headType?: DataType,
167
    tailType?: DataType,
168
  ): Promise<void> {
NEW
169
    const pair = this.pairMap.get(p.value);
×
NEW
170
    if (!pair) {
×
NEW
171
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
172
    }
NEW
173
    if (headType && pair.head.type !== headType) {
×
NEW
174
      throw new Error(`Expected head of type ${headType}, got ${pair.head.type}`);
×
175
    }
NEW
176
    if (tailType && pair.tail.type !== tailType) {
×
NEW
177
      throw new Error(`Expected tail of type ${tailType}, got ${pair.tail.type}`);
×
178
    }
NEW
179
    return Promise.resolve();
×
180
  }
181
  array_make<T extends DataType>(
182
    t: T,
183
    len: number,
184
    init?: TypedValue<NoInfer<T>>,
185
  ): Promise<TypedValue<DataType.ARRAY, NoInfer<T>>> {
186
    const elements = new Array(len).fill(init ?? { type: t, value: undefined }) as TypedValue<
1✔
187
      NoInfer<T>
188
    >[];
189
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
1✔
190
      type: DataType.ARRAY,
191
      value: this.uniqueId++ as ArrayIdentifier<typeof t>,
192
    };
193
    this.arrayMap.set(arrayValue.value, { type: t, elements });
1✔
194
    return Promise.resolve(arrayValue);
1✔
195
  }
196
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
NEW
197
    const array = this.arrayMap.get(a.value);
×
NEW
198
    if (!array) {
×
NEW
199
      throw new Error(`Invalid array identifier: ${a.value}`);
×
200
    }
NEW
201
    return Promise.resolve(array.elements.length);
×
202
  }
203
  array_get<T extends DataType>(
204
    a: TypedValue<DataType.ARRAY, T>,
205
    idx: number,
206
  ): Promise<TypedValue<NoInfer<T>>>;
207
  array_get(
208
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
209
    idx: number,
210
  ): Promise<TypedValue<DataType>> {
NEW
211
    const array = this.arrayMap.get(a.value);
×
212

NEW
213
    if (!array) {
×
NEW
214
      throw new Error(`Invalid array identifier: ${a.value}`);
×
215
    }
216

NEW
217
    if (idx < 0 || idx >= array.elements.length) {
×
NEW
218
      throw new Error(`Index out of bounds: ${idx}`);
×
219
    }
220

NEW
221
    const value = array.elements[idx];
×
222

NEW
223
    if (!value) {
×
NEW
224
      throw new Error(`Missing element at index ${idx}`);
×
225
    }
226

NEW
227
    return Promise.resolve(value);
×
228
  }
229

230
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
NEW
231
    const array = this.arrayMap.get(a.value);
×
NEW
232
    if (array === undefined) {
×
NEW
233
      throw new Error(`Invalid array identifier: ${a.value}`);
×
234
    }
NEW
235
    return Promise.resolve(array.type as NoInfer<T>);
×
236
  }
237
  array_set(
238
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
239
    idx: number,
240
    tv: TypedValue<DataType>,
241
  ): Promise<void>;
242
  array_set<T extends DataType>(
243
    a: TypedValue<DataType.ARRAY, T>,
244
    idx: number,
245
    tv: TypedValue<NoInfer<T>>,
246
  ): Promise<void> {
UNCOV
247
    const array = this.arrayMap.get(a.value) as
×
248
      | { type: T; elements: TypedValue<NoInfer<T>>[] }
249
      | undefined;
250

UNCOV
251
    if (!array) {
×
252
      throw new Error(`Invalid array identifier: ${a.value}`);
×
253
    }
254

255
    if (idx < 0 || idx >= array.elements.length) {
×
UNCOV
256
      throw new Error(`Index out of bounds: ${idx}`);
×
257
    }
258

259
    array.elements[idx] = tv;
×
260

UNCOV
261
    return Promise.resolve();
×
262
  }
263
  array_assert<T extends DataType>(
264
    a: TypedValue<DataType.ARRAY>,
265
    type?: T,
266
    length?: number,
267
  ): Promise<void> {
268
    const array = this.arrayMap.get(a.value);
×
UNCOV
269
    if (!array) {
×
UNCOV
270
      throw new Error(`Invalid array identifier: ${a.value}`);
×
271
    }
272
    if (type !== undefined && array.type !== type) {
×
273
      throw new Error(`Expected array of type ${type}, got ${array.type}`);
×
274
    }
UNCOV
275
    if (length !== undefined && array.elements.length !== length) {
×
276
      throw new Error(`Expected array of length ${length}, got ${array.elements.length}`);
×
277
    }
UNCOV
278
    return Promise.resolve();
×
279
  }
280
  closure_make<const Arg extends readonly DataType[], const Ret extends DataType>(
281
    sig: IFunctionSignature<Arg, Ret>,
282
    func: ExternCallable<Arg, Ret>,
283
    dependsOn?: (TypedValue<DataType> | null)[],
284
  ): Promise<TypedValue<DataType.CLOSURE, Ret>> {
285
    const closureValue: TypedValue<DataType.CLOSURE, Ret> = {
403✔
286
      type: DataType.CLOSURE,
287
      value: this.uniqueId++ as ClosureIdentifier<Ret>,
288
    };
289
    this.closureMap.set(closureValue.value, { sig, func, dependsOn });
403✔
290
    return Promise.resolve(closureValue);
403✔
291
  }
292
  closure_is_vararg(c: TypedValue<DataType.CLOSURE>): Promise<boolean> {
293
    return Promise.resolve(this.closureMap.get(c.value)?.isVararg ?? false);
×
294
  }
295
  closure_arity(c: TypedValue<DataType.CLOSURE>): Promise<number> {
296
    return Promise.resolve(this.closureMap.get(c.value)?.sig.args.length ?? 0);
12!
297
  }
298
  closure_call<T extends DataType>(
299
    c: TypedValue<DataType.CLOSURE, T>,
300
    args: TypedValue<DataType>[],
301
    returnType: T,
302
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
UNCOV
303
    const closure = this.closureMap.get(c.value);
×
UNCOV
304
    if (closure === undefined) {
×
UNCOV
305
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
306
    }
UNCOV
307
    if (closure.sig.returnType !== returnType) {
×
UNCOV
308
      throw new Error(`Expected return type ${returnType}, got ${closure.sig.returnType}`);
×
309
    }
310
    const gen = closure.func(...coerceArgsToSignature(args, closure.sig.args)) as AsyncGenerator<
×
311
      void,
312
      TypedValue<NoInfer<T>>,
313
      undefined
314
    >;
UNCOV
315
    return coerceGeneratorReturn(gen);
×
316
  }
317
  closure_call_unchecked<T extends DataType>(
318
    c: TypedValue<DataType.CLOSURE, T>,
319
    args: TypedValue<DataType>[],
320
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
321
    const closure = this.closureMap.get(c.value);
262✔
322
    if (closure === undefined) {
262!
UNCOV
323
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
324
    }
325
    const coercedArgs = coerceArgsToSignature(args, closure.sig.args);
262✔
326
    const gen = closure.func(...coercedArgs) as AsyncGenerator<
262✔
327
      void,
328
      TypedValue<NoInfer<T>>,
329
      undefined
330
    >;
331
    return coerceGeneratorReturn(gen);
262✔
332
  }
333
  /**
334
   * Fast path for a closure that provably never needs to leave the current
335
   * synchronous call - e.g. a scalar-in/scalar-out wave function sampled
336
   * 44100x/sec by the sound module. `func` (an `ExternCallable`, normally
337
   * only callable as an AsyncGenerator per conductor's own contract) may
338
   * additionally carry a `.sync` escape hatch: a plain function computing
339
   * the exact same result with no Promise/generator indirection at all. An
340
   * engine's module-interop layer sets `.sync` only when it can prove the
341
   * closure never needs a real host round-trip (see py2js's moduleInterop.ts
342
   * pyClosureFunc); a closure with no such proof (every CSE-machine closure
343
   * today, or a py2js closure that touches something asyncOnly) simply never
344
   * gets one.
345
   *
346
   * Returns `undefined` when the closure has no sync form - the signal for
347
   * "fall back to closure_call_unchecked" - which is unambiguous because a
348
   * TypedValue always wraps a real `{ type, value }` pair, even for
349
   * DataType.VOID; the bare JS value `undefined` is never a legitimate
350
   * closure result.
351
   */
352
  closure_call_sync<T extends DataType>(
353
    c: TypedValue<DataType.CLOSURE, T>,
354
    args: TypedValue<DataType>[],
355
  ): TypedValue<NoInfer<T>> | undefined {
356
    const closure = this.closureMap.get(c.value);
3✔
357
    const func = closure?.func as
3✔
358
      | (ExternCallable<DataType[], T> & {
359
          sync?: (...a: TypedValue<DataType>[]) => TypedValue<DataType> | undefined;
360
        })
361
      | undefined;
362
    const coercedArgs = closure ? coerceArgsToSignature(args, closure.sig.args) : args;
3!
363
    const result = func?.sync?.(...coercedArgs) as TypedValue<NoInfer<T>> | undefined;
3✔
364
    return result === undefined ? undefined : coerceReturnValue(result);
2✔
365
  }
366
  closure_arity_assert(c: TypedValue<DataType.CLOSURE>, arity: number): Promise<void> {
UNCOV
367
    const closure = this.closureMap.get(c.value);
×
UNCOV
368
    if (!closure) {
×
UNCOV
369
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
370
    }
UNCOV
371
    if (closure.sig.args.length !== arity && !closure.isVararg) {
×
UNCOV
372
      throw new Error(`Expected closure of arity ${arity}, got ${closure.sig.args.length}`);
×
373
    }
UNCOV
374
    return Promise.resolve();
×
375
  }
376
  opaque_make(v: unknown, immutable?: boolean): Promise<TypedValue<DataType.OPAQUE>> {
377
    const opaqueValue: TypedValue<DataType.OPAQUE> = {
3✔
378
      type: DataType.OPAQUE,
379
      value: this.uniqueId++ as OpaqueIdentifier,
380
    };
381
    this.opaqueMap.set(opaqueValue.value, { value: v, immutable: immutable || false });
3✔
382
    return Promise.resolve(opaqueValue);
3✔
383
  }
384
  opaque_get(o: TypedValue<DataType.OPAQUE>): Promise<unknown> {
385
    const opaque = this.opaqueMap.get(o.value);
1✔
386
    if (!opaque) {
1!
UNCOV
387
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
388
    }
389
    return Promise.resolve(opaque.value);
1✔
390
  }
391
  opaque_update(o: TypedValue<DataType.OPAQUE>, v: unknown): Promise<void> {
UNCOV
392
    const opaque = this.opaqueMap.get(o.value);
×
UNCOV
393
    if (!opaque) {
×
UNCOV
394
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
395
    }
NEW
396
    if (opaque.immutable) {
×
UNCOV
397
      throw new Error(`Cannot update immutable opaque value with identifier: ${o.value}`);
×
398
    }
UNCOV
399
    opaque.value = v;
×
UNCOV
400
    return Promise.resolve();
×
401
  }
402
  tie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
NEW
403
    throw new Error("Method not implemented.");
×
404
  }
405
  untie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
406
    throw new Error("Method not implemented.");
×
407
  }
408
  async list(...elements: TypedValue<DataType>[]): Promise<TypedValue<DataType.LIST>> {
409
    const list = await elements.reduceRight(
5✔
410
      async (acc, el) => {
411
        return this.pair_make(el, await acc);
13✔
412
      },
413
      Promise.resolve({ type: DataType.EMPTY_LIST, value: null }) as Promise<
414
        TypedValue<DataType.LIST>
415
      >,
416
    );
417
    return list;
5✔
418
  }
419
  is_list(xs: TypedValue<DataType.LIST>): Promise<boolean> {
420
    let current: TypedValue<DataType> = xs;
4✔
421
    while (current.type !== DataType.EMPTY_LIST) {
4✔
422
      if (current.type !== DataType.PAIR) {
7✔
423
        return Promise.resolve(false);
2✔
424
      }
425
      const pair = this.pairMap.get(current.value);
5✔
426
      if (pair === undefined) {
5✔
427
        return Promise.resolve(false);
1✔
428
      }
429
      current = pair.tail;
4✔
430
    }
431
    return Promise.resolve(true);
1✔
432
  }
433
  list_to_vec(xs: TypedValue<DataType.LIST>): Promise<TypedValue<DataType>[]> {
434
    return new Promise((resolve, reject) => {
10✔
435
      const result: TypedValue<DataType>[] = [];
10✔
436
      let current: TypedValue<DataType> = xs;
10✔
437
      while (current.type !== DataType.EMPTY_LIST) {
10✔
438
        if (current.type !== DataType.PAIR) {
17✔
439
          reject(new Error(`Expected a list, got type ${current.type}`));
1✔
440
          return;
1✔
441
        }
442
        const pair = this.pairMap.get(current.value);
16✔
443
        if (!pair) {
16✔
444
          reject(new Error(`Invalid pair identifier: ${current.value}`));
1✔
445
          return;
1✔
446
        }
447
        result.push(pair.head);
15✔
448
        current = pair.tail;
15✔
449
      }
450
      resolve(result);
8✔
451
    });
452
  }
453
  async *accumulate<T extends Exclude<DataType, DataType.VOID>>(
454
    op: TypedValue<DataType.CLOSURE, T>,
455
    initial: TypedValue<T>,
456
    sequence: TypedValue<DataType.LIST>,
457
    _resultType: T,
458
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
459
    let acc = initial;
3✔
460
    let current: TypedValue<DataType> = sequence;
3✔
461
    while (current.type !== DataType.EMPTY_LIST) {
3✔
462
      if (current.type !== DataType.PAIR) {
4✔
463
        throw new Error(`Expected a list, got type ${current.type}`);
1✔
464
      }
465
      const pair = this.pairMap.get(current.value);
3✔
466
      if (!pair) {
3!
UNCOV
467
        throw new Error(`Invalid pair identifier: ${current.value}`);
×
468
      }
469
      acc = yield* this.closure_call_unchecked(op, [acc, pair.head]);
3✔
470
      current = pair.tail;
3✔
471
    }
472
    return acc;
2✔
473
  }
474
  length(xs: TypedValue<DataType.LIST>): Promise<number> {
475
    let length = 0;
3✔
476
    let current: TypedValue<DataType> = xs;
3✔
477
    while (current.type !== DataType.EMPTY_LIST) {
3✔
478
      if (current.type !== DataType.PAIR) {
6✔
479
        throw new Error(`Expected a list, got type ${current.type}`);
1✔
480
      }
481
      const pair = this.pairMap.get(current.value);
5✔
482
      if (!pair) {
5✔
483
        throw new Error(`Invalid pair identifier: ${current.value}`);
1✔
484
      }
485
      length++;
4✔
486
      current = pair.tail;
4✔
487
    }
488
    return Promise.resolve(length);
1✔
489
  }
490
}
491

492
/**
493
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
494
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
495
 * evaluator built around `GenericDataHandler` has those two halves on two
496
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
497
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
498
 * combines them into the one object the registration call needs, so
499
 * combining stays a one-line call at each registration site instead of ~20
500
 * lines of per-evaluator forwarding methods duplicated alongside the
501
 * bookkeeping this class already centralizes.
502
 */
503
export function asInterfacableEvaluator(
18✔
504
  evaluator: IEvaluator,
505
  dataHandler: GenericDataHandler,
506
): IInterfacableEvaluator {
507
  return new Proxy(evaluator, {
43✔
508
    get(target, prop, receiver) {
509
      if (prop in dataHandler) {
421✔
510
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
511
        // and write dataHandler, not the proxy — a plain Reflect.get returns
512
        // the method unbound, so calling it here would set `this` to the
513
        // proxy and (absent a `set` trap) silently write to `evaluator`.
514
        const value = Reflect.get(dataHandler, prop, dataHandler);
420✔
515
        return typeof value === "function" ? value.bind(dataHandler) : value;
420!
516
      }
517
      return Reflect.get(target, prop, receiver);
1✔
518
    },
519
  }) as unknown as IInterfacableEvaluator;
520
}
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