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

source-academy / py-slang / 29895312120

22 Jul 2026 05:58AM UTC coverage: 86.247% (+0.07%) from 86.179%
29895312120

Pull #307

github

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

4392 of 5483 branches covered (80.1%)

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.3
/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;
102✔
35
  private pairMap = new Map<
102✔
36
    PairIdentifier,
37
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
38
  >();
39
  private arrayMap = new Map<
102✔
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<
102✔
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 }>();
102✔
57
  private uniqueId = 0;
102✔
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<
5✔
118
      NoInfer<T>
119
    >[];
120
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
5✔
121
      type: DataType.ARRAY,
122
      value: this.uniqueId++ as ArrayIdentifier<typeof t>,
123
    };
124
    this.arrayMap.set(arrayValue.value, { type: t, elements });
5✔
125
    return Promise.resolve(arrayValue);
5✔
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);
4✔
140
    if (!array) {
4!
NEW
UNCOV
141
      return arg;
×
142
    }
143
    // Untyped and recursive, per Martin's correction: an array's own `.type` tag (whatever it was
144
    // declared with at array_make time) is not something GenericDataHandler should gate on - an
145
    // array can hold heterogeneous elements (e.g. a Sound-like [closure, number] shape), so every
146
    // element gets the same scalar-level treatment coerceArgsToSignature already gives bare
147
    // arguments, independent of what the array as a whole claims to be.
148
    if (!array.elements.some(el => el.type === DataType.INTEGER)) {
4✔
149
      return arg;
1✔
150
    }
151
    const coercedElements: TypedValue<DataType>[] = array.elements.map(el =>
3✔
152
      el.type === DataType.INTEGER
4✔
153
        ? ({ type: DataType.NUMBER, value: Number(el.value) } as TypedValue<DataType.NUMBER>)
154
        : el,
155
    );
156
    const newId = this.uniqueId++ as ArrayIdentifier<DataType>;
3✔
157
    this.arrayMap.set(newId, { type: array.type, elements: coercedElements });
3✔
158
    return { type: DataType.ARRAY, value: newId };
3✔
159
  }
160
  /**
161
   * Backward-compatibility shim for `DataType.INTEGER`: conductor added it well after
162
   * `DataType.NUMBER` already had a whole ecosystem of modules (sound, midi, repeat, ...) built
163
   * against it, declaring numeric parameters `DataType.NUMBER` and doing raw JS arithmetic on
164
   * `.value` assuming it's always a `number`. `closure_call`/`closure_call_unchecked` never
165
   * validated an argument's actual `DataType` against the closure's declared signature (see this
166
   * class's own doc comment), so once py-slang started tagging a Python `int` as
167
   * `DataType.INTEGER` (a `bigint`) instead of silently widening it to `DataType.NUMBER`, calling
168
   * any such module with a plain integer literal (e.g. `sine_sound(220, 2)`) started throwing
169
   * "Cannot mix BigInt and other types" deep inside the module's own body instead of working like
170
   * it always did.
171
   *
172
   * One-directional by design, per the module-interface contract Martin settled on: values passed
173
   * *into* a module can be floats or ints, and an int is silently downcast to a float (declared
174
   * NUMBER/actual INTEGER narrows to a JS `number`). There is deliberately no reverse direction -
175
   * modules do not get to declare an INTEGER parameter and have py-slang widen a float into a
176
   * `bigint` for them. Keeping modules bigint-free is the point: it's simpler, keeps arithmetic
177
   * fast, and doesn't assume every future language backing a module has bigints at all.
178
   *
179
   * Deliberately does NOT recurse into a `DataType.PAIR`/`LIST` argument to normalize elements
180
   * inside it (an earlier version of this function did, to unbreak a `sum_list`-style module that
181
   * reads raw NUMBER off list elements) - conductor's LIST/PAIR carries no declared element type,
182
   * so `entry(t)`/`left_branch(t)`/etc. on a binary_tree also declare their tree argument
183
   * `DataType.LIST`, indistinguishable at this layer from a *flat list of numbers*. Recursing
184
   * doesn't just fail to help sum_list - it actively walks straight into the tree's own stored
185
   * value and flattens it back to NUMBER before the module ever sees it, silently reintroducing
186
   * exactly the "1.0" bug this was all for. A list-consuming module that wants real integers has
187
   * to become INTEGER-aware itself; there's no safe way to guess it from here. This is a Python §2
188
   * concern specifically - modules that model chapter-2-style structures (binary_tree, and any
189
   * future module built the same way) necessarily live with that ambiguity and get no automatic
190
   * help here.
191
   *
192
   * `DataType.ARRAY` doesn't have that ambiguity - see `coerceArrayIfNumeric` above: unlike
193
   * LIST/PAIR (an untyped chain of pairs whose length and origin can't be told apart from a flat
194
   * numeric list without walking into unrelated structures), an ARRAY is flat, so there's no
195
   * tree/list confusion possible from just walking its elements. Untyped and recursive - it does
196
   * not matter what an array's own declared type is (an array can be heterogeneous, e.g. a
197
   * Sound-like `[closure, number]` shape); every element gets the same scalar coercion
198
   * `coerceArgsToSignature` already applies to a bare argument, nothing more.
199
   */
200
  private coerceArgsToSignature(
201
    args: TypedValue<DataType>[],
202
    declaredArgTypes: readonly DataType[],
203
  ): TypedValue<DataType>[] {
204
    return args.map((arg, i) => {
269✔
205
      const declared = declaredArgTypes[i];
210✔
206
      if (declared === DataType.NUMBER && arg.type === DataType.INTEGER) {
210✔
207
        return { type: DataType.NUMBER, value: Number(arg.value) };
82✔
208
      }
209
      if (arg.type === DataType.ARRAY) {
128✔
210
        return this.coerceArrayIfNumeric(arg);
4✔
211
      }
212
      return arg;
124✔
213
    });
214
  }
215
  /**
216
   * Enforces the other half of the same contract on the way out: "numerical values coming from
217
   * modules ... are always floats" - unconditionally, not just for modules that happen to declare
218
   * DataType.NUMBER. A module has no business returning DataType.INTEGER (or an ARRAY of INTEGER)
219
   * at all under this design, but this is the boundary that actually guarantees it rather than
220
   * trusting every module author to remember not to.
221
   */
222
  private coerceReturnValue<T extends DataType>(value: TypedValue<T>): TypedValue<T> {
223
    if ((value.type as DataType) === DataType.INTEGER) {
265!
NEW
224
      return { type: DataType.NUMBER, value: Number(value.value) } as TypedValue<T>;
×
225
    }
226
    if ((value.type as DataType) === DataType.ARRAY) {
265!
NEW
UNCOV
227
      return this.coerceArrayIfNumeric(value as TypedValue<DataType.ARRAY>) as TypedValue<T>;
×
228
    }
229
    return value;
265✔
230
  }
231
  /**
232
   * `closure_call`/`closure_call_unchecked` hand back an `AsyncGenerator` (the module may yield
233
   * control back to the host mid-call), so the return-value coercion can't just wrap a value - it
234
   * has to wrap the generator and coerce whatever it eventually returns, while passing every
235
   * yielded value through untouched.
236
   */
237
  private async *coerceGeneratorReturn<T extends DataType>(
238
    gen: AsyncGenerator<void, TypedValue<T>, undefined>,
239
  ): AsyncGenerator<void, TypedValue<T>, undefined> {
240
    const result = yield* gen;
266✔
241
    return this.coerceReturnValue(result);
264✔
242
  }
243
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
244
    const array = this.arrayMap.get(a.value);
4✔
245
    if (!array) {
4!
UNCOV
246
      throw new Error(`Invalid array identifier: ${a.value}`);
×
247
    }
248
    return Promise.resolve(array.elements.length);
4✔
249
  }
250
  array_get<T extends DataType>(
251
    a: TypedValue<DataType.ARRAY, T>,
252
    idx: number,
253
  ): Promise<TypedValue<NoInfer<T>>>;
254
  array_get(
255
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
256
    idx: number,
257
  ): Promise<TypedValue<DataType>> {
258
    const array = this.arrayMap.get(a.value);
8✔
259

260
    if (!array) {
8!
UNCOV
261
      throw new Error(`Invalid array identifier: ${a.value}`);
×
262
    }
263

264
    if (idx < 0 || idx >= array.elements.length) {
8!
265
      throw new Error(`Index out of bounds: ${idx}`);
×
266
    }
267

268
    const value = array.elements[idx];
8✔
269

270
    if (!value) {
8!
UNCOV
271
      throw new Error(`Missing element at index ${idx}`);
×
272
    }
273

274
    return Promise.resolve(value);
8✔
275
  }
276

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

298
    if (!array) {
4!
UNCOV
299
      throw new Error(`Invalid array identifier: ${a.value}`);
×
300
    }
301

302
    if (idx < 0 || idx >= array.elements.length) {
4!
303
      throw new Error(`Index out of bounds: ${idx}`);
×
304
    }
305

306
    array.elements[idx] = tv;
4✔
307

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

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