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

source-academy / py-slang / 29545691509

17 Jul 2026 12:46AM UTC coverage: 81.011% (+2.8%) from 78.24%
29545691509

push

github

web-flow
Revive PVML-in-browser pathway: full 4-chapter support, bigint/complex numbers, spread/rest args, parser group (#236)

* revamping equality and identity

* wording improved

* taking care of the PVML aspects of this spec change

* Restrict the native Pynter pathway to Python §3, and free up EQP/NEQP for it

Pynter's target is Python (SICPy) §3 specifically (see pynter/README.md,
updated separately in the pynter repo) -- it has no runtime notion of
"chapter" to gate narrower/wider rules for §1/§2/§4, so it implements
§3's semantics unconditionally. Enforce that pathway boundary instead
of silently running other chapters with the wrong rules:

- runCodePvmlDetailed (pvml-runner.ts) and generateNativePynterTestCases
  (tests/utils.ts) now throw a clear error for variant !== 3.
- PyPvmlPynterEvaluator's hardcoded chapter (previously 4, inconsistent
  with Pynter's actual scope) is now 3.
- Every native-Pynter test call site across the suite now declares §3.
  Most are still-valid §3 programs nominally written for another
  chapter (linked-list/stream/pairmutator/list/parser-stdlib), so only
  the declared variant changes. One exception: stdlib.test.ts's
  "Chapter 1 Builtins" miscTests asserts §1-specific restrictions (bool/
  function excluded from ==/!=/ordering, list literals rejected) that
  are genuinely false at §3, not just untested there -- that sweep is
  removed rather than mislabeled.
- operator-conformance-pynter.test.ts's chapter loop is now just [3],
  matching its now-single-chapter scope.

Renumbered EQP/NEQP (added for is/is not's pointer-identity semantics,
distinct from ==/!=' structural equality) from 89/90 down to 85/86, so
PYNTER_OPCODE_MAX can become a single contiguous threshold (0x56) now
that Pynter implements them -- the prior gap (85-88 unimplemented,
89-90 implemented) couldn't be expressed as one cutoff without also
wrongly admitting the still-unimplemented FLOORDIVG/NEWITER/FOR_ITER.

Verified via yarn pynter:report against ... (continued)

3336 of 4409 branches covered (75.66%)

Branch coverage included in aggregate %.

1140 of 1309 new or added lines in 18 files covered. (87.09%)

7 existing lines in 2 files now uncovered.

7287 of 8704 relevant lines covered (83.72%)

80545.68 hits per line

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

79.75
/src/stdlib/misc.ts
1
import { ExprNS } from "../ast-types";
2
import { Context } from "../engines/cse/context";
3
import { handleRuntimeError } from "../engines/cse/error";
24✔
4
import {
5
  BigIntValue,
6
  BoolValue,
7
  BuiltinValue,
8
  ComplexValue,
9
  NoneValue,
10
  NumberValue,
11
  StringValue,
12
  Value,
13
} from "../engines/cse/stash";
14
import { displayOutput, receiveInput } from "../engines/cse/streams";
24✔
15
import { isNumeric } from "../engines/cse/utils";
24✔
16
import { TypeError, UserError } from "../errors";
24✔
17
import { PyComplexNumber } from "../types";
24✔
18
import { GroupName, minArgMap, toPythonString, Validate } from "./utils";
24✔
19

20
const miscBuiltins = new Map<string, BuiltinValue>();
24✔
21

22
export class MiscBuiltins {
24✔
23
  @Validate(1, 1, "arity", true)
24
  static arity(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue {
24✔
25
    const func = args[0];
95✔
26
    if (func.type !== "builtin" && func.type !== "closure") {
95✔
27
      handleRuntimeError(context, new TypeError(source, command, context, func.type, "function"));
7✔
28
    }
29
    if (func.type === "closure") {
88✔
30
      const variadicInstance = func.closure.node.parameters.findIndex(param => param.isStarred);
11✔
31
      if (variadicInstance !== -1) {
11✔
32
        return { type: "bigint", value: BigInt(variadicInstance) };
4✔
33
      }
34
      return { type: "bigint", value: BigInt(func.closure.node.parameters.length) };
7✔
35
    }
36
    return { type: "bigint", value: BigInt(func.minArgs) };
77✔
37
  }
38

39
  @Validate(null, 2, "complex", true)
40
  static complex(
24✔
41
    args: Value[],
42
    source: string,
43
    command: ExprNS.Call,
44
    context: Context,
45
  ): ComplexValue {
46
    if (args.length === 0) {
45✔
47
      return { type: "complex", value: new PyComplexNumber(0, 0) };
1✔
48
    }
49
    if (args.length == 1) {
44✔
50
      const val = args[0];
27✔
51
      if (
27✔
52
        val.type !== "bigint" &&
103✔
53
        val.type !== "number" &&
54
        val.type !== "bool" &&
55
        val.type !== "string" &&
56
        val.type !== "complex"
57
      ) {
58
        handleRuntimeError(context, new TypeError(source, command, context, val.type, "complex"));
3✔
59
      }
60
      return {
24✔
61
        type: "complex",
62
        value: PyComplexNumber.fromValue(context, source, command, val.value),
63
      };
64
    }
65
    const invalidType = args.filter(
17✔
66
      val =>
67
        val.type !== "bigint" &&
34✔
68
        val.type !== "number" &&
69
        val.type !== "bool" &&
70
        val.type !== "complex",
71
    );
72
    if (invalidType.length > 0) {
17✔
73
      handleRuntimeError(
3✔
74
        context,
75
        new TypeError(
76
          source,
77
          command,
78
          context,
79
          invalidType[0].type,
80
          "'int', 'float', 'bool' or 'complex'",
81
        ),
82
      );
83
    }
84
    const [real, imag] = args as (BigIntValue | NumberValue | BoolValue | ComplexValue)[];
14✔
85
    const realPart = PyComplexNumber.fromValue(context, source, command, real.value);
14✔
86
    const imagPart = PyComplexNumber.fromValue(context, source, command, imag.value);
14✔
87
    return { type: "complex", value: realPart.add(imagPart.mul(new PyComplexNumber(0, 1))) };
14✔
88
  }
89

90
  @Validate(1, 1, "real", true)
91
  static real(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue {
24✔
92
    const val = args[0];
×
93
    if (val.type !== "complex") {
×
94
      handleRuntimeError(context, new TypeError(source, command, context, val.type, "complex"));
×
95
    }
96
    return { type: "number", value: val.value.real };
×
97
  }
98

99
  @Validate(1, 1, "imag", true)
100
  static imag(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue {
24✔
101
    const val = args[0];
×
102
    if (val.type !== "complex") {
×
103
      handleRuntimeError(context, new TypeError(source, command, context, val.type, "complex"));
×
104
    }
105
    return { type: "number", value: val.value.imag };
×
106
  }
107

108
  @Validate(1, 1, "abs", false)
109
  static abs(
24✔
110
    args: Value[],
111
    source: string,
112
    command: ExprNS.Call,
113
    context: Context,
114
  ): BigIntValue | NumberValue {
115
    const x = args[0];
18✔
116
    switch (x.type) {
18!
117
      case "bigint": {
118
        const intVal = x.value;
14✔
119
        const result: bigint = intVal < 0 ? -intVal : intVal;
14✔
120
        return { type: "bigint", value: result };
14✔
121
      }
122
      case "number": {
123
        return { type: "number", value: Math.abs(x.value) };
2✔
124
      }
125
      case "complex": {
126
        // Calculate the modulus (absolute value) of a complex number.
127
        const real = x.value.real;
×
128
        const imag = x.value.imag;
×
129
        const modulus = Math.sqrt(real * real + imag * imag);
×
130
        return { type: "number", value: modulus };
×
131
      }
132
      default:
133
        handleRuntimeError(
2✔
134
          context,
135
          new TypeError(source, command, context, args[0].type, "float', 'int' or 'complex"),
136
        );
137
    }
138
  }
139

140
  @Validate(1, 1, "len", true)
141
  static len(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue {
24✔
142
    const val = args[0];
16✔
143
    if (val.type === "string" || val.type === "list") {
16✔
144
      // The spread operator is used to count the number of Unicode code points
145
      // in the string
146
      return { type: "bigint", value: BigInt([...val.value].length) };
9✔
147
    }
148
    handleRuntimeError(
7✔
149
      context,
150
      new TypeError(source, command, context, val.type, "object with length"),
151
    );
152
  }
153

154
  static error(args: Value[], _source: string, command: ExprNS.Call, context: Context): Value {
155
    const output = "Error: " + args.map(arg => toPythonString(arg)).join(" ") + "\n";
6✔
156
    handleRuntimeError(context, new UserError(output, command));
4✔
157
  }
158

159
  @Validate(2, null, "max", true)
160
  static max(args: Value[], source: string, command: ExprNS.Call, context: Context): Value {
24✔
161
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
7✔
162
      let maxIndex = 0;
7✔
163
      for (let i = 1; i < args.length; i++) {
7✔
164
        if (args[i].value > args[maxIndex].value) {
14✔
165
          maxIndex = i;
10✔
166
        }
167
      }
168
      return args[maxIndex];
7✔
169
    }
170
    if (isNumeric(args[0])) {
×
171
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
172
      handleRuntimeError(
×
173
        context,
174
        new TypeError(source, command, context, invalidType.type, "int' or 'float"),
175
      );
176
    } else if (args[0].type === "string") {
×
177
      const invalidType = args.find(arg => arg.type !== "string")!;
×
178
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type, "str"));
×
179
    } else {
180
      handleRuntimeError(
×
181
        context,
182
        new TypeError(source, command, context, args[0].type, "int', 'float' or 'str'"),
183
      );
184
    }
185
  }
186

187
  @Validate(2, null, "min", true)
188
  static min(args: Value[], source: string, command: ExprNS.Call, context: Context): Value {
24✔
189
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
2!
190
      let minIndex = 0;
2✔
191
      for (let i = 1; i < args.length; i++) {
2✔
192
        if (args[i].value < args[minIndex].value) {
4✔
193
          minIndex = i;
2✔
194
        }
195
      }
196
      return args[minIndex];
2✔
197
    }
198
    if (isNumeric(args[0])) {
×
199
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
200
      handleRuntimeError(
×
201
        context,
202
        new TypeError(source, command, context, invalidType.type, "int' or 'float"),
203
      );
204
    } else if (args[0].type === "string") {
×
205
      const invalidType = args.find(arg => arg.type !== "string")!;
×
206
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type, "str"));
×
207
    } else {
208
      handleRuntimeError(
×
209
        context,
210
        new TypeError(source, command, context, args[0].type, "int', 'float' or 'str'"),
211
      );
212
    }
213
  }
214

215
  @Validate(null, 0, "random_random", true)
216
  static random_random(
24✔
217
    _args: Value[],
218
    _source: string,
219
    _command: ExprNS.Call,
220
    _context: Context,
221
  ): NumberValue {
222
    const result = Math.random();
×
223
    return { type: "number", value: result };
×
224
  }
225

226
  @Validate(1, 2, "round", true)
227
  static round(
24✔
228
    args: Value[],
229
    source: string,
230
    command: ExprNS.Call,
231
    context: Context,
232
  ): NumberValue | BigIntValue {
233
    const numArg = args[0];
15✔
234
    if (!isNumeric(numArg)) {
15✔
235
      handleRuntimeError(
2✔
236
        context,
237
        new TypeError(source, command, context, numArg.type, "float' or 'int"),
238
      );
239
    }
240

241
    let ndigitsArg: BigIntValue = { type: "bigint", value: BigInt(0) };
13✔
242
    if (args.length === 2 && args[1].type !== "none") {
13✔
243
      if (args[1].type !== "bigint") {
7✔
244
        handleRuntimeError(context, new TypeError(source, command, context, args[1].type, "int"));
1✔
245
      }
246
      ndigitsArg = args[1];
6✔
247
    } else {
248
      const shifted = Intl.NumberFormat("en-US", {
6✔
249
        roundingMode: "halfEven",
250
        useGrouping: false,
251
        maximumFractionDigits: 0,
252
      } as Intl.NumberFormatOptions).format(numArg.value);
253
      return { type: "bigint", value: BigInt(shifted) };
6✔
254
    }
255

256
    if (numArg.type === "number") {
6✔
257
      const numberValue: number = numArg.value;
4✔
258
      if (ndigitsArg.value >= 0) {
4✔
259
        const shifted = Intl.NumberFormat("en-US", {
3✔
260
          roundingMode: "halfEven",
261
          useGrouping: false,
262
          maximumFractionDigits: Number(ndigitsArg.value),
263
        } as Intl.NumberFormatOptions).format(numberValue);
264
        return { type: "number", value: Number(shifted) };
3✔
265
      } else {
266
        const shifted = Intl.NumberFormat("en-US", {
1✔
267
          roundingMode: "halfEven",
268
          useGrouping: false,
269
          maximumFractionDigits: 0,
270
        } as Intl.NumberFormatOptions).format(numArg.value / 10 ** -Number(ndigitsArg.value));
271
        return { type: "number", value: Number(shifted) * 10 ** -Number(ndigitsArg.value) };
1✔
272
      }
273
    } else {
274
      if (ndigitsArg.value >= 0) {
2!
275
        return numArg;
2✔
276
      } else {
277
        const shifted = Intl.NumberFormat("en-US", {
×
278
          roundingMode: "halfEven",
279
          useGrouping: false,
280
          maximumFractionDigits: 0,
281
        } as Intl.NumberFormatOptions).format(
282
          Number(numArg.value) / 10 ** -Number(ndigitsArg.value),
283
        );
284
        return { type: "bigint", value: BigInt(shifted) * 10n ** -ndigitsArg.value };
×
285
      }
286
    }
287
  }
288

289
  @Validate(null, 0, "time_time", true)
290
  static time_time(
24✔
291
    _args: Value[],
292
    _source: string,
293
    _command: ExprNS.Call,
294
    _context: Context,
295
  ): NumberValue {
296
    // Python's time.time() is documented as seconds since the epoch, not milliseconds — divide
297
    // Date.now()'s milliseconds down to match.
NEW
298
    return { type: "number", value: Date.now() / 1000 };
×
299
  }
300

301
  @Validate(1, 1, "is_none", true)
302
  static is_none(
24✔
303
    args: Value[],
304
    _source: string,
305
    _command: ExprNS.Call,
306
    _context: Context,
307
  ): BoolValue {
308
    const obj = args[0];
462✔
309
    return { type: "bool", value: obj.type === "none" };
462✔
310
  }
311

312
  @Validate(1, 1, "is_float", true)
313
  static is_float(
24✔
314
    args: Value[],
315
    _source: string,
316
    _command: ExprNS.Call,
317
    _context: Context,
318
  ): BoolValue {
319
    const obj = args[0];
9✔
320
    return { type: "bool", value: obj.type === "number" };
9✔
321
  }
322

323
  @Validate(1, 1, "is_string", true)
324
  static is_string(
24✔
325
    args: Value[],
326
    _source: string,
327
    _command: ExprNS.Call,
328
    _context: Context,
329
  ): BoolValue {
330
    const obj = args[0];
18✔
331
    return { type: "bool", value: obj.type === "string" };
18✔
332
  }
333

334
  @Validate(1, 1, "is_boolean", true)
335
  static is_boolean(
24✔
336
    args: Value[],
337
    _source: string,
338
    _command: ExprNS.Call,
339
    _context: Context,
340
  ): BoolValue {
341
    const obj = args[0];
10✔
342
    return { type: "bool", value: obj.type === "bool" };
10✔
343
  }
344

345
  @Validate(1, 1, "is_complex", true)
346
  static is_complex(
24✔
347
    args: Value[],
348
    _source: string,
349
    _command: ExprNS.Call,
350
    _context: Context,
351
  ): BoolValue {
352
    const obj = args[0];
×
353
    return { type: "bool", value: obj.type === "complex" };
×
354
  }
355

356
  @Validate(1, 1, "is_number", true)
357
  static is_number(
24✔
358
    args: Value[],
359
    _source: string,
360
    _command: ExprNS.Call,
361
    _context: Context,
362
  ): BoolValue {
363
    // Mirrors Scheme's `number?`: true for any number in the numeric tower
364
    // (integer, float or complex), but not for booleans.
365
    const obj = args[0];
9✔
366
    return {
9✔
367
      type: "bool",
368
      value: obj.type === "bigint" || obj.type === "number" || obj.type === "complex",
23✔
369
    };
370
  }
371

372
  @Validate(1, 1, "is_integer", true)
373
  static is_integer(
24✔
374
    args: Value[],
375
    _source: string,
376
    _command: ExprNS.Call,
377
    _context: Context,
378
  ): BoolValue {
379
    const obj = args[0];
9✔
380
    return { type: "bool", value: obj.type === "bigint" };
9✔
381
  }
382

383
  @Validate(1, 1, "is_function", true)
384
  static is_function(
24✔
385
    args: Value[],
386
    _source: string,
387
    _command: ExprNS.Call,
388
    _context: Context,
389
  ): BoolValue {
390
    const obj = args[0];
314✔
391
    return {
314✔
392
      type: "bool",
393
      value: obj.type === "function" || obj.type === "closure" || obj.type === "builtin",
719✔
394
    };
395
  }
396

397
  @Validate(0, 1, "input", true)
398
  static async input(
24✔
399
    args: Value[],
400
    _source: string,
401
    _command: ExprNS.Call,
402
    context: Context,
403
  ): Promise<Value> {
404
    const prompt = args.length > 0 ? toPythonString(args[0]) : undefined;
2✔
405
    if (prompt !== undefined) {
2✔
406
      // Matches CPython: input(prompt) writes the prompt to stdout (no trailing newline)
407
      // before blocking on stdin.
408
      await displayOutput(context, prompt);
1✔
409
    }
410
    const userInput = await receiveInput(context, prompt);
2✔
411
    return { type: "string", value: userInput };
2✔
412
  }
413

414
  static async print(
415
    args: Value[],
416
    _source: string,
417
    _command: ExprNS.Call,
418
    context: Context,
419
  ): Promise<Value> {
420
    const output = args.map(arg => toPythonString(arg)).join(" ") + "\n";
169✔
421
    await displayOutput(context, output);
154✔
422
    return { type: "none" };
154✔
423
  }
424
  static str(
425
    args: Value[],
426
    _source: string,
427
    _command: ExprNS.Call,
428
    _context: Context,
429
  ): StringValue {
430
    if (args.length === 0) {
11!
431
      return { type: "string", value: "" };
×
432
    }
433
    const obj = args[0];
11✔
434
    const result = toPythonString(obj);
11✔
435
    return { type: "string", value: result };
11✔
436
  }
437
  @Validate(1, 1, "repr", true)
438
  static repr(
24✔
439
    args: Value[],
440
    _source: string,
441
    _command: ExprNS.Call,
442
    _context: Context,
443
  ): StringValue {
444
    const obj = args[0];
15✔
445
    const result = toPythonString(obj, true);
15✔
446
    return { type: "string", value: result };
15✔
447
  }
448

449
  // Python's `breakpoint()` drops into a debugger; the CSE machine has no interactive debugger,
450
  // so as a value it is simply a no-op that yields `None`. A zero-arg call resolving to this
451
  // builtin is recognised by the CSE machine's step generator (interpreter.ts) as a breakpoint —
452
  // it records the step so the host's breakpoint-navigation controls can jump to it, then this
453
  // entry runs like any other builtin call. This mirrors the stepper's `breakpoint` entry
454
  // (src/conductor/stepper/builtins.ts), which does the analogous thing for the substitution model.
455
  static breakpoint(
456
    _args: Value[],
457
    _source: string,
458
    _command: ExprNS.Call,
459
    _context: Context,
460
  ): NoneValue {
461
    return { type: "none" };
9✔
462
  }
463
}
464
for (const builtin of Object.getOwnPropertyNames(MiscBuiltins)) {
24✔
465
  if (
672✔
466
    typeof MiscBuiltins[builtin as keyof typeof MiscBuiltins] === "function" &&
1,272✔
467
    !builtin.startsWith("_")
468
  ) {
469
    miscBuiltins.set(builtin, {
600✔
470
      type: "builtin",
471
      func: MiscBuiltins[builtin as keyof typeof MiscBuiltins] as BuiltinValue["func"],
472
      name: builtin,
473
      minArgs: minArgMap.get(builtin) || 0,
792✔
474
    });
475
  }
476
}
477

478
export default {
24✔
479
  name: GroupName.MISC,
480
  prelude: "",
481
  builtins: miscBuiltins,
482
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc