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

source-academy / py-slang / 29891610736

22 Jul 2026 04:38AM UTC coverage: 86.249% (+0.07%) from 86.179%
29891610736

Pull #307

github

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

4394 of 5485 branches covered (80.11%)

Branch coverage included in aggregate %.

36 of 47 new or added lines in 5 files covered. (76.6%)

202 existing lines in 10 files now uncovered.

9474 of 10594 relevant lines covered (89.43%)

178117.25 hits per line

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

60.59
/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
 * A conductor `IDataHandler` implementation with no engine-specific logic:
16
 * pairs, arrays, closures and opaques are all just bookkeeping over plain
17
 * Maps keyed by an incrementing id, and the list helpers (`list`/`is_list`/
18
 * `list_to_vec`/`accumulate`/`length`) walk that pair structure generically.
19
 * The only place an engine's own semantics enter the picture is the
20
 * `ExternCallable` passed to `closure_make` (authored by that engine's own
21
 * module-interop layer); `closure_call`/`closure_call_unchecked`/`closure_call_sync` do inspect
22
 * arguments now, but only to run them through `coerceArgsToSignature` above -
23
 * everything else about them still passes straight through unexamined.
24
 *
25
 * Originally written inline in PyCseEvaluatorBase (see PyCseEvaluator.ts);
26
 * extracted so every evaluator that talks to conductor modules (CSE, py2js,
27
 * and eventually WASM/PVML) shares one implementation instead of
28
 * re-deriving the same identifier-table bookkeeping per engine. An evaluator
29
 * holds one instance (`private dataHandler = new GenericDataHandler()`) and
30
 * hands it to both `context.evaluator` (or the engine's equivalent) and
31
 * `conductor.registerPlugin(ModuleLoaderRunnerPlugin, conductor, dataHandler)`.
32
 */
33
export class GenericDataHandler implements IDataHandler {
18✔
34
  hasDataInterface = true as const;
101✔
35
  private pairMap = new Map<
101✔
36
    PairIdentifier,
37
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
38
  >();
39
  private arrayMap = new Map<
101✔
40
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
    ArrayIdentifier<any>,
42
    { type: DataType; elements: TypedValue<DataType>[] }
43
  >();
44
  private closureMap = new Map<
101✔
45
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
    ClosureIdentifier<any>,
47
    {
48
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
      sig: IFunctionSignature<any, any>;
50
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
      func: ExternCallable<any, any>;
52
      dependsOn?: (TypedValue<DataType> | null)[];
53
      isVararg?: boolean;
54
    }
55
  >();
56
  private opaqueMap = new Map<OpaqueIdentifier, { value: unknown; immutable: boolean }>();
101✔
57
  private uniqueId = 0;
101✔
58
  pair_make(
59
    head: TypedValue<DataType>,
60
    tail: TypedValue<DataType>,
61
  ): Promise<TypedValue<DataType.PAIR>> {
62
    this.pairMap.set(this.uniqueId++ as PairIdentifier, { head, tail });
38✔
63
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
38✔
64
  }
65
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
66
    const pair = this.pairMap.get(p.value);
13✔
67
    if (!pair) {
13!
68
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
69
    }
70
    return Promise.resolve(pair.head);
13✔
71
  }
72
  pair_sethead(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
73
    const pair = this.pairMap.get(p.value);
×
74
    if (!pair) {
×
75
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
76
    }
77
    pair.head = tv;
×
78
    return Promise.resolve();
×
79
  }
80
  pair_tail(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
81
    const pair = this.pairMap.get(p.value);
9✔
82
    if (!pair) {
9!
83
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
84
    }
85
    return Promise.resolve(pair.tail);
9✔
86
  }
87
  pair_settail(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
UNCOV
88
    const pair = this.pairMap.get(p.value);
×
89
    if (!pair) {
×
90
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
91
    }
92
    pair.tail = tv;
×
UNCOV
93
    return Promise.resolve();
×
94
  }
95
  pair_assert(
96
    p: TypedValue<DataType.PAIR>,
97
    headType?: DataType,
98
    tailType?: DataType,
99
  ): Promise<void> {
100
    const pair = this.pairMap.get(p.value);
×
101
    if (!pair) {
×
102
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
103
    }
104
    if (headType && pair.head.type !== headType) {
×
UNCOV
105
      throw new Error(`Expected head of type ${headType}, got ${pair.head.type}`);
×
106
    }
107
    if (tailType && pair.tail.type !== tailType) {
×
108
      throw new Error(`Expected tail of type ${tailType}, got ${pair.tail.type}`);
×
109
    }
110
    return Promise.resolve();
×
111
  }
112
  array_make<T extends DataType>(
113
    t: T,
114
    len: number,
115
    init?: TypedValue<NoInfer<T>>,
116
  ): Promise<TypedValue<DataType.ARRAY, NoInfer<T>>> {
117
    const elements = new Array(len).fill(init ?? { type: t, value: undefined }) as TypedValue<
4✔
118
      NoInfer<T>
119
    >[];
120
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
4✔
121
      type: DataType.ARRAY,
122
      value: this.uniqueId++ as ArrayIdentifier<typeof t>,
123
    };
124
    this.arrayMap.set(arrayValue.value, { type: t, elements });
4✔
125
    return Promise.resolve(arrayValue);
4✔
126
  }
127
  /**
128
   * The ARRAY half of the NUMBER/INTEGER module-boundary contract (see `coerceArgsToSignature`'s
129
   * doc comment for why this is safe where the same move on LIST/PAIR isn't): an array records
130
   * its own element type at `array_make` time, so unlike a PAIR chain there's no risk of walking
131
   * into an unrelated structure and corrupting it - only a `DataType.NUMBER` array with at least
132
   * one `DataType.INTEGER` element is touched at all.
133
   *
134
   * Never mutates the array in place - the same `ArrayIdentifier` may still be referenced from
135
   * Python-side code that expects to see its original ints on a later read, so a coerced copy
136
   * gets a fresh identifier and only *that* identifier is handed to the module.
137
   */
138
  private coerceArrayIfNumeric(arg: TypedValue<DataType.ARRAY>): TypedValue<DataType.ARRAY> {
139
    const array = this.arrayMap.get(arg.value);
3✔
140
    if (!array || array.type !== DataType.NUMBER) {
3!
NEW
UNCOV
141
      return arg;
×
142
    }
143
    if (!array.elements.some(el => el.type === DataType.INTEGER)) {
3✔
144
      return arg;
1✔
145
    }
146
    const coercedElements: TypedValue<DataType>[] = array.elements.map(el =>
2✔
147
      el.type === DataType.INTEGER
3✔
148
        ? ({ type: DataType.NUMBER, value: Number(el.value) } as TypedValue<DataType.NUMBER>)
149
        : el,
150
    );
151
    const newId = this.uniqueId++ as ArrayIdentifier<DataType.NUMBER>;
2✔
152
    this.arrayMap.set(newId, { type: DataType.NUMBER, elements: coercedElements });
2✔
153
    return { type: DataType.ARRAY, value: newId };
2✔
154
  }
155
  /**
156
   * Backward-compatibility shim for `DataType.INTEGER`: conductor added it well after
157
   * `DataType.NUMBER` already had a whole ecosystem of modules (sound, midi, repeat, ...) built
158
   * against it, declaring numeric parameters `DataType.NUMBER` and doing raw JS arithmetic on
159
   * `.value` assuming it's always a `number`. `closure_call`/`closure_call_unchecked` never
160
   * validated an argument's actual `DataType` against the closure's declared signature (see this
161
   * class's own doc comment), so once py-slang started tagging a Python `int` as
162
   * `DataType.INTEGER` (a `bigint`) instead of silently widening it to `DataType.NUMBER`, calling
163
   * any such module with a plain integer literal (e.g. `sine_sound(220, 2)`) started throwing
164
   * "Cannot mix BigInt and other types" deep inside the module's own body instead of working like
165
   * it always did.
166
   *
167
   * One-directional by design, per the module-interface contract Martin settled on: values passed
168
   * *into* a module can be floats or ints, and an int is silently downcast to a float (declared
169
   * NUMBER/actual INTEGER narrows to a JS `number`). There is deliberately no reverse direction -
170
   * modules do not get to declare an INTEGER parameter and have py-slang widen a float into a
171
   * `bigint` for them. Keeping modules bigint-free is the point: it's simpler, keeps arithmetic
172
   * fast, and doesn't assume every future language backing a module has bigints at all.
173
   *
174
   * Deliberately does NOT recurse into a `DataType.PAIR`/`LIST` argument to normalize elements
175
   * inside it (an earlier version of this function did, to unbreak a `sum_list`-style module that
176
   * reads raw NUMBER off list elements) - conductor's LIST/PAIR carries no declared element type,
177
   * so `entry(t)`/`left_branch(t)`/etc. on a binary_tree also declare their tree argument
178
   * `DataType.LIST`, indistinguishable at this layer from a *flat list of numbers*. Recursing
179
   * doesn't just fail to help sum_list - it actively walks straight into the tree's own stored
180
   * value and flattens it back to NUMBER before the module ever sees it, silently reintroducing
181
   * exactly the "1.0" bug this was all for. A list-consuming module that wants real integers has
182
   * to become INTEGER-aware itself; there's no safe way to guess it from here. This is a Python §2
183
   * concern specifically - modules that model chapter-2-style structures (binary_tree, and any
184
   * future module built the same way) necessarily live with that ambiguity and get no automatic
185
   * help here.
186
   *
187
   * `DataType.ARRAY` doesn't have that ambiguity - see `coerceArrayIfNumeric` above: unlike
188
   * LIST/PAIR (an untyped chain of pairs), an ARRAY is flat and records its own element type at
189
   * `array_make` time, so there's no tree/list confusion possible - unconditionally safe to
190
   * coerce. A module modelling genuinely flat numeric data (a matrix, a sample buffer, ...)
191
   * should be declaring `DataType.ARRAY`, not `DataType.LIST`, precisely so it gets this for free
192
   * instead of hand-rolling the sum_list-style per-element check.
193
   */
194
  private coerceArgsToSignature(
195
    args: TypedValue<DataType>[],
196
    declaredArgTypes: readonly DataType[],
197
  ): TypedValue<DataType>[] {
198
    return args.map((arg, i) => {
268✔
199
      const declared = declaredArgTypes[i];
209✔
200
      if (declared === DataType.NUMBER && arg.type === DataType.INTEGER) {
209✔
201
        return { type: DataType.NUMBER, value: Number(arg.value) };
82✔
202
      }
203
      if (arg.type === DataType.ARRAY) {
127✔
204
        return this.coerceArrayIfNumeric(arg);
3✔
205
      }
206
      return arg;
124✔
207
    });
208
  }
209
  /**
210
   * Enforces the other half of the same contract on the way out: "numerical values coming from
211
   * modules ... are always floats" - unconditionally, not just for modules that happen to declare
212
   * DataType.NUMBER. A module has no business returning DataType.INTEGER (or an ARRAY of INTEGER)
213
   * at all under this design, but this is the boundary that actually guarantees it rather than
214
   * trusting every module author to remember not to.
215
   */
216
  private coerceReturnValue<T extends DataType>(value: TypedValue<T>): TypedValue<T> {
217
    if ((value.type as DataType) === DataType.INTEGER) {
264!
NEW
218
      return { type: DataType.NUMBER, value: Number(value.value) } as TypedValue<T>;
×
219
    }
220
    if ((value.type as DataType) === DataType.ARRAY) {
264!
NEW
221
      return this.coerceArrayIfNumeric(value as TypedValue<DataType.ARRAY>) as TypedValue<T>;
×
222
    }
223
    return value;
264✔
224
  }
225
  /**
226
   * `closure_call`/`closure_call_unchecked` hand back an `AsyncGenerator` (the module may yield
227
   * control back to the host mid-call), so the return-value coercion can't just wrap a value - it
228
   * has to wrap the generator and coerce whatever it eventually returns, while passing every
229
   * yielded value through untouched.
230
   */
231
  private async *coerceGeneratorReturn<T extends DataType>(
232
    gen: AsyncGenerator<void, TypedValue<T>, undefined>,
233
  ): AsyncGenerator<void, TypedValue<T>, undefined> {
234
    const result = yield* gen;
265✔
235
    return this.coerceReturnValue(result);
263✔
236
  }
237
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
238
    const array = this.arrayMap.get(a.value);
3✔
239
    if (!array) {
3!
UNCOV
240
      throw new Error(`Invalid array identifier: ${a.value}`);
×
241
    }
242
    return Promise.resolve(array.elements.length);
3✔
243
  }
244
  array_get<T extends DataType>(
245
    a: TypedValue<DataType.ARRAY, T>,
246
    idx: number,
247
  ): Promise<TypedValue<NoInfer<T>>>;
248
  array_get(
249
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
250
    idx: number,
251
  ): Promise<TypedValue<DataType>> {
252
    const array = this.arrayMap.get(a.value);
6✔
253

254
    if (!array) {
6!
255
      throw new Error(`Invalid array identifier: ${a.value}`);
×
256
    }
257

258
    if (idx < 0 || idx >= array.elements.length) {
6!
UNCOV
259
      throw new Error(`Index out of bounds: ${idx}`);
×
260
    }
261

262
    const value = array.elements[idx];
6✔
263

264
    if (!value) {
6!
265
      throw new Error(`Missing element at index ${idx}`);
×
266
    }
267

268
    return Promise.resolve(value);
6✔
269
  }
270

271
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
UNCOV
272
    const array = this.arrayMap.get(a.value);
×
UNCOV
273
    if (array === undefined) {
×
UNCOV
274
      throw new Error(`Invalid array identifier: ${a.value}`);
×
275
    }
UNCOV
276
    return Promise.resolve(array.type as NoInfer<T>);
×
277
  }
278
  array_set(
279
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
280
    idx: number,
281
    tv: TypedValue<DataType>,
282
  ): Promise<void>;
283
  array_set<T extends DataType>(
284
    a: TypedValue<DataType.ARRAY, T>,
285
    idx: number,
286
    tv: TypedValue<NoInfer<T>>,
287
  ): Promise<void> {
288
    const array = this.arrayMap.get(a.value) as
3✔
289
      | { type: T; elements: TypedValue<NoInfer<T>>[] }
290
      | undefined;
291

292
    if (!array) {
3!
UNCOV
293
      throw new Error(`Invalid array identifier: ${a.value}`);
×
294
    }
295

296
    if (idx < 0 || idx >= array.elements.length) {
3!
UNCOV
297
      throw new Error(`Index out of bounds: ${idx}`);
×
298
    }
299

300
    array.elements[idx] = tv;
3✔
301

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

531
/**
532
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
533
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
534
 * evaluator built around `GenericDataHandler` has those two halves on two
535
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
536
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
537
 * combines them into the one object the registration call needs, so
538
 * combining stays a one-line call at each registration site instead of ~20
539
 * lines of per-evaluator forwarding methods duplicated alongside the
540
 * bookkeeping this class already centralizes.
541
 */
542
export function asInterfacableEvaluator(
18✔
543
  evaluator: IEvaluator,
544
  dataHandler: GenericDataHandler,
545
): IInterfacableEvaluator {
546
  return new Proxy(evaluator, {
43✔
547
    get(target, prop, receiver) {
548
      if (prop in dataHandler) {
421✔
549
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
550
        // and write dataHandler, not the proxy — a plain Reflect.get returns
551
        // the method unbound, so calling it here would set `this` to the
552
        // proxy and (absent a `set` trap) silently write to `evaluator`.
553
        const value = Reflect.get(dataHandler, prop, dataHandler);
420✔
554
        return typeof value === "function" ? value.bind(dataHandler) : value;
420!
555
      }
556
      return Reflect.get(target, prop, receiver);
1✔
557
    },
558
  }) as unknown as IInterfacableEvaluator;
559
}
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