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

source-academy / py-slang / 29631471367

18 Jul 2026 04:59AM UTC coverage: 80.964% (+0.3%) from 80.647%
29631471367

push

github

web-flow
Overhaul of "wrong type" messages: operators and builtin functions, CSE machine and PVML-in-browser (#273)

* Fix operand-type names leaking JS internals into TypeError messages

Both engines' "unsupported operand type(s)" errors were showing
internal representation names instead of Python type names for
several types -- reported for PVML (`1 + None` said "'null' and
'bigint'" instead of "'NoneType' and 'int'"), but the same class of
bug turned out to affect CSE too.

PVML (src/engines/pvml/types.ts): `PVMLType`'s string values were the
engine's own internal tags (JS `typeof`/`null`/discriminant strings),
used directly in error-message interpolation. Corrected every member
to the name real Python (and the CSE machine's own `typeTranslator`)
would show: NUMBER "number"->"float", BIGINT "bigint"->"int", BOOLEAN
"boolean"->"bool", STRING "string"->"str", NULL "null"->"NoneType",
ARRAY "array"->"list", CLOSURE "closure"->"function", PRIMITIVE
"primitive"->"builtin_function_or_method". COMPLEX was already
correct. UNDEFINED/ITERATOR are internal-only sentinels that can't
reach one of these messages (verified: any local/free-variable read
that would yield JS `undefined` is intercepted by
UnboundLocalError/FreeVariableUnboundError first), left as-is.

CSE (src/engines/cse/types.ts, src/errors/errors.ts): tracing the
equivalent PVML fix's own reference point -- CSE's `typeTranslator` --
surfaced two real, independent bugs of the same kind:
- `typeTranslator`'s switch had no `"list"` case, so `[1,2] + 1`
  reported "'unknown' and 'int'" instead of "'list' and 'int'".
- `src/errors/errors.ts` carried its own separate copy of
  `typeTranslator` (comment: "Local copy to avoid circular import from
  utils"), missing both the `"list"` case above and the `"builtin"`
  case the canonical copy already had -- and the circular-import
  concern the comment cites no longer applies (this file already
  imports `operatorTranslator` from the exact same module). Deleted
  the... (continued)

3423 of 4524 branches covered (75.66%)

Branch coverage included in aggregate %.

173 of 241 new or added lines in 16 files covered. (71.78%)

5 existing lines in 2 files now uncovered.

7508 of 8977 relevant lines covered (83.64%)

80734.93 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";
33✔
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";
33✔
15
import { isNumeric } from "../engines/cse/utils";
33✔
16
import { TypeError, UserError } from "../errors";
33✔
17
import { PyComplexNumber } from "../types";
33✔
18
import { GroupName, minArgMap, toPythonString, Validate } from "./utils";
33✔
19

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

22
export class MiscBuiltins {
33✔
23
  @Validate(1, 1, "arity", true)
24
  static arity(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue {
33✔
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));
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(
33✔
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));
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(context, new TypeError(source, command, context, invalidType[0].type));
3✔
74
    }
75
    const [real, imag] = args as (BigIntValue | NumberValue | BoolValue | ComplexValue)[];
14✔
76
    const realPart = PyComplexNumber.fromValue(context, source, command, real.value);
14✔
77
    const imagPart = PyComplexNumber.fromValue(context, source, command, imag.value);
14✔
78
    return { type: "complex", value: realPart.add(imagPart.mul(new PyComplexNumber(0, 1))) };
14✔
79
  }
80

81
  @Validate(1, 1, "real", true)
82
  static real(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue {
33✔
83
    const val = args[0];
×
84
    if (val.type !== "complex") {
×
NEW
85
      handleRuntimeError(context, new TypeError(source, command, context, val.type));
×
86
    }
87
    return { type: "number", value: val.value.real };
×
88
  }
89

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

99
  @Validate(1, 1, "abs", false)
100
  static abs(
33✔
101
    args: Value[],
102
    source: string,
103
    command: ExprNS.Call,
104
    context: Context,
105
  ): BigIntValue | NumberValue {
106
    const x = args[0];
18✔
107
    switch (x.type) {
18!
108
      case "bigint": {
109
        const intVal = x.value;
14✔
110
        const result: bigint = intVal < 0 ? -intVal : intVal;
14✔
111
        return { type: "bigint", value: result };
14✔
112
      }
113
      case "number": {
114
        return { type: "number", value: Math.abs(x.value) };
2✔
115
      }
116
      case "complex": {
117
        // Calculate the modulus (absolute value) of a complex number.
118
        const real = x.value.real;
×
119
        const imag = x.value.imag;
×
120
        const modulus = Math.sqrt(real * real + imag * imag);
×
121
        return { type: "number", value: modulus };
×
122
      }
123
      default:
124
        handleRuntimeError(context, new TypeError(source, command, context, args[0].type));
2✔
125
    }
126
  }
127

128
  @Validate(1, 1, "len", true)
129
  static len(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue {
33✔
130
    const val = args[0];
16✔
131
    if (val.type === "string" || val.type === "list") {
16✔
132
      // The spread operator is used to count the number of Unicode code points
133
      // in the string
134
      return { type: "bigint", value: BigInt([...val.value].length) };
9✔
135
    }
136
    handleRuntimeError(context, new TypeError(source, command, context, val.type));
7✔
137
  }
138

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

144
  @Validate(2, null, "max", true)
145
  static max(args: Value[], source: string, command: ExprNS.Call, context: Context): Value {
33✔
146
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
7✔
147
      let maxIndex = 0;
7✔
148
      for (let i = 1; i < args.length; i++) {
7✔
149
        if (args[i].value > args[maxIndex].value) {
14✔
150
          maxIndex = i;
10✔
151
        }
152
      }
153
      return args[maxIndex];
7✔
154
    }
155
    if (isNumeric(args[0])) {
×
156
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
NEW
157
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
UNCOV
158
    } else if (args[0].type === "string") {
×
159
      const invalidType = args.find(arg => arg.type !== "string")!;
×
NEW
160
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
161
    } else {
NEW
162
      handleRuntimeError(context, new TypeError(source, command, context, args[0].type));
×
163
    }
164
  }
165

166
  @Validate(2, null, "min", true)
167
  static min(args: Value[], source: string, command: ExprNS.Call, context: Context): Value {
33✔
168
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
2!
169
      let minIndex = 0;
2✔
170
      for (let i = 1; i < args.length; i++) {
2✔
171
        if (args[i].value < args[minIndex].value) {
4✔
172
          minIndex = i;
2✔
173
        }
174
      }
175
      return args[minIndex];
2✔
176
    }
177
    if (isNumeric(args[0])) {
×
178
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
NEW
179
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
UNCOV
180
    } else if (args[0].type === "string") {
×
181
      const invalidType = args.find(arg => arg.type !== "string")!;
×
NEW
182
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
183
    } else {
NEW
184
      handleRuntimeError(context, new TypeError(source, command, context, args[0].type));
×
185
    }
186
  }
187

188
  @Validate(null, 0, "random_random", true)
189
  static random_random(
33✔
190
    _args: Value[],
191
    _source: string,
192
    _command: ExprNS.Call,
193
    _context: Context,
194
  ): NumberValue {
195
    const result = Math.random();
×
196
    return { type: "number", value: result };
×
197
  }
198

199
  @Validate(1, 2, "round", true)
200
  static round(
33✔
201
    args: Value[],
202
    source: string,
203
    command: ExprNS.Call,
204
    context: Context,
205
  ): NumberValue | BigIntValue {
206
    const numArg = args[0];
15✔
207
    if (!isNumeric(numArg)) {
15✔
208
      handleRuntimeError(context, new TypeError(source, command, context, numArg.type));
2✔
209
    }
210

211
    let ndigitsArg: BigIntValue = { type: "bigint", value: BigInt(0) };
13✔
212
    if (args.length === 2 && args[1].type !== "none") {
13✔
213
      if (args[1].type !== "bigint") {
7✔
214
        handleRuntimeError(context, new TypeError(source, command, context, args[1].type));
1✔
215
      }
216
      ndigitsArg = args[1];
6✔
217
    } else {
218
      const shifted = Intl.NumberFormat("en-US", {
6✔
219
        roundingMode: "halfEven",
220
        useGrouping: false,
221
        maximumFractionDigits: 0,
222
      } as Intl.NumberFormatOptions).format(numArg.value);
223
      return { type: "bigint", value: BigInt(shifted) };
6✔
224
    }
225

226
    if (numArg.type === "number") {
6✔
227
      const numberValue: number = numArg.value;
4✔
228
      if (ndigitsArg.value >= 0) {
4✔
229
        const shifted = Intl.NumberFormat("en-US", {
3✔
230
          roundingMode: "halfEven",
231
          useGrouping: false,
232
          maximumFractionDigits: Number(ndigitsArg.value),
233
        } as Intl.NumberFormatOptions).format(numberValue);
234
        return { type: "number", value: Number(shifted) };
3✔
235
      } else {
236
        const shifted = Intl.NumberFormat("en-US", {
1✔
237
          roundingMode: "halfEven",
238
          useGrouping: false,
239
          maximumFractionDigits: 0,
240
        } as Intl.NumberFormatOptions).format(numArg.value / 10 ** -Number(ndigitsArg.value));
241
        return { type: "number", value: Number(shifted) * 10 ** -Number(ndigitsArg.value) };
1✔
242
      }
243
    } else {
244
      if (ndigitsArg.value >= 0) {
2!
245
        return numArg;
2✔
246
      } else {
247
        const shifted = Intl.NumberFormat("en-US", {
×
248
          roundingMode: "halfEven",
249
          useGrouping: false,
250
          maximumFractionDigits: 0,
251
        } as Intl.NumberFormatOptions).format(
252
          Number(numArg.value) / 10 ** -Number(ndigitsArg.value),
253
        );
254
        return { type: "bigint", value: BigInt(shifted) * 10n ** -ndigitsArg.value };
×
255
      }
256
    }
257
  }
258

259
  @Validate(null, 0, "time_time", true)
260
  static time_time(
33✔
261
    _args: Value[],
262
    _source: string,
263
    _command: ExprNS.Call,
264
    _context: Context,
265
  ): NumberValue {
266
    // Python's time.time() is documented as seconds since the epoch, not milliseconds — divide
267
    // Date.now()'s milliseconds down to match.
268
    return { type: "number", value: Date.now() / 1000 };
×
269
  }
270

271
  @Validate(1, 1, "is_none", true)
272
  static is_none(
33✔
273
    args: Value[],
274
    _source: string,
275
    _command: ExprNS.Call,
276
    _context: Context,
277
  ): BoolValue {
278
    const obj = args[0];
462✔
279
    return { type: "bool", value: obj.type === "none" };
462✔
280
  }
281

282
  @Validate(1, 1, "is_float", true)
283
  static is_float(
33✔
284
    args: Value[],
285
    _source: string,
286
    _command: ExprNS.Call,
287
    _context: Context,
288
  ): BoolValue {
289
    const obj = args[0];
9✔
290
    return { type: "bool", value: obj.type === "number" };
9✔
291
  }
292

293
  @Validate(1, 1, "is_string", true)
294
  static is_string(
33✔
295
    args: Value[],
296
    _source: string,
297
    _command: ExprNS.Call,
298
    _context: Context,
299
  ): BoolValue {
300
    const obj = args[0];
18✔
301
    return { type: "bool", value: obj.type === "string" };
18✔
302
  }
303

304
  @Validate(1, 1, "is_boolean", true)
305
  static is_boolean(
33✔
306
    args: Value[],
307
    _source: string,
308
    _command: ExprNS.Call,
309
    _context: Context,
310
  ): BoolValue {
311
    const obj = args[0];
10✔
312
    return { type: "bool", value: obj.type === "bool" };
10✔
313
  }
314

315
  @Validate(1, 1, "is_complex", true)
316
  static is_complex(
33✔
317
    args: Value[],
318
    _source: string,
319
    _command: ExprNS.Call,
320
    _context: Context,
321
  ): BoolValue {
322
    const obj = args[0];
×
323
    return { type: "bool", value: obj.type === "complex" };
×
324
  }
325

326
  @Validate(1, 1, "is_number", true)
327
  static is_number(
33✔
328
    args: Value[],
329
    _source: string,
330
    _command: ExprNS.Call,
331
    _context: Context,
332
  ): BoolValue {
333
    // Mirrors Scheme's `number?`: true for any number in the numeric tower
334
    // (integer, float or complex), but not for booleans.
335
    const obj = args[0];
9✔
336
    return {
9✔
337
      type: "bool",
338
      value: obj.type === "bigint" || obj.type === "number" || obj.type === "complex",
23✔
339
    };
340
  }
341

342
  @Validate(1, 1, "is_integer", true)
343
  static is_integer(
33✔
344
    args: Value[],
345
    _source: string,
346
    _command: ExprNS.Call,
347
    _context: Context,
348
  ): BoolValue {
349
    const obj = args[0];
9✔
350
    return { type: "bool", value: obj.type === "bigint" };
9✔
351
  }
352

353
  @Validate(1, 1, "is_function", true)
354
  static is_function(
33✔
355
    args: Value[],
356
    _source: string,
357
    _command: ExprNS.Call,
358
    _context: Context,
359
  ): BoolValue {
360
    const obj = args[0];
314✔
361
    return {
314✔
362
      type: "bool",
363
      value: obj.type === "function" || obj.type === "closure" || obj.type === "builtin",
719✔
364
    };
365
  }
366

367
  @Validate(0, 1, "input", true)
368
  static async input(
33✔
369
    args: Value[],
370
    _source: string,
371
    _command: ExprNS.Call,
372
    context: Context,
373
  ): Promise<Value> {
374
    const prompt = args.length > 0 ? toPythonString(args[0]) : undefined;
2✔
375
    if (prompt !== undefined) {
2✔
376
      // Matches CPython: input(prompt) writes the prompt to stdout (no trailing newline)
377
      // before blocking on stdin.
378
      await displayOutput(context, prompt);
1✔
379
    }
380
    const userInput = await receiveInput(context, prompt);
2✔
381
    return { type: "string", value: userInput };
2✔
382
  }
383

384
  static async print(
385
    args: Value[],
386
    _source: string,
387
    _command: ExprNS.Call,
388
    context: Context,
389
  ): Promise<Value> {
390
    const output = args.map(arg => toPythonString(arg)).join(" ") + "\n";
169✔
391
    await displayOutput(context, output);
154✔
392
    return { type: "none" };
154✔
393
  }
394
  static str(
395
    args: Value[],
396
    _source: string,
397
    _command: ExprNS.Call,
398
    _context: Context,
399
  ): StringValue {
400
    if (args.length === 0) {
11!
401
      return { type: "string", value: "" };
×
402
    }
403
    const obj = args[0];
11✔
404
    const result = toPythonString(obj);
11✔
405
    return { type: "string", value: result };
11✔
406
  }
407
  @Validate(1, 1, "repr", true)
408
  static repr(
33✔
409
    args: Value[],
410
    _source: string,
411
    _command: ExprNS.Call,
412
    _context: Context,
413
  ): StringValue {
414
    const obj = args[0];
15✔
415
    const result = toPythonString(obj, true);
15✔
416
    return { type: "string", value: result };
15✔
417
  }
418

419
  // Python's `breakpoint()` drops into a debugger; the CSE machine has no interactive debugger,
420
  // so as a value it is simply a no-op that yields `None`. A zero-arg call resolving to this
421
  // builtin is recognised by the CSE machine's step generator (interpreter.ts) as a breakpoint —
422
  // it records the step so the host's breakpoint-navigation controls can jump to it, then this
423
  // entry runs like any other builtin call. This mirrors the stepper's `breakpoint` entry
424
  // (src/conductor/stepper/builtins.ts), which does the analogous thing for the substitution model.
425
  static breakpoint(
426
    _args: Value[],
427
    _source: string,
428
    _command: ExprNS.Call,
429
    _context: Context,
430
  ): NoneValue {
431
    return { type: "none" };
9✔
432
  }
433
}
434
for (const builtin of Object.getOwnPropertyNames(MiscBuiltins)) {
33✔
435
  if (
924✔
436
    typeof MiscBuiltins[builtin as keyof typeof MiscBuiltins] === "function" &&
1,749✔
437
    !builtin.startsWith("_")
438
  ) {
439
    miscBuiltins.set(builtin, {
825✔
440
      type: "builtin",
441
      func: MiscBuiltins[builtin as keyof typeof MiscBuiltins] as BuiltinValue["func"],
442
      name: builtin,
443
      minArgs: minArgMap.get(builtin) || 0,
1,089✔
444
    });
445
  }
446
}
447

448
export default {
33✔
449
  name: GroupName.MISC,
450
  prelude: "",
451
  builtins: miscBuiltins,
452
};
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