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

source-academy / py-slang / 29965288246

22 Jul 2026 11:10PM UTC coverage: 86.427% (-0.001%) from 86.428%
29965288246

Pull #322

github

web-flow
Merge 386e89a1b into 84c1091c6
Pull Request #322: Add set_timeout/clear_all_timeout to MISC (py2js)

4390 of 5467 branches covered (80.3%)

Branch coverage included in aggregate %.

19 of 21 new or added lines in 2 files covered. (90.48%)

9466 of 10565 relevant lines covered (89.6%)

178583.34 hits per line

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

88.38
/src/stdlib/misc.ts
1
import { ExprNS } from "../ast-types";
2
import { Context } from "../engines/cse/context";
3
import { handleRuntimeError } from "../engines/cse/error";
51✔
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";
51✔
15
import { isNumeric } from "../engines/cse/utils";
51✔
16
import { TypeError, UserError } from "../errors";
51✔
17
import { PyComplexNumber } from "../types";
51✔
18
import { GroupName, minArgMap, toPythonString, Validate } from "./utils";
51✔
19

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

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

39
  @Validate(null, 2, "complex", true)
40
  static complex(
51✔
41
    args: Value[],
42
    source: string,
43
    command: ExprNS.Call,
44
    context: Context,
45
  ): ComplexValue {
46
    if (args.length === 0) {
106✔
47
      return { type: "complex", value: new PyComplexNumber(0, 0) };
5✔
48
    }
49
    if (args.length == 1) {
101✔
50
      const val = args[0];
57✔
51
      if (
57✔
52
        val.type !== "bigint" &&
213✔
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));
13✔
59
      }
60
      return {
44✔
61
        type: "complex",
62
        value: PyComplexNumber.fromValue(context, source, command, val.value),
63
      };
64
    }
65
    const invalidType = args.filter(
44✔
66
      val =>
67
        val.type !== "bigint" &&
88✔
68
        val.type !== "number" &&
69
        val.type !== "bool" &&
70
        val.type !== "complex",
71
    );
72
    if (invalidType.length > 0) {
44✔
73
      handleRuntimeError(context, new TypeError(source, command, context, invalidType[0].type));
3✔
74
    }
75
    const [real, imag] = args as (BigIntValue | NumberValue | BoolValue | ComplexValue)[];
41✔
76
    const realPart = PyComplexNumber.fromValue(context, source, command, real.value);
41✔
77
    const imagPart = PyComplexNumber.fromValue(context, source, command, imag.value);
41✔
78
    return { type: "complex", value: realPart.add(imagPart.mul(new PyComplexNumber(0, 1))) };
41✔
79
  }
80

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

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

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

128
  @Validate(1, 1, "len", true)
129
  static len(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue {
51✔
130
    const val = args[0];
48✔
131
    if (val.type === "string" || val.type === "list") {
48✔
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) };
17✔
135
    }
136
    handleRuntimeError(context, new TypeError(source, command, context, val.type));
31✔
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";
70✔
141
    handleRuntimeError(context, new UserError(output, command));
56✔
142
  }
143

144
  @Validate(2, null, "max", true)
145
  static max(args: Value[], source: string, command: ExprNS.Call, context: Context): Value {
51✔
146
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
25✔
147
      let maxIndex = 0;
25✔
148
      for (let i = 1; i < args.length; i++) {
25✔
149
        if (args[i].value > args[maxIndex].value) {
32✔
150
          maxIndex = i;
16✔
151
        }
152
      }
153
      return args[maxIndex];
25✔
154
    }
155
    if (isNumeric(args[0])) {
×
156
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
157
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
158
    } else if (args[0].type === "string") {
×
159
      const invalidType = args.find(arg => arg.type !== "string")!;
×
160
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
161
    } else {
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 {
51✔
168
    if (args.every(isNumeric) || args.every(arg => arg.type === "string")) {
18!
169
      let minIndex = 0;
18✔
170
      for (let i = 1; i < args.length; i++) {
18✔
171
        if (args[i].value < args[minIndex].value) {
20✔
172
          minIndex = i;
6✔
173
        }
174
      }
175
      return args[minIndex];
18✔
176
    }
177
    if (isNumeric(args[0])) {
×
178
      const invalidType = args.find(arg => !isNumeric(arg))!;
×
179
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
180
    } else if (args[0].type === "string") {
×
181
      const invalidType = args.find(arg => arg.type !== "string")!;
×
182
      handleRuntimeError(context, new TypeError(source, command, context, invalidType.type));
×
183
    } else {
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(
51✔
190
    _args: Value[],
191
    _source: string,
192
    _command: ExprNS.Call,
193
    _context: Context,
194
  ): NumberValue {
195
    const result = Math.random();
4✔
196
    return { type: "number", value: result };
4✔
197
  }
198

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

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

226
    if (numArg.type === "number") {
16✔
227
      const numberValue: number = numArg.value;
10✔
228
      if (ndigitsArg.value >= 0) {
10✔
229
        const shifted = Intl.NumberFormat("en-US", {
9✔
230
          roundingMode: "halfEven",
231
          useGrouping: false,
232
          maximumFractionDigits: Number(ndigitsArg.value),
233
        } as Intl.NumberFormatOptions).format(numberValue);
234
        return { type: "number", value: Number(shifted) };
9✔
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) {
6!
245
        return numArg;
6✔
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(
51✔
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 };
4✔
269
  }
270

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

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

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

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

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

326
  @Validate(1, 1, "is_number", true)
327
  static is_number(
51✔
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];
39✔
336
    return {
39✔
337
      type: "bool",
338
      value: obj.type === "bigint" || obj.type === "number" || obj.type === "complex",
101✔
339
    };
340
  }
341

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

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

367
  @Validate(0, 1, "input", true)
368
  static async input(
51✔
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";
825✔
391
    await displayOutput(context, output);
801✔
392
    return { type: "none" };
801✔
393
  }
394
  static str(
395
    args: Value[],
396
    _source: string,
397
    _command: ExprNS.Call,
398
    _context: Context,
399
  ): StringValue {
400
    if (args.length === 0) {
66✔
401
      return { type: "string", value: "" };
4✔
402
    }
403
    const obj = args[0];
62✔
404
    const result = toPythonString(obj);
62✔
405
    return { type: "string", value: result };
62✔
406
  }
407
  @Validate(1, 1, "repr", true)
408
  static repr(
51✔
409
    args: Value[],
410
    _source: string,
411
    _command: ExprNS.Call,
412
    _context: Context,
413
  ): StringValue {
414
    const obj = args[0];
49✔
415
    const result = toPythonString(obj, true);
49✔
416
    return { type: "string", value: result };
49✔
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" };
59✔
432
  }
433

434
  // set_timeout(f, t): schedules f to be called with no arguments after t
435
  // milliseconds, without blocking the calling code (source-academy/py-slang#311).
436
  // Mirrors the sound_matrix module's existing JS set_timeout(f, t) (same
437
  // signature and units), moved into the language itself rather than staying
438
  // module-gated, since it's a capability of the evaluator, not of any one
439
  // module.
440
  //
441
  // Not yet implemented on the CSE machine: f must still be callable when the
442
  // real timer fires, which may be well after this evaluate() run has
443
  // finished and context.control/context.stash have been torn down — the CSE
444
  // machine has no way to resume a closure call from cold today (unlike
445
  // PVML's invokeValueAsync, built for sound_matrix's own set_timeout, or
446
  // py2js, where a compiled Python function already is a plain JS closure and
447
  // needs no re-entry machinery at all — see
448
  // src/engines/py2js/runtime.ts). Implemented only by py2js for now; this
449
  // entry exists so the name resolves consistently across engines.
450
  @Validate(2, 2, "set_timeout", true)
451
  static set_timeout(
51✔
452
    _args: Value[],
453
    _source: string,
454
    _command: ExprNS.Call,
455
    _context: Context,
456
  ): NoneValue {
NEW
457
    throw new Error("set_timeout is not yet supported by the CSE machine");
×
458
  }
459

460
  // clear_all_timeout(): cancels every pending set_timeout callback scheduled
461
  // so far. See set_timeout above for why this is CSE-unsupported for now.
462
  @Validate(0, 0, "clear_all_timeout", true)
463
  static clear_all_timeout(
51✔
464
    _args: Value[],
465
    _source: string,
466
    _command: ExprNS.Call,
467
    _context: Context,
468
  ): NoneValue {
NEW
469
    throw new Error("clear_all_timeout is not yet supported by the CSE machine");
×
470
  }
471
}
472
for (const builtin of Object.getOwnPropertyNames(MiscBuiltins)) {
51✔
473
  if (
1,530✔
474
    typeof MiscBuiltins[builtin as keyof typeof MiscBuiltins] === "function" &&
2,907✔
475
    !builtin.startsWith("_")
476
  ) {
477
    miscBuiltins.set(builtin, {
1,377✔
478
      type: "builtin",
479
      func: MiscBuiltins[builtin as keyof typeof MiscBuiltins] as BuiltinValue["func"],
480
      name: builtin,
481
      minArgs: minArgMap.get(builtin) || 0,
1,836✔
482
    });
483
  }
484
}
485

486
export default {
51✔
487
  name: GroupName.MISC,
488
  prelude: "",
489
  builtins: miscBuiltins,
490
};
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