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

source-academy / py-slang / 29745916326

20 Jul 2026 01:21PM UTC coverage: 86.632% (-0.3%) from 86.906%
29745916326

push

github

web-flow
py2js: compile-to-JavaScript evaluator for Python §1-4 (#282)

* Add py2js engine: compile-to-JavaScript evaluator for SICPy chapter 1

A fourth execution engine (alongside CSE, PVML and WASM) that compiles
the py-slang AST to JavaScript source and runs it natively, in the style
of js-slang's transpiler:

- src/engines/py2js/runtime.ts: unboxed value model (int=bigint,
  float=number, None=null, complex=PyComplexNumber objects); operator
  helpers mirror the CSE machine's chapter-1 semantics (same dispatch
  order, same §1 restrictions); tail-call markers + trampoline for
  proper tail calls; dual compilation support (def2/acall/callSync) so
  conductor modules can call user functions synchronously in hot loops
  while the program spine stays async-capable.
- src/engines/py2js/compiler.ts: AST-to-JS codegen, $-prefix identifier
  mangling, sync and dual modes; exec-style only (no last-expression
  value, matching Python semantics).
- src/engines/py2js/index.ts: runCodePy2Js/runCodePy2JsDual — parse,
  resolve with the chapter validators, compile, run; chapter 1 only.
- src/tests/operator-conformance-py2js.test.ts: table-driven operator
  conformance sweep (749 cases) over the docs/specs typing tables,
  pinned against a freshly computed CSE reference with print()-wrapped
  observation, mirroring the PVML-in-browser sweep.
- src/types/value-types.ts: extract context-free PyComplexNumber.divBy
  so py2js reuses the CPython-style division algorithm without a CSE
  Context.
- experiments/py2js/: the exploratory prototype, differential harness
  and benchmarks that motivated the design (CSE/PVML comparisons,
  dual-compilation sound-module measurements); not wired into the
  build or tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbExZBaxgeUL4oi1t7PG93

* Address #282 Gemini review: hypot for complex abs; guard non-finite math_floor; keep error kind in Py2JsRunError

- abs(complex) uses Math.... (continued)

4179 of 5196 branches covered (80.43%)

Branch coverage included in aggregate %.

854 of 1006 new or added lines in 8 files covered. (84.89%)

9035 of 10057 relevant lines covered (89.84%)

183357.32 hits per line

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

44.29
/src/conductor/GenericDataHandler.ts
1
import type { IEvaluator, IInterfacableEvaluator } from "@sourceacademy/conductor/runner";
2
import {
15✔
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 {
15✔
33
  hasDataInterface = true as const;
52✔
34
  private pairMap = new Map<
52✔
35
    PairIdentifier,
36
    { head: TypedValue<DataType>; tail: TypedValue<DataType> }
37
  >();
38
  private arrayMap = new Map<
52✔
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<
52✔
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 }>();
52✔
56
  private uniqueId = 0;
52✔
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 });
23✔
62
    return Promise.resolve({ type: DataType.PAIR, value: (this.uniqueId - 1) as PairIdentifier });
23✔
63
  }
64
  pair_head(p: TypedValue<DataType.PAIR>): Promise<TypedValue<DataType>> {
65
    const pair = this.pairMap.get(p.value);
10✔
66
    if (!pair) {
10!
NEW
67
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
68
    }
69
    return Promise.resolve(pair.head);
10✔
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>> {
80
    const pair = this.pairMap.get(p.value);
6✔
81
    if (!pair) {
6!
NEW
82
      throw new Error(`Invalid pair identifier: ${p.value}`);
×
83
    }
84
    return Promise.resolve(pair.tail);
6✔
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> = {
21✔
216
      type: DataType.CLOSURE,
217
      value: this.uniqueId++ as ClosureIdentifier<Ret>,
218
    };
219
    this.closureMap.set(closureValue.value, { sig, func, dependsOn });
21✔
220
    return Promise.resolve(closureValue);
21✔
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);
12!
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<
20✔
247
      void,
248
      TypedValue<NoInfer<T>>,
249
      undefined
250
    >;
251
  }
252
  /**
253
   * Fast path for a closure that provably never needs to leave the current
254
   * synchronous call - e.g. a scalar-in/scalar-out wave function sampled
255
   * 44100x/sec by the sound module. `func` (an `ExternCallable`, normally
256
   * only callable as an AsyncGenerator per conductor's own contract) may
257
   * additionally carry a `.sync` escape hatch: a plain function computing
258
   * the exact same result with no Promise/generator indirection at all. An
259
   * engine's module-interop layer sets `.sync` only when it can prove the
260
   * closure never needs a real host round-trip (see py2js's moduleInterop.ts
261
   * pyClosureFunc); a closure with no such proof (every CSE-machine closure
262
   * today, or a py2js closure that touches something asyncOnly) simply never
263
   * gets one.
264
   *
265
   * Returns `undefined` when the closure has no sync form - the signal for
266
   * "fall back to closure_call_unchecked" - which is unambiguous because a
267
   * TypedValue always wraps a real `{ type, value }` pair, even for
268
   * DataType.VOID; the bare JS value `undefined` is never a legitimate
269
   * closure result.
270
   */
271
  closure_call_sync<T extends DataType>(
272
    c: TypedValue<DataType.CLOSURE, T>,
273
    args: TypedValue<DataType>[],
274
  ): TypedValue<NoInfer<T>> | undefined {
275
    const func = this.closureMap.get(c.value)?.func as
3✔
276
      | (ExternCallable<DataType[], T> & {
277
          sync?: (...a: TypedValue<DataType>[]) => TypedValue<DataType> | undefined;
278
        })
279
      | undefined;
280
    return func?.sync?.(...args) as TypedValue<NoInfer<T>> | undefined;
3✔
281
  }
282
  closure_arity_assert(c: TypedValue<DataType.CLOSURE>, arity: number): Promise<void> {
NEW
283
    const closure = this.closureMap.get(c.value);
×
NEW
284
    if (!closure) {
×
NEW
285
      throw new Error(`Invalid closure identifier: ${c.value}`);
×
286
    }
NEW
287
    if (closure.sig.args.length !== arity && !closure.isVararg) {
×
NEW
288
      throw new Error(`Expected closure of arity ${arity}, got ${closure.sig.args.length}`);
×
289
    }
NEW
290
    return Promise.resolve();
×
291
  }
292
  opaque_make(v: unknown, immutable?: boolean): Promise<TypedValue<DataType.OPAQUE>> {
293
    const opaqueValue: TypedValue<DataType.OPAQUE> = {
2✔
294
      type: DataType.OPAQUE,
295
      value: this.uniqueId++ as OpaqueIdentifier,
296
    };
297
    this.opaqueMap.set(opaqueValue.value, { value: v, immutable: immutable || false });
2✔
298
    return Promise.resolve(opaqueValue);
2✔
299
  }
300
  opaque_get(o: TypedValue<DataType.OPAQUE>): Promise<unknown> {
NEW
301
    const opaque = this.opaqueMap.get(o.value);
×
NEW
302
    if (!opaque) {
×
NEW
303
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
304
    }
NEW
305
    return Promise.resolve(opaque.value);
×
306
  }
307
  opaque_update(o: TypedValue<DataType.OPAQUE>, v: unknown): Promise<void> {
NEW
308
    const opaque = this.opaqueMap.get(o.value);
×
NEW
309
    if (!opaque) {
×
NEW
310
      throw new Error(`Invalid opaque identifier: ${o.value}`);
×
311
    }
NEW
312
    if (opaque.immutable) {
×
NEW
313
      throw new Error(`Cannot update immutable opaque value with identifier: ${o.value}`);
×
314
    }
NEW
315
    opaque.value = v;
×
NEW
316
    return Promise.resolve();
×
317
  }
318
  tie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
NEW
319
    throw new Error("Method not implemented.");
×
320
  }
321
  untie(_dependent: TypedValue<DataType>, _dependee: TypedValue<DataType> | null): Promise<void> {
NEW
322
    throw new Error("Method not implemented.");
×
323
  }
324
  async list(...elements: TypedValue<DataType>[]): Promise<TypedValue<DataType.LIST>> {
325
    const list = await elements.reduceRight(
5✔
326
      async (acc, el) => {
327
        return this.pair_make(el, await acc);
13✔
328
      },
329
      Promise.resolve({ type: DataType.EMPTY_LIST, value: null }) as Promise<
330
        TypedValue<DataType.LIST>
331
      >,
332
    );
333
    return list;
5✔
334
  }
335
  is_list(xs: TypedValue<DataType.LIST>): Promise<boolean> {
336
    let current: TypedValue<DataType> = xs;
4✔
337
    while (current.type !== DataType.EMPTY_LIST) {
4✔
338
      if (current.type !== DataType.PAIR) {
7✔
339
        return Promise.resolve(false);
2✔
340
      }
341
      const pair = this.pairMap.get(current.value);
5✔
342
      if (pair === undefined) {
5✔
343
        return Promise.resolve(false);
1✔
344
      }
345
      current = pair.tail;
4✔
346
    }
347
    return Promise.resolve(true);
1✔
348
  }
349
  list_to_vec(xs: TypedValue<DataType.LIST>): Promise<TypedValue<DataType>[]> {
350
    return new Promise((resolve, reject) => {
3✔
351
      const result: TypedValue<DataType>[] = [];
3✔
352
      let current: TypedValue<DataType> = xs;
3✔
353
      while (current.type !== DataType.EMPTY_LIST) {
3✔
354
        if (current.type !== DataType.PAIR) {
5✔
355
          reject(new Error(`Expected a list, got type ${current.type}`));
1✔
356
          return;
1✔
357
        }
358
        const pair = this.pairMap.get(current.value);
4✔
359
        if (!pair) {
4✔
360
          reject(new Error(`Invalid pair identifier: ${current.value}`));
1✔
361
          return;
1✔
362
        }
363
        result.push(pair.head);
3✔
364
        current = pair.tail;
3✔
365
      }
366
      resolve(result);
1✔
367
    });
368
  }
369
  async *accumulate<T extends Exclude<DataType, DataType.VOID>>(
370
    op: TypedValue<DataType.CLOSURE, T>,
371
    initial: TypedValue<T>,
372
    sequence: TypedValue<DataType.LIST>,
373
    _resultType: T,
374
  ): AsyncGenerator<void, TypedValue<NoInfer<T>>, undefined> {
375
    let acc = initial;
3✔
376
    let current: TypedValue<DataType> = sequence;
3✔
377
    while (current.type !== DataType.EMPTY_LIST) {
3✔
378
      if (current.type !== DataType.PAIR) {
4✔
379
        throw new Error(`Expected a list, got type ${current.type}`);
1✔
380
      }
381
      const pair = this.pairMap.get(current.value);
3✔
382
      if (!pair) {
3!
NEW
383
        throw new Error(`Invalid pair identifier: ${current.value}`);
×
384
      }
385
      acc = yield* this.closure_call_unchecked(op, [acc, pair.head]);
3✔
386
      current = pair.tail;
3✔
387
    }
388
    return acc;
2✔
389
  }
390
  length(xs: TypedValue<DataType.LIST>): Promise<number> {
391
    let length = 0;
3✔
392
    let current: TypedValue<DataType> = xs;
3✔
393
    while (current.type !== DataType.EMPTY_LIST) {
3✔
394
      if (current.type !== DataType.PAIR) {
6✔
395
        throw new Error(`Expected a list, got type ${current.type}`);
1✔
396
      }
397
      const pair = this.pairMap.get(current.value);
5✔
398
      if (!pair) {
5✔
399
        throw new Error(`Invalid pair identifier: ${current.value}`);
1✔
400
      }
401
      length++;
4✔
402
      current = pair.tail;
4✔
403
    }
404
    return Promise.resolve(length);
1✔
405
  }
406
}
407

408
/**
409
 * `ModuleLoaderRunnerPlugin`'s constructor requires a single object
410
 * satisfying `IInterfacableEvaluator` (`IEvaluator & IDataHandler`) — but an
411
 * evaluator built around `GenericDataHandler` has those two halves on two
412
 * different objects (the evaluator itself, extending `BasicEvaluator`, is
413
 * the `IEvaluator`; its `dataHandler` field is the `IDataHandler`). A Proxy
414
 * combines them into the one object the registration call needs, so
415
 * combining stays a one-line call at each registration site instead of ~20
416
 * lines of per-evaluator forwarding methods duplicated alongside the
417
 * bookkeeping this class already centralizes.
418
 */
419
export function asInterfacableEvaluator(
15✔
420
  evaluator: IEvaluator,
421
  dataHandler: GenericDataHandler,
422
): IInterfacableEvaluator {
423
  return new Proxy(evaluator, {
12✔
424
    get(target, prop, receiver) {
425
      if (prop in dataHandler) {
5✔
426
        // Bind so stateful methods (this.uniqueId++ in pair_make etc.) read
427
        // and write dataHandler, not the proxy — a plain Reflect.get returns
428
        // the method unbound, so calling it here would set `this` to the
429
        // proxy and (absent a `set` trap) silently write to `evaluator`.
430
        const value = Reflect.get(dataHandler, prop, dataHandler);
4✔
431
        return typeof value === "function" ? value.bind(dataHandler) : value;
4!
432
      }
433
      return Reflect.get(target, prop, receiver);
1✔
434
    },
435
  }) as unknown as IInterfacableEvaluator;
436
}
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