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

source-academy / py-slang / 29679550781

19 Jul 2026 08:16AM UTC coverage: 86.124% (-0.8%) from 86.906%
29679550781

Pull #282

github

web-flow
Merge 4ecf4186a into 5a049ad5b
Pull Request #282: py2js: compile-to-JavaScript evaluator for Python §1 and §2

4066 of 5074 branches covered (80.13%)

Branch coverage included in aggregate %.

603 of 786 new or added lines in 8 files covered. (76.72%)

8776 of 9837 relevant lines covered (89.21%)

172797.94 hits per line

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

17.51
/src/conductor/GenericDataHandler.ts
1
import type { IEvaluator, IInterfacableEvaluator } from "@sourceacademy/conductor/runner";
2
import {
10✔
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) and the arguments/results flowing through
22
 * `closure_call`/`closure_call_unchecked` — this class never inspects them.
23
 *
24
 * Originally written inline in PyCseEvaluatorBase (see PyCseEvaluator.ts);
25
 * extracted so every evaluator that talks to conductor modules (CSE, py2js,
26
 * and eventually WASM/PVML) shares one implementation instead of
27
 * re-deriving the same identifier-table bookkeeping per engine. An evaluator
28
 * holds one instance (`private dataHandler = new GenericDataHandler()`) and
29
 * hands it to both `context.evaluator` (or the engine's equivalent) and
30
 * `conductor.registerPlugin(ModuleLoaderRunnerPlugin, conductor, dataHandler)`.
31
 */
32
export class GenericDataHandler implements IDataHandler {
10✔
33
  hasDataInterface = true as const;
30✔
34
  private pairMap = new Map<
30✔
35
    PairIdentifier,
36
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
37
  >();
38
  private arrayMap = new Map<
30✔
39
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
    ArrayIdentifier<any>,
41
    { type: DataType; elements: TypedValue<DataType>[] }
42
  >();
43
  private closureMap = new Map<
30✔
44
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
    ClosureIdentifier<any>,
46
    {
47
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
      sig: IFunctionSignature<any, any>;
49
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
      func: ExternCallable<any, any>;
51
      dependsOn?: (TypedValue<DataType> | null)[];
52
      isVararg?: boolean;
53
    }
54
  >();
55
  private opaqueMap = new Map<OpaqueIdentifier, { value: unknown; immutable: boolean }>();
30✔
56
  private uniqueId = 0;
30✔
57
  pair_make(
58
    head: TypedValue<DataType>,
59
    tail: TypedValue<DataType>,
60
  ): Promise<TypedValue<DataType.PAIR>> {
61
    this.pairMap.set(this.uniqueId++ as PairIdentifier, { head, tail });
3✔
62
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
3✔
63
  }
64
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
65
    const pair = this.pairMap.get(p.value);
3✔
66
    if (!pair) {
3!
NEW
67
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
68
    }
69
    return Promise.resolve(pair.head);
3✔
70
  }
71
  pair_sethead(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
NEW
72
    const pair = this.pairMap.get(p.value);
×
NEW
73
    if (!pair) {
×
NEW
74
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
75
    }
NEW
76
    pair.head = tv;
×
NEW
77
    return Promise.resolve();
×
78
  }
79
  pair_tail(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
NEW
80
    const pair = this.pairMap.get(p.value);
×
NEW
81
    if (!pair) {
×
NEW
82
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
83
    }
NEW
84
    return Promise.resolve(pair.tail);
×
85
  }
86
  pair_settail(p: TypedValue<DataType.PAIR>, tv: TypedValue<DataType>): Promise<void> {
NEW
87
    const pair = this.pairMap.get(p.value);
×
NEW
88
    if (!pair) {
×
NEW
89
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
90
    }
NEW
91
    pair.tail = tv;
×
NEW
92
    return Promise.resolve();
×
93
  }
94
  pair_assert(
95
    p: TypedValue<DataType.PAIR>,
96
    headType?: DataType,
97
    tailType?: DataType,
98
  ): Promise<void> {
NEW
99
    const pair = this.pairMap.get(p.value);
×
NEW
100
    if (!pair) {
×
NEW
101
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
102
    }
NEW
103
    if (headType && pair.head.type !== headType) {
×
NEW
104
      throw new Error(`Expected head of type ${headType}, got ${pair.head.type}`);
×
105
    }
NEW
106
    if (tailType && pair.tail.type !== tailType) {
×
NEW
107
      throw new Error(`Expected tail of type ${tailType}, got ${pair.tail.type}`);
×
108
    }
NEW
109
    return Promise.resolve();
×
110
  }
111
  array_make<T extends DataType>(
112
    t: T,
113
    len: number,
114
    init?: TypedValue<NoInfer<T>>,
115
  ): Promise<TypedValue<DataType.ARRAY, NoInfer<T>>> {
116
    const elements = new Array(len).fill(init ?? { type: t, value: undefined }) as TypedValue<
1✔
117
      NoInfer<T>
118
    >[];
119
    const arrayValue: TypedValue<DataType.ARRAY, NoInfer<T>> = {
1✔
120
      type: DataType.ARRAY,
121
      value: this.uniqueId++ as ArrayIdentifier<typeof t>,
122
    };
123
    this.arrayMap.set(arrayValue.value, { type: t, elements });
1✔
124
    return Promise.resolve(arrayValue);
1✔
125
  }
126
  array_length(a: TypedValue<DataType.ARRAY>): Promise<number> {
NEW
127
    const array = this.arrayMap.get(a.value);
×
NEW
128
    if (!array) {
×
NEW
129
      throw new Error(`Invalid array identifier: ${a.value}`);
×
130
    }
NEW
131
    return Promise.resolve(array.elements.length);
×
132
  }
133
  array_get<T extends DataType>(
134
    a: TypedValue<DataType.ARRAY, T>,
135
    idx: number,
136
  ): Promise<TypedValue<NoInfer<T>>>;
137
  array_get(
138
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
139
    idx: number,
140
  ): Promise<TypedValue<DataType>> {
NEW
141
    const array = this.arrayMap.get(a.value);
×
142

NEW
143
    if (!array) {
×
NEW
144
      throw new Error(`Invalid array identifier: ${a.value}`);
×
145
    }
146

NEW
147
    if (idx < 0 || idx >= array.elements.length) {
×
NEW
148
      throw new Error(`Index out of bounds: ${idx}`);
×
149
    }
150

NEW
151
    const value = array.elements[idx];
×
152

NEW
153
    if (!value) {
×
NEW
154
      throw new Error(`Missing element at index ${idx}`);
×
155
    }
156

NEW
157
    return Promise.resolve(value);
×
158
  }
159

160
  array_type<T extends DataType>(a: TypedValue<DataType.ARRAY, T>): Promise<NoInfer<T>> {
NEW
161
    const array = this.arrayMap.get(a.value);
×
NEW
162
    if (array === undefined) {
×
NEW
163
      throw new Error(`Invalid array identifier: ${a.value}`);
×
164
    }
NEW
165
    return Promise.resolve(array.type as NoInfer<T>);
×
166
  }
167
  array_set(
168
    a: TypedValue<DataType.ARRAY, DataType.VOID>,
169
    idx: number,
170
    tv: TypedValue<DataType>,
171
  ): Promise<void>;
172
  array_set<T extends DataType>(
173
    a: TypedValue<DataType.ARRAY, T>,
174
    idx: number,
175
    tv: TypedValue<NoInfer<T>>,
176
  ): Promise<void> {
NEW
177
    const array = this.arrayMap.get(a.value) as
×
178
      | { type: T; elements: TypedValue<NoInfer<T>>[] }
179
      | undefined;
180

NEW
181
    if (!array) {
×
NEW
182
      throw new Error(`Invalid array identifier: ${a.value}`);
×
183
    }
184

NEW
185
    if (idx < 0 || idx >= array.elements.length) {
×
NEW
186
      throw new Error(`Index out of bounds: ${idx}`);
×
187
    }
188

NEW
189
    array.elements[idx] = tv;
×
190

NEW
191
    return Promise.resolve();
×
192
  }
193
  array_assert<T extends DataType>(
194
    a: TypedValue<DataType.ARRAY>,
195
    type?: T,
196
    length?: number,
197
  ): Promise<void> {
NEW
198
    const array = this.arrayMap.get(a.value);
×
NEW
199
    if (!array) {
×
NEW
200
      throw new Error(`Invalid array identifier: ${a.value}`);
×
201
    }
NEW
202
    if (type !== undefined && array.type !== type) {
×
NEW
203
      throw new Error(`Expected array of type ${type}, got ${array.type}`);
×
204
    }
NEW
205
    if (length !== undefined && array.elements.length !== length) {
×
NEW
206
      throw new Error(`Expected array of length ${length}, got ${array.elements.length}`);
×
207
    }
NEW
208
    return Promise.resolve();
×
209
  }
210
  closure_make<const Arg extends readonly DataType[], const Ret extends DataType>(
211
    sig: IFunctionSignature<Arg, Ret>,
212
    func: ExternCallable<Arg, Ret>,
213
    dependsOn?: (TypedValue<DataType> | null)[],
214
  ): Promise<TypedValue<DataType.CLOSURE, Ret>> {
215
    const closureValue: TypedValue<DataType.CLOSURE, Ret> = {
10✔
216
      type: DataType.CLOSURE,
217
      value: this.uniqueId++ as ClosureIdentifier<Ret>,
218
    };
219
    this.closureMap.set(closureValue.value, { sig, func, dependsOn });
10✔
220
    return Promise.resolve(closureValue);
10✔
221
  }
222
  closure_is_vararg(c: TypedValue<DataType.CLOSURE>): Promise<boolean> {
NEW
223
    return Promise.resolve(this.closureMap.get(c.value)?.isVararg ?? false);
×
224
  }
225
  closure_arity(c: TypedValue<DataType.CLOSURE>): Promise<number> {
226
    return Promise.resolve(this.closureMap.get(c.value)?.sig.args.length ?? 0);
7!
227
  }
228
  closure_call<T extends DataType>(
229
    c: TypedValue<DataType.CLOSURE, T>,
230
    args: TypedValue<DataType>[],
231
    returnType: T,
232
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
NEW
233
    const closure = this.closureMap.get(c.value);
×
NEW
234
    if (closure === undefined) {
×
NEW
235
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
236
    }
NEW
237
    if (closure.sig.returnType !== returnType) {
×
NEW
238
      throw new Error(`Expected return type ${returnType}, got ${closure.sig.returnType}`);
×
239
    }
NEW
240
    return closure.func(...args) as AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined>;
×
241
  }
242
  closure_call_unchecked<T extends DataType>(
243
    c: TypedValue<DataType.CLOSURE, T>,
244
    args: TypedValue<DataType>[],
245
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
246
    return this.closureMap.get(c.value)?.func(...args) as AsyncGenerator<
10✔
247
      void,
248
      TypedValue<NoInfer<T>>,
249
      undefined
250
    >;
251
  }
252
  closure_arity_assert(c: TypedValue<DataType.CLOSURE>, arity: number): Promise<void> {
NEW
253
    const closure = this.closureMap.get(c.value);
×
NEW
254
    if (!closure) {
×
NEW
255
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
256
    }
NEW
257
    if (closure.sig.args.length !== arity && !closure.isVararg) {
×
NEW
258
      throw new Error(`Expected closure of arity ${arity}, got ${closure.sig.args.length}`);
×
259
    }
NEW
260
    return Promise.resolve();
×
261
  }
262
  opaque_make(v: unknown, immutable?: boolean): Promise<TypedValue<DataType.OPAQUE>> {
263
    const opaqueValue: TypedValue<DataType.OPAQUE> = {
2✔
264
      type: DataType.OPAQUE,
265
      value: this.uniqueId++ as OpaqueIdentifier,
266
    };
267
    this.opaqueMap.set(opaqueValue.value, { value: v, immutable: immutable || false });
2✔
268
    return Promise.resolve(opaqueValue);
2✔
269
  }
270
  opaque_get(o: TypedValue<DataType.OPAQUE>): Promise<unknown> {
NEW
271
    const opaque = this.opaqueMap.get(o.value);
×
NEW
272
    if (!opaque) {
×
NEW
273
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
274
    }
NEW
275
    return Promise.resolve(opaque.value);
×
276
  }
277
  opaque_update(o: TypedValue<DataType.OPAQUE>, v: unknown): Promise<void> {
NEW
278
    const opaque = this.opaqueMap.get(o.value);
×
NEW
279
    if (!opaque) {
×
NEW
280
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
281
    }
NEW
282
    if (opaque.immutable) {
×
NEW
283
      throw new Error(`Cannot update immutable opaque value with identifier: ${o.value}`);
×
284
    }
NEW
285
    opaque.value = v;
×
NEW
286
    return Promise.resolve();
×
287
  }
288
  tie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
NEW
289
    throw new Error("Method not implemented.");
×
290
  }
291
  untie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
NEW
292
    throw new Error("Method not implemented.");
×
293
  }
294
  async list(...elements: TypedValue<DataType>[]): Promise<TypedValue<DataType.LIST>> {
NEW
295
    const list = await elements.reduceRight(
×
296
      async (acc, el) => {
NEW
297
        return this.pair_make(el, await acc);
×
298
      },
299
      Promise.resolve({ type: DataType.EMPTY_LIST, value: null }) as Promise<
300
        TypedValue<DataType.LIST>
301
      >,
302
    );
NEW
303
    return list;
×
304
  }
305
  is_list(xs: TypedValue<DataType.LIST>): Promise<boolean> {
NEW
306
    let current: TypedValue<DataType> = xs;
×
NEW
307
    while (current.type !== DataType.EMPTY_LIST) {
×
NEW
308
      if (current.type !== DataType.PAIR) {
×
NEW
309
        return Promise.resolve(false);
×
310
      }
NEW
311
      const pair = this.pairMap.get(current.value);
×
NEW
312
      if (pair === undefined) {
×
NEW
313
        return Promise.resolve(false);
×
314
      }
NEW
315
      current = pair.tail;
×
316
    }
NEW
317
    return Promise.resolve(true);
×
318
  }
319
  list_to_vec(xs: TypedValue<DataType.LIST>): Promise<TypedValue<DataType>[]> {
NEW
320
    return new Promise((resolve, reject) => {
×
NEW
321
      const result: TypedValue<DataType>[] = [];
×
NEW
322
      let current: TypedValue<DataType> = xs;
×
NEW
323
      while (current.type !== DataType.EMPTY_LIST) {
×
NEW
324
        if (current.type !== DataType.PAIR) {
×
NEW
325
          reject(new Error(`Expected a list, got type ${current.type}`));
×
NEW
326
          return;
×
327
        }
NEW
328
        const pair = this.pairMap.get(current.value);
×
NEW
329
        if (!pair) {
×
NEW
330
          reject(new Error(`Invalid pair identifier: ${current.value}`));
×
NEW
331
          return;
×
332
        }
NEW
333
        result.push(pair.head);
×
NEW
334
        current = pair.tail;
×
335
      }
NEW
336
      resolve(result);
×
337
    });
338
  }
339
  async *accumulate<T extends Exclude<DataType, DataType.VOID>>(
340
    op: TypedValue<DataType.CLOSURE, T>,
341
    initial: TypedValue<T>,
342
    sequence: TypedValue<DataType.LIST>,
343
    _resultType: T,
344
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
NEW
345
    let acc = initial;
×
NEW
346
    let current: TypedValue<DataType> = sequence;
×
NEW
347
    while (current.type !== DataType.EMPTY_LIST) {
×
NEW
348
      if (current.type !== DataType.PAIR) {
×
NEW
349
        throw new Error(`Expected a list, got type ${current.type}`);
×
350
      }
NEW
351
      const pair = this.pairMap.get(current.value);
×
NEW
352
      if (!pair) {
×
NEW
353
        throw new Error(`Invalid pair identifier: ${current.value}`);
×
354
      }
NEW
355
      acc = yield* this.closure_call_unchecked(op, [acc, pair.head]);
×
NEW
356
      current = pair.tail;
×
357
    }
NEW
358
    return acc;
×
359
  }
360
  length(xs: TypedValue<DataType.LIST>): Promise<number> {
NEW
361
    let length = 0;
×
NEW
362
    let current: TypedValue<DataType> = xs;
×
NEW
363
    while (current.type !== DataType.EMPTY_LIST) {
×
NEW
364
      if (current.type !== DataType.PAIR) {
×
NEW
365
        throw new Error(`Expected a list, got type ${current.type}`);
×
366
      }
NEW
367
      const pair = this.pairMap.get(current.value);
×
NEW
368
      if (!pair) {
×
NEW
369
        throw new Error(`Invalid pair identifier: ${current.value}`);
×
370
      }
NEW
371
      length++;
×
NEW
372
      current = pair.tail;
×
373
    }
NEW
374
    return Promise.resolve(length);
×
375
  }
376
}
377

378
/**
379
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
380
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
381
 * evaluator built around `GenericDataHandler` has those two halves on two
382
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
383
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
384
 * combines them into the one object the registration call needs, so
385
 * combining stays a one-line call at each registration site instead of ~20
386
 * lines of per-evaluator forwarding methods duplicated alongside the
387
 * bookkeeping this class already centralizes.
388
 */
389
export function asInterfacableEvaluator(
10✔
390
  evaluator: IEvaluator,
391
  dataHandler: GenericDataHandler,
392
): IInterfacableEvaluator {
393
  return new Proxy(evaluator, {
12✔
394
    get(target, prop, receiver) {
395
      if (prop in dataHandler) {
5✔
396
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
397
        // and write dataHandler, not the proxy — a plain Reflect.get returns
398
        // the method unbound, so calling it here would set `this` to the
399
        // proxy and (absent a `set` trap) silently write to `evaluator`.
400
        const value = Reflect.get(dataHandler, prop, dataHandler);
4✔
401
        return typeof value === "function" ? value.bind(dataHandler) : value;
4!
402
      }
403
      return Reflect.get(target, prop, receiver);
1✔
404
    },
405
  }) as unknown as IInterfacableEvaluator;
406
}
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