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

source-academy / py-slang / 30916820724

04 Aug 2026 02:01PM UTC coverage: 86.062% (+0.04%) from 86.027%
30916820724

Pull #399

github

web-flow
Merge 59eea6204 into 64acbb674
Pull Request #399: py2js: stop showing a fabricated line number, name the enclosing predefined function

4680 of 5860 branches covered (79.86%)

Branch coverage included in aggregate %.

78 of 90 new or added lines in 4 files covered. (86.67%)

76 existing lines in 3 files now uncovered.

10435 of 11703 relevant lines covered (89.17%)

166418.02 hits per line

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

93.3
/src/engines/py2js/stdlibBridge.ts
1
/**
2
 * py2js engine — stdlib bridge.
3
 *
4
 * The stdlib groups (src/stdlib/misc.ts, math.ts, …) are written against the
5
 * CSE machine's tagged Value union with `(args, source, command, context)`
6
 * signatures. This bridge exposes them to py2js by converting the engine's
7
 * native (unboxed) values to tagged Values at the call boundary and back for
8
 * the result — so both engines run the *same* builtin implementations, and
9
 * stdlib semantics can never drift between them. Conformance is pinned by
10
 * src/tests/stdlib-conformance-py2js.test.ts.
11
 *
12
 * What crosses the boundary at chapters 1-2: int/float/bool/str/None/complex
13
 * round-trip losslessly (complex is a PyComplexNumber on both sides); a pair
14
 * (a 2-element PyList) round-trips to/from CSE's identically-shaped 2-element
15
 * list Value — chapter 2 has no list-literal syntax and no list.ts group, so
16
 * pair()/llist() (the only way to construct one) always produce exactly that
17
 * shape. A function crosses one way *as a value a builtin inspects* (a user function
18
 * becomes a minimal FunctionValue, so is_function answers True and error
19
 * messages say 'function'; a py2js builtin becomes a stub BuiltinValue —
20
 * neither is callable from stdlib code, which no chapter-1/2 builtin
21
 * attempts) — but it can still flow back out *unchanged*, e.g.
22
 * `head(pair(1, f))` or `llist(f)`, since no chapter-1/2 builtin fabricates a
23
 * genuinely new function value, only passes an argument through structurally
24
 * (into/out of a pair). `functionOrigin` (a WeakMap from the synthetic tagged
25
 * stand-in back to the real PyFunction) is what makes that round-trip
26
 * lossless without reconstructing a function from its printable stand-in,
27
 * which is impossible in general.
28
 *
29
 * Error handling: stdlib builtins raise through handleRuntimeError, which
30
 * records on the bridge's Context and throws the error class — the throw
31
 * propagates out of the compiled py2js program and is wrapped into a
32
 * Py2JsRunError by index.ts, preserving the error-class name. The `command`
33
 * node each builtin receives is a synthetic Call whose callee token carries
34
 * the builtin's name, so messages that name the callee ("unsupported argument
35
 * type for math_sin…") stay accurate; source positions point at the program
36
 * start until py2js grows real error locations.
37
 *
38
 * Async builtins (print, input — the stream-based ones) are NOT bridged:
39
 * their sync py2js replacements live in runtime.ts's native core, and the
40
 * bridge's Promise guard below is the backstop for any future async stdlib
41
 * addition. `arity` is also native (py2js functions are not CSE closures);
42
 * bridged builtins carry pyMinArgs so it reports the same numbers the CSE
43
 * machine does.
44
 */
45
import type { BaseDataVisualizerRunnerPlugin } from "@sourceacademy/runner-data-visualizer";
46

47
import { ExprNS } from "../../ast-types";
19✔
48
import { RuntimeSourceError } from "../../errors";
19✔
49
import { Token, TokenType } from "../../tokenizer";
19✔
50
import { GroupName } from "../../stdlib/utils";
19✔
51
import type { Group } from "../../stdlib/utils";
52
import { Context } from "../cse/context";
19✔
53
import type { Environment } from "../cse/environment";
54
import type { BuiltinValue, ListValue, Value } from "../cse/stash";
55
import {
19✔
56
  isPairShaped,
57
  Py2JsRuntime,
58
  Py2JsRuntimeError,
59
  PyFunction,
60
  PyList,
61
  PyOpaque,
62
  pyTypeName,
63
  PyValue,
64
} from "./runtime";
65

66
function syntheticCallNode(name: string): ExprNS.Call {
67
  const token = new Token(TokenType.NAME, name, 1, 0, 0);
408,505✔
68
  token.synthetic = true;
408,505✔
69
  const callee = new ExprNS.Variable(token, token, token);
408,505✔
70
  return new ExprNS.Call(token, token, callee, []);
408,505✔
71
}
72

73
/** Maps a synthetic tagged stand-in (built below) back to the real PyFunction
74
 * it stands in for — see the file header's note on lossless pass-through. */
75
const functionOrigin = new WeakMap<object, PyFunction>();
19✔
76

77
/**
78
 * `memo` maps a PyList already being converted to its (still being filled
79
 * in) tagged ListValue, keyed by identity and populated *before* recursing
80
 * into the list's elements/spine — chapter 3's set_head/set_tail, or a
81
 * literal-list subscript assignment (`a[0] = a`), can build a genuinely
82
 * cyclic PyList (issue #341), and without this a bridged builtin's argument
83
 * conversion recurses/loops forever converting it. Populating the memo
84
 * before descending means a PyList reachable from itself converts to a
85
 * ListValue reachable from itself, so whatever CSE builtin receives it sees
86
 * the same kind of cyclic Value graph its own native (non-bridged) callers
87
 * already have to cope with (e.g. stringify.ts's ancestor tracking) —
88
 * cycle-handling stays the shared builtin's job, not this bridge's.
89
 */
90
function toTagged(v: PyValue, memo: Map<PyList, Value> = new Map()): Value {
3,655✔
91
  switch (typeof v) {
4,980✔
92
    case "bigint":
93
      return { type: "bigint", value: v };
2,279✔
94
    case "number":
95
      return { type: "number", value: v };
739✔
96
    case "boolean":
97
      return { type: "bool", value: v };
149✔
98
    case "string":
99
      return { type: "string", value: v };
310✔
100
    case "function": {
101
      // See file header: functions cross as inspectable, non-callable
102
      // stand-ins. The fallbacks cover bare JS functions that bypassed
103
      // annotateHostFunction (index.ts establishes the metadata invariant
104
      // for extraBuiltins; Function#name can be "", hence || not ??).
105
      const name = v.pyName ?? (v.name || "(anonymous)");
280!
106
      const tagged: Value = v.pyBuiltin
280✔
107
        ? {
108
            type: "builtin",
109
            name,
110
            minArgs: v.pyMinArgs ?? Math.max(0, v.pyArity ?? v.length),
168!
111
            func: () => {
112
              throw new Py2JsRuntimeError(
×
113
                "SystemError",
114
                `stdlib bridge: ${name} cannot be called from a bridged builtin`,
115
              );
116
            },
117
          }
118
        : {
119
            type: "function",
120
            name,
121
            params: [],
122
            body: [],
123
            env: undefined as unknown as Environment,
124
          };
125
      functionOrigin.set(tagged, v);
280✔
126
      return tagged;
280✔
127
    }
128
    default:
129
      if (v === null) return { type: "none" };
1,223✔
130
      // CSE has no separate representation for a pair vs. an arbitrary-
131
      // length list (both are its flat `{type:"list", value: Value[]}` —
132
      // see src/engines/cse/stash.ts), so converting element-wise here
133
      // reproduces CSE's own is_list/list_length answers exactly, including
134
      // on a 2-element list (which CSE cannot tell apart from a pair
135
      // either — see runtime.ts's PyList doc comment).
136
      if (Array.isArray(v)) return toTaggedList(v, memo);
708✔
137
      // No chapter-1/2 stdlib builtin accepts an opaque module value as an
138
      // argument (abs/math_sqrt/etc. all type-check against "opaque" being
139
      // absent from their accepted types), so this just needs to produce
140
      // *some* CSE Value the builtin's own dispatch will reject cleanly —
141
      // matching CSE's own opaque conversion shape (modules.ts).
142
      if (v instanceof PyOpaque) return { type: "opaque", value: v.typed };
151!
143
      return { type: "complex", value: v };
151✔
144
  }
145
}
146

147
/**
148
 * Converts a PyList to CSE's nested list Value. A long proper list built via
149
 * pair()/llist() (or one built via chapter-3+ mutation into the same shape)
150
 * has its entire length along the "spine" (chained tail positions), so this
151
 * walks that spine iteratively rather than recursing — a naive
152
 * `{ type: "list", value: [toTagged(v[0]), toTagged(v[1])] }` applied
153
 * per-element via plain recursion would cost one JS stack frame per element
154
 * just to convert a single argument for one bridged call, and a list of a
155
 * few thousand elements already overflows the stack, regardless of how
156
 * tail-recursive the user's own Python is (its own tail calls go through
157
 * py2js's trampoline; this bridge conversion does not). The walk continues
158
 * exactly as long as the current node is itself a 2-element list (a chain
159
 * link) not already seen this walk; it stops — falling through to the final
160
 * tail conversion — the moment that shape breaks (a flat N-element (N≠2)
161
 * literal list: zero iterations; a pair whose tail isn't itself a pair: one
162
 * iteration) or the chain loops back on itself (chapter 3's set_tail, or an
163
 * N-element list looping back into itself, e.g. `a[0] = a`). `heads[i]`/the
164
 * final tail are still converted via the ordinary (recursive) toTagged,
165
 * since each head is normally a scalar leaf; a list-of-lists nested
166
 * arbitrarily deep through a head position remains a (much rarer)
167
 * recursion, same as before.
168
 *
169
 * Each spine node gets its ListValue placeholder allocated (and memoized)
170
 * before its tail is known, exactly like toTagged's memo contract — so
171
 * closing a cycle back onto an earlier node just reuses that node's
172
 * placeholder as the tail, producing a genuinely cyclic Value graph instead
173
 * of looping forever building `heads`.
174
 */
175
function toTaggedList(v: PyList, memo: Map<PyList, Value>): Value {
176
  const memoized = memo.get(v);
557✔
177
  if (memoized !== undefined) return memoized;
557✔
178
  if (!isPairShaped(v)) {
555✔
179
    const listValue: ListValue = { type: "list", value: [] };
9✔
180
    memo.set(v, listValue);
9✔
181
    listValue.value = v.map(elem => toTagged(elem, memo));
17✔
182
    return listValue;
9✔
183
  }
184
  const nodes: PyList[] = [];
546✔
185
  const placeholders: ListValue[] = [];
546✔
186
  let current: PyValue = v;
546✔
187
  while (isPairShaped(current)) {
546✔
188
    const seen = memo.get(current);
762✔
189
    if (seen !== undefined) break;
762!
190
    const placeholder: ListValue = { type: "list", value: [] };
762✔
191
    memo.set(current, placeholder);
762✔
192
    nodes.push(current);
762✔
193
    placeholders.push(placeholder);
762✔
194
    current = current[1];
762✔
195
  }
196
  let tail = isPairShaped(current) ? memo.get(current)! : toTagged(current, memo);
546!
197
  for (let i = nodes.length - 1; i >= 0; i--) {
546✔
198
    placeholders[i].value = [toTagged(nodes[i][0], memo), tail];
762✔
199
    tail = placeholders[i];
762✔
200
  }
201
  return memo.get(v)!;
546✔
202
}
203

204
function fromTagged(name: string, v: Value): PyValue {
205
  switch (v.type) {
28,058!
206
    case "bigint":
207
    case "number":
208
    case "string":
209
    case "complex":
210
      return v.value;
26,753✔
211
    case "bool":
212
      return v.value;
281✔
213
    case "none":
214
      return null;
288✔
215
    case "list": {
216
      // Mirrors toTaggedList's iterative spine-walk, in the opposite
217
      // direction: a long proper list/pair chain a bridged builtin returns
218
      // (enum_llist, reverse, map, …) is nested 2-element CSE list Values
219
      // all the way down, so reconstructing it via plain per-element
220
      // recursion (fromTagged on each tail) would cost one JS stack frame
221
      // per element — the same failure mode toTaggedList's own doc comment
222
      // describes, just crossing the boundary in the other direction.
223
      if (v.value.length !== 2) return v.value.map(el => fromTagged(name, el));
662!
224
      const heads: PyValue[] = [];
662✔
225
      let current: Value = v;
662✔
226
      while (current.type === "list" && current.value.length === 2) {
662✔
227
        heads.push(fromTagged(name, current.value[0]));
900✔
228
        current = current.value[1];
900✔
229
      }
230
      let tail = fromTagged(name, current);
662✔
231
      for (let i = heads.length - 1; i >= 0; i--) {
662✔
232
        tail = [heads[i], tail];
900✔
233
      }
234
      return tail;
662✔
235
    }
236
    case "function":
237
    case "builtin": {
238
      // A function value only ever flows back out as one of THIS bridge's
239
      // own synthetic stand-ins passed through unchanged (see file header;
240
      // e.g. head(pair(1, f)) or llist(f)) — never a genuinely new closure a
241
      // builtin fabricated, which no chapter-1/2 builtin does. Recover the
242
      // original PyFunction rather than trying to reconstruct one.
243
      const original = functionOrigin.get(v);
74✔
244
      if (original !== undefined) return original;
74✔
245
      throw new Py2JsRuntimeError(
×
246
        "SystemError",
247
        `stdlib bridge: ${name}() returned a function value the bridge did not itself produce`,
248
      );
249
    }
250
    default:
251
      // No chapter-1/2 stdlib builtin returns a closure or list-family value
252
      // other than a pair; reaching this means the bridge needs extending,
253
      // not that user code is wrong.
254
      throw new Py2JsRuntimeError(
×
255
        "SystemError",
256
        `stdlib bridge: ${name}() returned an unbridgeable '${v.type}' value`,
257
      );
258
  }
259
}
260

261
function bridgeBuiltin(
262
  rt: Py2JsRuntime,
263
  name: string,
264
  builtin: BuiltinValue,
265
  context: Context,
266
  source: string,
267
): PyFunction {
268
  const command = syntheticCallNode(name);
408,505✔
269
  const call = builtin.func as (
408,505✔
270
    args: Value[],
271
    source: string,
272
    command: ExprNS.Call,
273
    context: Context,
274
  ) => Value | undefined | Promise<Value | undefined>;
275
  // pyArity -1: argument-count validation is the builtin's own @Validate
276
  // wrapper, so arity errors carry the CSE machine's exact messages.
277
  const f = rt.def(name, -1, (...args: PyValue[]) => {
408,505✔
278
    let result: Value | undefined | Promise<Value | undefined>;
279
    try {
2,732✔
280
      result = call(
2,732✔
281
        args.map(a => toTagged(a)),
3,655✔
282
        source,
283
        command,
284
        context,
285
      );
286
    } catch (e) {
287
      // handleRuntimeError (src/engines/cse/error.ts) throws a
288
      // RuntimeSourceError — a plain object implementing SourceError, not
289
      // `extends Error` (see errors.ts) — so it fails `instanceof Error`
290
      // everywhere up the call chain (including index.ts's own catch
291
      // blocks), collapsing to the useless "[object Object]" (py-slang#295)
292
      // instead of surfacing whatever real message .message holds. Convert
293
      // it into a proper Py2JsRuntimeError here, at the boundary where the
294
      // CSE-shaped error actually originates, the same way every other
295
      // py2js-native error already carries its kind as `.name`.
296
      // error.constructor.name is the exact class name (TypeError,
297
      // IndexError, ZeroDivisionError, ...) for every RuntimeSourceError
298
      // subclass — none of them set `.name` themselves (CSE's own
299
      // displayError falls back to a generic "Error" name for the same
300
      // reason), so the constructor is the only reliable source for it.
301
      if (e instanceof RuntimeSourceError) {
1,381✔
302
        // The builtin itself is named in e.message already ("...for tail: ...");
303
        // this instead names the *enclosing* predefined function, if any — e.g. a
304
        // student calling map() never wrote or sees _map, the internal helper that
305
        // actually calls tail() (py-slang#397).
306
        const enclosing = rt.enclosingPreludeFunction();
1,358✔
307
        const message = enclosing
1,358✔
308
          ? `${e.message} (in predefined function '${enclosing}')`
309
          : e.message;
310
        throw new Py2JsRuntimeError(e.constructor.name, message);
1,358✔
311
      }
312
      throw e;
23✔
313
    }
314
    if (result instanceof Promise) {
1,351!
NEW
315
      throw new Py2JsRuntimeError(
×
316
        "RuntimeError",
317
        `${name}() is asynchronous and not yet supported by the py2js engine`,
318
      );
319
    }
320
    return result === undefined ? null : fromTagged(name, result);
1,351!
321
  });
322
  f.pyBuiltin = true;
408,505✔
323
  f.pyNative = true;
408,505✔
324
  f.pyMinArgs = builtin.minArgs;
408,505✔
325
  return f;
408,505✔
326
}
327

328
/**
329
 * set_head/set_tail (chapter 3's pair-mutators group): mutate an existing
330
 * 2-element list *in place*. This cannot go through the generic toTagged/
331
 * fromTagged round-trip like every other bridged builtin — that round-trip
332
 * converts the argument into a *fresh* CSE-side value, so a mutation the CSE
333
 * builtin performs on it (as pairmutator.ts does) would be silently lost
334
 * rather than visible on the original PyList the caller's Python variable
335
 * still points to.
336
 */
337
function nativeSetPairSlot(name: string, index: 0 | 1, sayPair: boolean): PyFunction {
338
  const f = ((...args: PyValue[]) => {
2,526✔
339
    const target = args[0];
7✔
340
    const value = args[1];
7✔
341
    if (isPairShaped(target)) {
7✔
342
      target[index] = value;
6✔
343
      return null;
6✔
344
    }
345
    throw new Py2JsRuntimeError(
1✔
346
      "TypeError",
347
      `${name}() expects a pair as first argument, got '${pyTypeName(target, sayPair)}'`,
348
    );
349
  }) as PyFunction;
350
  f.pyName = name;
2,526✔
351
  f.pyArity = 2;
2,526✔
352
  f.pyBuiltin = true;
2,526✔
353
  f.pyNative = true;
2,526✔
354
  f.pyMinArgs = 2;
2,526✔
355
  return f;
2,526✔
356
}
357

358
/**
359
 * stream() (chapter 3's stream group): the one native primitive the group
360
 * needs — every other stream function (stream_map, stream_filter, …) is pure
361
 * Python in stream.prelude.ts, already runnable once pairs/closures work, so
362
 * it never touches this. Not bridged generically because CSE's own
363
 * StreamBuiltins.stream fabricates a brand new closure (the lazy tail thunk)
364
 * on every call — the bridge's toTagged/fromTagged round-trip only handles
365
 * function values that either originated in py2js or pass through
366
 * unchanged, not ones a CSE builtin invents on the spot — so it's
367
 * reimplemented directly against py2js's own PyList/PyFunction instead,
368
 * mirroring StreamBuiltins.stream's recursion exactly.
369
 *
370
 * `build` takes an index rather than re-slicing `args` on every lazy step:
371
 * `args.slice(1)` would copy the remaining N-1 elements at each of N steps,
372
 * O(N^2) time and space for an N-element stream.
373
 */
374
function nativeStream(rt: Py2JsRuntime): PyFunction {
375
  const build = (args: PyValue[], index: number): PyValue => {
1,263✔
376
    if (index >= args.length) return null;
19✔
377
    const tail = rt.def("anonymous stream", 0, () => build(args, index + 1));
16✔
378
    tail.pyBuiltin = true;
16✔
379
    tail.pyNative = true;
16✔
380
    return [args[index], tail];
16✔
381
  };
382
  const f = ((...args: PyValue[]) => build(args, 0)) as PyFunction;
1,263✔
383
  f.pyName = "stream";
1,263✔
384
  f.pyArity = -1;
1,263✔
385
  f.pyBuiltin = true;
1,263✔
386
  f.pyNative = true;
1,263✔
387
  f.pyMinArgs = 0;
1,263✔
388
  return f;
1,263✔
389
}
390

391
/**
392
 * apply_in_underlying_python(f, xs) (chapter 4's parser/"mce" group): calls
393
 * `f` with the arguments in linked-list `xs`. CSE's own implementation
394
 * (ParserBuiltins.apply_in_underlying_python in src/stdlib/parser.ts) pushes
395
 * onto its own control/stash for the CSE step loop to pick up later, rather
396
 * than returning a value — semantically incompatible with bridgeBuiltin's
397
 * generic path, which expects a builtin to return a Value synchronously (a
398
 * generic bridge attempt would silently no-op: it would run against a fresh,
399
 * throwaway Context whose control/stash nothing ever steps). Reimplemented
400
 * natively instead: walk the argument list exactly as permissively as CSE
401
 * does (any 2-element-list-shaped chain, terminated by anything that isn't,
402
 * not strictly requiring a None tail) and invoke `f` through the runtime's
403
 * own synchronous call trampoline (the same
404
 * one a TS module uses to call back into Python — see Py2JsRuntime.callSync).
405
 *
406
 * `parse`/`tokenize` need no such native treatment: both just transform a
407
 * string into CSE's tagged parse tree (transform() in src/stdlib/parser.ts),
408
 * built entirely out of 2-element cons cells, which the existing
409
 * toTagged/fromTagged round-trip already reconstructs correctly as nested
410
 * PyLists — so they go through the ordinary generic bridge above unchanged.
411
 */
412
function walkArgList(xs: PyValue): PyValue[] {
413
  const args: PyValue[] = [];
4✔
414
  // Cycle guard: set_tail/set_head (chapter 3's pair mutators) can build a
415
  // genuinely self-referential pair (`p = pair(1, 2); set_tail(p, p)`).
416
  // Without this, a circular argument list would spin forever, growing
417
  // `args` without bound until the process runs out of memory — visited
418
  // tracks list *nodes* by identity (not values), so only an actual cycle
419
  // back to a node already on this walk trips it, not e.g. two
420
  // separately-built pairs that happen to compare equal.
421
  const visited = new Set<object>();
4✔
422
  let current = xs;
4✔
423
  while (isPairShaped(current)) {
4✔
424
    if (visited.has(current)) {
4✔
425
      throw new Py2JsRuntimeError("RuntimeError", "circular list structure in arguments");
1✔
426
    }
427
    visited.add(current);
3✔
428
    args.push(current[0]);
3✔
429
    current = current[1];
3✔
430
  }
431
  return args;
3✔
432
}
433

434
function nativeApplyInUnderlyingPython(rt: Py2JsRuntime): PyFunction {
435
  const f = ((...args: PyValue[]) => rt.callSync(args[0], walkArgList(args[1]))) as PyFunction;
20✔
436
  f.pyName = "apply_in_underlying_python";
20✔
437
  f.pyArity = 2;
20✔
438
  f.pyBuiltin = true;
20✔
439
  f.pyNative = true;
20✔
440
  f.pyMinArgs = 2;
20✔
441
  return f;
20✔
442
}
443

444
/**
445
 * draw_data(value1, *values) (bridged as part of the LINKED_LISTS group, alongside
446
 * pair/llist/head/tail — available from chapter 2 onward, matching the documented signature
447
 * (docs/lib/linked_list.py) and CSE's identical chapter gating). Reimplemented natively rather than
448
 * through the generic bridge for two reasons:
449
 *
450
 *  - toTagged/toTaggedList (above) walk a pair/list's spine with no cycle guard — fine for every
451
 *    other bridged builtin, none of which is ever handed a genuinely self-referential structure in
452
 *    practice, but chapter 3+'s set_head/set_tail can build exactly that, and a user visualizing such
453
 *    a structure is precisely the case draw_data exists to handle gracefully (a "ref" node, not a
454
 *    hang). Passing the native args straight to the plugin's sendDrawing — which walks them via
455
 *    toDataVisualizerNodePy2Js (conductor/dataVisualizer/), with its own refs-based cycle guard —
456
 *    avoids that conversion entirely.
457
 *  - sendDrawing is synchronous and fire-and-forget (a channel send), so there is no async result to
458
 *    thread back through the generic bridge's Value round-trip in the first place.
459
 *
460
 * `plugin` is undefined below chapter 2 (bridgeStdlibGroups is never called with one there — see
461
 * Py2JsEvaluator.ts) and in every standalone/test run (runCodePy2Js et al. pass no dataVisualizer
462
 * option), in which case this is a silent no-op, exactly like context.dataVisualizer?.sendDrawing(...)
463
 * on the CSE side when no host conductor is attached.
464
 */
465
function nativeDrawData(plugin: BaseDataVisualizerRunnerPlugin<PyValue> | undefined): PyFunction {
466
  const f = ((...args: PyValue[]) => {
3,321✔
467
    if (args.length < 1) {
9✔
468
      throw new Py2JsRuntimeError(
1✔
469
        "TypeError",
470
        `draw_data() takes at least 1 argument (${args.length} given)`,
471
      );
472
    }
473
    plugin?.sendDrawing(args);
8✔
474
    return null;
8✔
475
  }) as PyFunction;
476
  f.pyName = "draw_data";
3,321✔
477
  f.pyArity = -1;
3,321✔
478
  f.pyBuiltin = true;
3,321✔
479
  f.pyNative = true;
3,321✔
480
  f.pyMinArgs = 1;
3,321✔
481
  return f;
3,321✔
482
}
483

484
/**
485
 * Bridge every builtin (and constant) of the given stdlib groups into py2js
486
 * native values. `source` is the program text, used by stdlib error
487
 * constructors for their (currently synthetic) location info.
488
 */
489
export function bridgeStdlibGroups(
19✔
490
  rt: Py2JsRuntime,
491
  groups: Group[],
492
  source: string,
493
  variant: number,
494
  dataVisualizer?: BaseDataVisualizerRunnerPlugin<PyValue>,
495
): Record<string, PyValue> {
496
  const context = new Context();
5,029✔
497
  context.variant = variant;
5,029✔
498
  const out: Record<string, PyValue> = {};
5,029✔
499
  for (const group of groups) {
5,029✔
500
    for (const [name, value] of group.builtins) {
17,188✔
501
      out[name] =
433,650✔
502
        value.type === "builtin"
433,650✔
503
          ? bridgeBuiltin(rt, name, value, context, source)
504
          : fromTagged(name, value);
505
    }
506
    // See nativeSetPairSlot/nativeStream/nativeDrawData's doc comments for why these groups'
507
    // primitives are reimplemented natively instead of left as the generic bridge produced above.
508
    if (group.name === GroupName.PAIRMUTATORS) {
17,188✔
509
      out.set_head = nativeSetPairSlot("set_head", 0, variant <= 2);
1,263✔
510
      out.set_tail = nativeSetPairSlot("set_tail", 1, variant <= 2);
1,263✔
511
    }
512
    if (group.name === GroupName.STREAMS) {
17,188✔
513
      out.stream = nativeStream(rt);
1,263✔
514
    }
515
    if (group.name === GroupName.MCE) {
17,188✔
516
      out.apply_in_underlying_python = nativeApplyInUnderlyingPython(rt);
20✔
517
    }
518
    // LINKED_LISTS (pair/llist/head/tail) is chapter 2's own "list library" — draw_data belongs
519
    // alongside it rather than in its own group (unlike the CSE machine, which uses a dedicated
520
    // DATA_VISUALIZER group), so it inherits the exact same chapter-2-onward availability.
521
    if (group.name === GroupName.LINKED_LISTS) {
17,188✔
522
      out.draw_data = nativeDrawData(dataVisualizer);
3,321✔
523
    }
524
  }
525
  return out;
5,029✔
526
}
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