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

source-academy / py-slang / 29896344061

22 Jul 2026 06:18AM UTC coverage: 86.153% (-0.03%) from 86.179%
29896344061

Pull #307

github

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

4388 of 5481 branches covered (80.06%)

Branch coverage included in aggregate %.

25 of 33 new or added lines in 5 files covered. (75.76%)

207 existing lines in 10 files now uncovered.

9449 of 10580 relevant lines covered (89.31%)

178352.9 hits per line

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

52.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;
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 });
39✔
63
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
39✔
64
  }
65
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
66
    const pair = this.pairMap.get(p.value);
14✔
67
    if (!pair) {
14!
68
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
69
    }
70
    return Promise.resolve(pair.head);
14✔
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<
1✔
118
      NoInfer<T>
119
    >[];
120
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
1✔
121
      type: DataType.ARRAY,
122
      value: this.uniqueId++ as ArrayIdentifier<typeof t>,
123
    };
124
    this.arrayMap.set(arrayValue.value, { type: t, elements });
1✔
125
    return Promise.resolve(arrayValue);
1✔
126
  }
127
  /**
128
   * Backward-compatibility shim for `DataType.INTEGER`: conductor added it well after
129
   * `DataType.NUMBER` already had a whole ecosystem of modules (sound, midi, repeat, ...) built
130
   * against it, declaring numeric parameters `DataType.NUMBER` and doing raw JS arithmetic on
131
   * `.value` assuming it's always a `number`. `closure_call`/`closure_call_unchecked` never
132
   * validated an argument's actual `DataType` against the closure's declared signature (see this
133
   * class's own doc comment), so once py-slang started tagging a Python `int` as
134
   * `DataType.INTEGER` (a `bigint`) instead of silently widening it to `DataType.NUMBER`, calling
135
   * any such module with a plain integer literal (e.g. `sine_sound(220, 2)`) started throwing
136
   * "Cannot mix BigInt and other types" deep inside the module's own body instead of working like
137
   * it always did.
138
   *
139
   * Martin's rule, verbatim: "numerical values coming from modules (including from functions that
140
   * are passed to/from modules) are always floats. Values passed to modules (including to
141
   * functions that are passed to/from modules) can be floats and ints; ints are silently converted
142
   * to floats." That rule is about *numerical* values specifically - a module's own arithmetic
143
   * (`DataType.NUMBER`-declared params/returns). It is gated on the *declared* type, not the
144
   * argument's or return value's actual runtime tag, precisely so it leaves alone every other
145
   * declared type (`OPAQUE`, `PAIR`, `LIST`, ...) that merely stores and returns a value verbatim
146
   * without doing any arithmetic on it - see `binary_tree`: `make_tree`'s `value` param is declared
147
   * `DataType.OPAQUE` (accepts literally anything, including a Python int that stays tagged
148
   * `DataType.INTEGER` the whole time it sits in the tree), and `entry(t)` returns that exact same
149
   * stored `TypedValue` back out, declared `DataType.OPAQUE` too. If either direction coerced
150
   * based on the *value's* tag instead of the *declared* type, a tree holding an int would
151
   * silently print `1.0` instead of `1` again - the entire bug this PR exists to fix - just via a
152
   * different code path. There is deliberately no reverse direction on the argument side either -
153
   * modules do not get to declare an INTEGER parameter and have py-slang widen a float into a
154
   * `bigint` for them. Keeping modules bigint-free is the point: it's simpler, keeps arithmetic
155
   * fast, and doesn't assume every future language backing a module has bigints at all.
156
   *
157
   * Deliberately does NOT recurse into a `DataType.PAIR`/`LIST` argument to normalize elements
158
   * inside it (an earlier version of this function did, to unbreak a `sum_list`-style module that
159
   * reads raw NUMBER off list elements) - conductor's LIST/PAIR carries no declared element type,
160
   * so `entry(t)`/`left_branch(t)`/etc. on a binary_tree also declare their tree argument
161
   * `DataType.LIST`, indistinguishable at this layer from a *flat list of numbers*. Recursing
162
   * doesn't just fail to help sum_list - it actively walks straight into the tree's own stored
163
   * value and flattens it back to NUMBER before the module ever sees it, silently reintroducing
164
   * exactly the "1.0" bug this was all for. A list-consuming module that wants real integers has
165
   * to become INTEGER-aware itself; there's no safe way to guess it from here. This is a Python §2
166
   * concern specifically - modules that model chapter-2-style structures (binary_tree, and any
167
   * future module built the same way) necessarily live with that ambiguity and get no automatic
168
   * help here.
169
   *
170
   * No `DataType.ARRAY` handling here either, on the same reasoning as the PAIR/LIST case above,
171
   * plus a narrower practical one: nothing in py-slang's own `pythonToModule` (CSE/py2js/PVML
172
   * alike) ever constructs a `DataType.ARRAY` - a Python list always becomes a PAIR/EMPTY_LIST
173
   * chain (see the "list" case in each engine's `pythonToModule`) - so no code path today can ever
174
   * hand `closure_call` an ARRAY containing an INTEGER element in the first place. Add it back if
175
   * and when a real producer exists, gated on declared type exactly like everything else here, not
176
   * before.
177
   */
178
  private coerceArgsToSignature(
179
    args: TypedValue<DataType>[],
180
    declaredArgTypes: readonly DataType[],
181
  ): TypedValue<DataType>[] {
182
    return args.map((arg, i) => {
269✔
183
      const declared = declaredArgTypes[i];
209✔
184
      if (declared === DataType.NUMBER && arg.type === DataType.INTEGER) {
209✔
185
        return { type: DataType.NUMBER, value: Number(arg.value) };
83✔
186
      }
187
      return arg;
126✔
188
    });
189
  }
190
  /**
191
   * Enforces the other half of the same contract on the way out: a `DataType.NUMBER`-declared
192
   * return narrows an actual `INTEGER` value to a float, for the same reason and with the same
193
   * declared-type gating as `coerceArgsToSignature` above - anything declared something else
194
   * (`OPAQUE`, `PAIR`, `LIST`, ...) passes its actual value through completely untouched,
195
   * regardless of what that value's own runtime tag happens to be. This has to be gated on the
196
   * *declared* return type specifically, not "is the returned value's tag INTEGER" - `entry(t)`
197
   * declares `DataType.OPAQUE` and hands back whatever `TypedValue` was stored verbatim, which for
198
   * a tree holding a Python int really is `{ type: DataType.INTEGER, ... }` at runtime; coercing it
199
   * here regardless of declared type would reintroduce the exact "1.0" bug this PR exists to fix.
200
   */
201
  private coerceReturnValue<T extends DataType>(
202
    value: TypedValue<T>,
203
    declaredReturnType: DataType,
204
  ): TypedValue<T> {
205
    if (declaredReturnType === DataType.NUMBER && (value.type as DataType) === DataType.INTEGER) {
265✔
206
      return { type: DataType.NUMBER, value: Number(value.value) } as TypedValue<T>;
1✔
207
    }
208
    return value;
264✔
209
  }
210
  /**
211
   * `closure_call`/`closure_call_unchecked` hand back an `AsyncGenerator` (the module may yield
212
   * control back to the host mid-call), so the return-value coercion can't just wrap a value - it
213
   * has to wrap the generator and coerce whatever it eventually returns, while passing every
214
   * yielded value through untouched.
215
   */
216
  private async *coerceGeneratorReturn<T extends DataType>(
217
    gen: AsyncGenerator<void, TypedValue<T>, undefined>,
218
    declaredReturnType: DataType,
219
  ): AsyncGenerator<void, TypedValue<T>, undefined> {
220
    const result = yield* gen;
266✔
221
    return this.coerceReturnValue(result, declaredReturnType);
264✔
222
  }
223
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
224
    const array = this.arrayMap.get(a.value);
×
UNCOV
225
    if (!array) {
×
226
      throw new Error(`Invalid array identifier: ${a.value}`);
×
227
    }
UNCOV
228
    return Promise.resolve(array.elements.length);
×
229
  }
230
  array_get<T extends DataType>(
231
    a: TypedValue<DataType.ARRAY, T>,
232
    idx: number,
233
  ): Promise<TypedValue<NoInfer<T>>>;
234
  array_get(
235
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
236
    idx: number,
237
  ): Promise<TypedValue<DataType>> {
UNCOV
238
    const array = this.arrayMap.get(a.value);
×
239

UNCOV
240
    if (!array) {
×
241
      throw new Error(`Invalid array identifier: ${a.value}`);
×
242
    }
243

UNCOV
244
    if (idx < 0 || idx >= array.elements.length) {
×
UNCOV
245
      throw new Error(`Index out of bounds: ${idx}`);
×
246
    }
247

UNCOV
248
    const value = array.elements[idx];
×
249

UNCOV
250
    if (!value) {
×
251
      throw new Error(`Missing element at index ${idx}`);
×
252
    }
253

UNCOV
254
    return Promise.resolve(value);
×
255
  }
256

257
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
258
    const array = this.arrayMap.get(a.value);
×
UNCOV
259
    if (array === undefined) {
×
UNCOV
260
      throw new Error(`Invalid array identifier: ${a.value}`);
×
261
    }
UNCOV
262
    return Promise.resolve(array.type as NoInfer<T>);
×
263
  }
264
  array_set(
265
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
266
    idx: number,
267
    tv: TypedValue<DataType>,
268
  ): Promise<void>;
269
  array_set<T extends DataType>(
270
    a: TypedValue<DataType.ARRAY, T>,
271
    idx: number,
272
    tv: TypedValue<NoInfer<T>>,
273
  ): Promise<void> {
UNCOV
274
    const array = this.arrayMap.get(a.value) as
×
275
      | { type: T; elements: TypedValue<NoInfer<T>>[] }
276
      | undefined;
277

UNCOV
278
    if (!array) {
×
UNCOV
279
      throw new Error(`Invalid array identifier: ${a.value}`);
×
280
    }
281

UNCOV
282
    if (idx < 0 || idx >= array.elements.length) {
×
UNCOV
283
      throw new Error(`Index out of bounds: ${idx}`);
×
284
    }
285

UNCOV
286
    array.elements[idx] = tv;
×
287

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

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