pll-python 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.cjs +579 -3341
- package/package.json +1 -1
- package/worker.cjs +250 -491
package/cli.cjs
CHANGED
|
@@ -30,7 +30,7 @@ var path8 = __toESM(require("node:path"));
|
|
|
30
30
|
// src/cli/args.ts
|
|
31
31
|
var import_node_util = require("node:util");
|
|
32
32
|
|
|
33
|
-
// src/
|
|
33
|
+
// src/core/errorText.ts
|
|
34
34
|
function errorText(err) {
|
|
35
35
|
return err instanceof Error ? err.message : String(err);
|
|
36
36
|
}
|
|
@@ -101,7 +101,7 @@ var path4 = __toESM(require("node:path"));
|
|
|
101
101
|
var fs3 = __toESM(require("node:fs/promises"));
|
|
102
102
|
var path3 = __toESM(require("node:path"));
|
|
103
103
|
|
|
104
|
-
// src/
|
|
104
|
+
// src/core/editDistance.ts
|
|
105
105
|
function editDistance(a, b) {
|
|
106
106
|
let twoBack = [];
|
|
107
107
|
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
@@ -123,7 +123,7 @@ function editDistance(a, b) {
|
|
|
123
123
|
return previous[b.length];
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// src/
|
|
126
|
+
// src/core/level.ts
|
|
127
127
|
var LEVEL_RAW = "raw";
|
|
128
128
|
var LEVEL_BEGINNER = "beginner";
|
|
129
129
|
var LEVEL_INTERMEDIATE = "intermediate";
|
|
@@ -273,38 +273,11 @@ function closestLevel(named) {
|
|
|
273
273
|
}
|
|
274
274
|
return best !== null && bestDistance <= Math.max(2, Math.floor(best.length / 3)) ? best : null;
|
|
275
275
|
}
|
|
276
|
-
function levelRejectsBoolAsNumber(level) {
|
|
277
|
-
return level === LEVEL_BEGINNER || level === LEVEL_INTERMEDIATE;
|
|
278
|
-
}
|
|
279
|
-
function levelRefusesReassignment(level, scope) {
|
|
280
|
-
return level === LEVEL_BEGINNER || level === LEVEL_INTERMEDIATE && scope === "module";
|
|
281
|
-
}
|
|
282
276
|
function levelHasStaticChecks(level) {
|
|
283
277
|
return level === LEVEL_BEGINNER || level === LEVEL_INTERMEDIATE;
|
|
284
278
|
}
|
|
285
279
|
|
|
286
|
-
// src/
|
|
287
|
-
function levelHeaderFinding(source, fileName, level) {
|
|
288
|
-
const problem = levelHeaderProblem(source);
|
|
289
|
-
if (problem === null) {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
return {
|
|
293
|
-
id: "level-header",
|
|
294
|
-
errorType: "Level",
|
|
295
|
-
headline: problem.message,
|
|
296
|
-
howToFix: problem.howToFix,
|
|
297
|
-
fileName,
|
|
298
|
-
lineNumber: problem.line,
|
|
299
|
-
column: null,
|
|
300
|
-
nameToken: null,
|
|
301
|
-
severity: "error",
|
|
302
|
-
origin: "static",
|
|
303
|
-
level
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// src/common/analyzers/findingLocation.ts
|
|
280
|
+
// src/run/finding.ts
|
|
308
281
|
function serializeFinding(finding) {
|
|
309
282
|
return {
|
|
310
283
|
errorType: finding.errorType,
|
|
@@ -329,2687 +302,498 @@ function findingLocation(finding) {
|
|
|
329
302
|
label
|
|
330
303
|
};
|
|
331
304
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
examplar: "<pll:examplar>",
|
|
339
|
-
matplotlib: "<pll:matplotlib>"
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// src/common/errors/libraryFacts.ts
|
|
343
|
-
var FUNCTION_TAKERS = {
|
|
344
|
-
filter: {
|
|
345
|
-
calls: "`filter` calls your function with one row at a time",
|
|
346
|
-
each: "each row",
|
|
347
|
-
param: "r: dict",
|
|
348
|
-
keeps: "keeps the rows where it returns `True`"
|
|
349
|
-
},
|
|
350
|
-
transform_column: {
|
|
351
|
-
calls: "`transform_column` calls your function with one *value* from the column, not a row",
|
|
352
|
-
each: "each value in the column",
|
|
353
|
-
param: "v",
|
|
354
|
-
keeps: "puts what it returns in place of the value"
|
|
355
|
-
},
|
|
356
|
-
add_column: {
|
|
357
|
-
calls: "`add_column` calls your function with one row at a time",
|
|
358
|
-
each: "each row",
|
|
359
|
-
param: "r: dict",
|
|
360
|
-
keeps: "puts what it returns in the new column"
|
|
361
|
-
},
|
|
362
|
-
animate: { calls: "`animate` calls your function with the tick count, a number" },
|
|
363
|
-
big_bang: { calls: "`big_bang` calls your handlers with the state" }
|
|
364
|
-
};
|
|
365
|
-
var FUNCTION_TAKER_NAMES = Object.keys(FUNCTION_TAKERS);
|
|
366
|
-
function libraryCaller(frames) {
|
|
367
|
-
for (const { fileName, functionName } of frames) {
|
|
368
|
-
if (fileName === PLL_LIBRARY_FILES.reactor) {
|
|
369
|
-
return "reactor";
|
|
370
|
-
}
|
|
371
|
-
const inLibrary = fileName === PLL_LIBRARY_FILES.table || fileName === PLL_LIBRARY_FILES.image;
|
|
372
|
-
if (inLibrary && functionName !== null && FUNCTION_TAKER_NAMES.includes(functionName)) {
|
|
373
|
-
return functionName;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
var HANDLER_KEYWORDS = ["to_draw", "on_tick", "stop_when", "on_key", "on_mouse", "on_receive"];
|
|
379
|
-
var LIBRARY_HINTS = {
|
|
380
|
-
table: [
|
|
381
|
-
'The column names come first, as a list of strings, then all the rows inside one more list: `table(["month", "riders"], [["Jan", 1], ["Feb", 2]])`.'
|
|
382
|
-
],
|
|
383
|
-
table_from_columns: [
|
|
384
|
-
'`table_from_columns` takes one dictionary: `table_from_columns({"month": [...], "riders": [...]})`.'
|
|
385
|
-
]
|
|
386
|
-
};
|
|
387
|
-
function libraryHint(name) {
|
|
388
|
-
return Object.prototype.hasOwnProperty.call(LIBRARY_HINTS, name) ? LIBRARY_HINTS[name] : [];
|
|
389
|
-
}
|
|
390
|
-
var TABLE_METHODS = [
|
|
391
|
-
"columns",
|
|
392
|
-
"length",
|
|
393
|
-
"column",
|
|
394
|
-
"row",
|
|
395
|
-
"rows",
|
|
396
|
-
"filter",
|
|
397
|
-
"transform_column",
|
|
398
|
-
"add_column",
|
|
399
|
-
"add_row",
|
|
400
|
-
"select_columns",
|
|
401
|
-
"order_by",
|
|
402
|
-
"head",
|
|
403
|
-
"tail",
|
|
404
|
-
"sum",
|
|
405
|
-
"mean",
|
|
406
|
-
"min",
|
|
407
|
-
"max",
|
|
408
|
-
"count",
|
|
409
|
-
"bar_chart",
|
|
410
|
-
"scatter_chart",
|
|
411
|
-
"scatter_plot",
|
|
412
|
-
"line_chart",
|
|
413
|
-
"histogram",
|
|
414
|
-
"labeled_scatter_plot",
|
|
415
|
-
"pie_chart",
|
|
416
|
-
"dot_plot",
|
|
417
|
-
"labeled_dot_plot",
|
|
418
|
-
"freq_bar_chart",
|
|
419
|
-
"box_plot",
|
|
420
|
-
"lr_plot",
|
|
421
|
-
"labeled_lr_plot",
|
|
422
|
-
"linear_regression",
|
|
423
|
-
"to_pandas"
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
// src/common/errors/nameErrorExplainer.ts
|
|
427
|
-
var IMPORT_FOR = {
|
|
428
|
-
dataclass: "from dataclasses import dataclass",
|
|
429
|
-
field: "from dataclasses import field",
|
|
430
|
-
pd: "import pandas as pd",
|
|
431
|
-
pandas: "import pandas",
|
|
432
|
-
np: "import numpy as np",
|
|
433
|
-
numpy: "import numpy",
|
|
434
|
-
math: "import math",
|
|
435
|
-
pytest: "import pytest",
|
|
436
|
-
statistics: "import statistics",
|
|
437
|
-
random: "import random",
|
|
438
|
-
Callable: "from typing import Callable",
|
|
439
|
-
Optional: "from typing import Optional",
|
|
440
|
-
Any: "from typing import Any",
|
|
441
|
-
datetime: "import datetime",
|
|
442
|
-
json: "import json",
|
|
443
|
-
csv: "import csv",
|
|
444
|
-
re: "import re"
|
|
445
|
-
};
|
|
446
|
-
function definedLaterAt(source, name, usedOn) {
|
|
447
|
-
if (source === void 0 || usedOn === null) {
|
|
448
|
-
return null;
|
|
305
|
+
function formatFriendlyError(finding) {
|
|
306
|
+
const lines = [];
|
|
307
|
+
lines.push(`${finding.errorType}: ${finding.headline}`);
|
|
308
|
+
const location = findingLocation(finding);
|
|
309
|
+
if (location) {
|
|
310
|
+
lines.push(` at ${location.label}`);
|
|
449
311
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
for (let i = usedOn; i < lines.length; i++) {
|
|
456
|
-
if (defined.test(lines[i])) {
|
|
457
|
-
return i + 1;
|
|
312
|
+
if (finding.howToFix.length > 0) {
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push("How to fix:");
|
|
315
|
+
for (const item of finding.howToFix) {
|
|
316
|
+
lines.push(` - ${item}`);
|
|
458
317
|
}
|
|
459
318
|
}
|
|
460
|
-
return
|
|
319
|
+
return lines;
|
|
461
320
|
}
|
|
462
|
-
function
|
|
463
|
-
|
|
321
|
+
function levelHeaderFinding(source, fileName, level) {
|
|
322
|
+
const problem = levelHeaderProblem(source);
|
|
323
|
+
if (problem === null) {
|
|
464
324
|
return null;
|
|
465
325
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
326
|
+
return {
|
|
327
|
+
id: "level-header",
|
|
328
|
+
errorType: "Level",
|
|
329
|
+
headline: problem.message,
|
|
330
|
+
howToFix: problem.howToFix,
|
|
331
|
+
fileName,
|
|
332
|
+
lineNumber: problem.line,
|
|
333
|
+
column: null,
|
|
334
|
+
nameToken: null,
|
|
335
|
+
severity: "error",
|
|
336
|
+
origin: "static",
|
|
337
|
+
level
|
|
338
|
+
};
|
|
472
339
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
478
|
-
for (const taker of FUNCTION_TAKER_NAMES) {
|
|
479
|
-
const pattern = new RegExp(
|
|
480
|
-
`\\b${taker}\\s*\\(\\s*([A-Za-z_]\\w*)\\s*\\(\\s*${escaped}\\s*\\)`
|
|
481
|
-
);
|
|
482
|
-
const match = pattern.exec(line);
|
|
483
|
-
if (match !== null) {
|
|
484
|
-
return `${taker}:${match[1]}`;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return null;
|
|
340
|
+
function findRuntimeFinding(fileName, level, error) {
|
|
341
|
+
return { ...error.finding, fileName: error.finding.fileName ?? fileName, level };
|
|
488
342
|
}
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
343
|
+
function explainTestReport(event, fileName, level) {
|
|
344
|
+
return {
|
|
345
|
+
...event,
|
|
346
|
+
tests: event.tests.map(
|
|
347
|
+
({ error, ...test }) => error ? { ...test, finding: serializeFinding(findRuntimeFinding(fileName, level, error)) } : test
|
|
348
|
+
)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/run/examplarSource.ts
|
|
353
|
+
var MAX_BUNDLE_BYTES = 4 * 1024 * 1024;
|
|
354
|
+
var DIRECTIVE_RE = /^#\s*examplar\s+(\S+)\s*$/;
|
|
355
|
+
function parseExamplarDirective(source) {
|
|
356
|
+
const hits = [];
|
|
357
|
+
const lines = source.split(/\r?\n/);
|
|
358
|
+
for (let i = 0; i < lines.length; i++) {
|
|
359
|
+
const match = DIRECTIVE_RE.exec(lines[i].trim());
|
|
360
|
+
if (match) {
|
|
361
|
+
hits.push({ url: match[1], line: i + 1 });
|
|
496
362
|
}
|
|
497
363
|
}
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
function methodCalledAsFunction(line, name) {
|
|
501
|
-
if (line === null || !TABLE_METHODS.includes(name)) {
|
|
502
|
-
return null;
|
|
364
|
+
if (hits.length === 0) {
|
|
365
|
+
return { kind: "none" };
|
|
503
366
|
}
|
|
504
|
-
|
|
505
|
-
return found ? { table: found[1], rest: (found[2] ?? "").trim() } : null;
|
|
506
|
-
}
|
|
507
|
-
function unboundKind(message) {
|
|
508
|
-
const m = message.match(/cannot access (free|local) variable '/);
|
|
509
|
-
return m ? m[1] : null;
|
|
510
|
-
}
|
|
511
|
-
function explainNameError(parsed, context = {}) {
|
|
512
|
-
const named = parsed.nameToken !== null;
|
|
513
|
-
const name = parsed.nameToken ?? "this name";
|
|
514
|
-
const didYouMean = extractDidYouMean(parsed.message);
|
|
515
|
-
const unbound = unboundKind(parsed.message);
|
|
516
|
-
if (unbound !== null) {
|
|
367
|
+
if (hits.length > 1) {
|
|
517
368
|
return {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
unbound === "local" ? `If you meant a \`${name}\` from outside this function: assigning to it anywhere inside makes it local to the whole function, so the outer one is not visible here. Pass it in as a parameter instead.` : `The \`${name}\` here belongs to the function around this one, which sets it after this line runs.`
|
|
522
|
-
]
|
|
369
|
+
kind: "error",
|
|
370
|
+
line: hits[1].line,
|
|
371
|
+
message: `there is more than one \`#examplar\` line (lines ${hits.map((h) => h.line).join(" and ")}). Keep the one you want.`
|
|
523
372
|
};
|
|
524
373
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
374
|
+
const { url, line } = hits[0];
|
|
375
|
+
const problem = validateBundleUrl(url);
|
|
376
|
+
return problem ? { kind: "error", message: problem, line } : { kind: "found", url, line };
|
|
377
|
+
}
|
|
378
|
+
function validateBundleUrl(url) {
|
|
379
|
+
let parsed;
|
|
380
|
+
try {
|
|
381
|
+
parsed = new URL(url);
|
|
382
|
+
} catch {
|
|
383
|
+
return `\`${url}\` is not a URL.`;
|
|
535
384
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
headline: `Python reads \`${hyphen}\` as \`${hyphen.split("-").join("` minus `")}\`: a name cannot have a \`-\` in it.`,
|
|
540
|
-
howToFix: [`Write \`${hyphen.replace(/-/g, "_")}\`, with an underscore.`]
|
|
541
|
-
};
|
|
385
|
+
if (parsed.protocol === "https:") {
|
|
386
|
+
return null;
|
|
542
387
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
headline: `\`${name}\` is a method of a table, so it is called on one.`,
|
|
547
|
-
howToFix: [`Write \`${method.table}.${name}(${method.rest})\`.`]
|
|
548
|
-
};
|
|
388
|
+
if (parsed.protocol === "http:") {
|
|
389
|
+
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
390
|
+
return local ? null : `\`${url}\` must use https (plain http is only allowed for localhost).`;
|
|
549
391
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
392
|
+
return `\`${url}\` must be an https address.`;
|
|
393
|
+
}
|
|
394
|
+
async function loadBundle(url, store, fetchImpl = globalThis.fetch) {
|
|
395
|
+
const cached = await store.read(url).catch(() => void 0);
|
|
396
|
+
let response;
|
|
397
|
+
try {
|
|
398
|
+
response = await fetchImpl(url, {
|
|
399
|
+
headers: cached?.etag ? { "If-None-Match": cached.etag } : {}
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const why = errorText(err);
|
|
403
|
+
return cached ? { json: cached.json, fromCache: true, note: `could not reach ${url} (${why}); using the cached copy` } : { fromCache: false, error: `could not reach ${url} (${why}), and nothing is cached` };
|
|
556
404
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const annotated = annotationTarget(context.line ?? null, name);
|
|
560
|
-
if (annotated !== null) {
|
|
561
|
-
const example = annotated ? `${annotated}: "${name}"` : `-> "${name}"`;
|
|
562
|
-
return {
|
|
563
|
-
headline: `\`${name}\` is defined on line ${laterOn}, after this line, and an annotation is read as the file runs.`,
|
|
564
|
-
howToFix: [
|
|
565
|
-
`Write the type as a string - \`${example}\` - and Python looks it up later, once it exists.`,
|
|
566
|
-
"A type that names itself can only be written that way: the union needs the class, and the class needs the union."
|
|
567
|
-
]
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
return {
|
|
571
|
-
headline: `\`${name}\` is defined on line ${laterOn}, after this line.`,
|
|
572
|
-
howToFix: [
|
|
573
|
-
"Python runs a file from the top, so nothing further down exists yet.",
|
|
574
|
-
`Move the definition of \`${name}\` above this line.`
|
|
575
|
-
]
|
|
576
|
-
};
|
|
405
|
+
if (response.status === 304 && cached) {
|
|
406
|
+
return { json: cached.json, fromCache: true };
|
|
577
407
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
return cached ? {
|
|
410
|
+
json: cached.json,
|
|
411
|
+
fromCache: true,
|
|
412
|
+
note: `${url} returned ${response.status}; using the cached copy`
|
|
413
|
+
} : { fromCache: false, error: `${url} returned ${response.status}` };
|
|
414
|
+
}
|
|
415
|
+
const json = await response.text();
|
|
416
|
+
if (json.length > MAX_BUNDLE_BYTES) {
|
|
417
|
+
return cached ? { json: cached.json, fromCache: true, note: `${url} is unexpectedly large; using the cached copy` } : {
|
|
418
|
+
fromCache: false,
|
|
419
|
+
error: `${url} is ${json.length} bytes, past the ${MAX_BUNDLE_BYTES}-byte limit`
|
|
586
420
|
};
|
|
587
421
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
`Make sure \`${name}\` is defined before this line runs.`
|
|
591
|
-
];
|
|
592
|
-
if (named) {
|
|
593
|
-
howToFix.push(`If \`${name}\` should be text, put it in quotes: \`"${name}"\`.`);
|
|
422
|
+
if (!looksLikeBundle(json)) {
|
|
423
|
+
return cached ? { json: cached.json, fromCache: true, note: `${url} did not return a bundle; using the cached copy` } : { fromCache: false, error: `${url} did not return an Examplar bundle` };
|
|
594
424
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
425
|
+
const etag = response.headers.get("etag") ?? void 0;
|
|
426
|
+
try {
|
|
427
|
+
await store.write(url, { json, etag });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
return { json, fromCache: false, note: `could not keep a copy for offline use (${errorText(err)})` };
|
|
599
430
|
}
|
|
600
|
-
return {
|
|
431
|
+
return { json, fromCache: false };
|
|
601
432
|
}
|
|
602
|
-
function
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return
|
|
433
|
+
function looksLikeBundle(json) {
|
|
434
|
+
try {
|
|
435
|
+
const bundle = JSON.parse(json);
|
|
436
|
+
return typeof bundle === "object" && bundle !== null && "examplar" in bundle && Array.isArray(bundle.wheats) && Array.isArray(bundle.chaffs);
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
606
439
|
}
|
|
607
|
-
return m[1].split(/,|\bor\b/).map((s) => s.trim().replace(/^['"`]|['"`]$/g, "")).filter((s) => s.length > 0);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// src/common/errors/pythonError.ts
|
|
611
|
-
function userFrames(error) {
|
|
612
|
-
return error.frames.filter((frame) => frame.user);
|
|
613
|
-
}
|
|
614
|
-
function innermostUserFrame(error) {
|
|
615
|
-
const frames = userFrames(error);
|
|
616
|
-
return frames.length > 0 ? frames[frames.length - 1] : null;
|
|
617
440
|
}
|
|
618
441
|
|
|
619
|
-
// src/
|
|
620
|
-
|
|
621
|
-
|
|
442
|
+
// src/pyodide/sharedMemory.ts
|
|
443
|
+
var STDIN_SAB_BYTES = 64 * 1024;
|
|
444
|
+
var STDIN_STATE_INDEX = 0;
|
|
445
|
+
var STDIN_LENGTH_INDEX = 1;
|
|
446
|
+
var STDIN_REQUEST_INDEX = 2;
|
|
447
|
+
var STDIN_PAYLOAD_OFFSET = 16;
|
|
448
|
+
var STDIN_STATE_WAITING = 0;
|
|
449
|
+
var STDIN_STATE_DATA = 1;
|
|
450
|
+
var STDIN_STATE_EOF = 2;
|
|
451
|
+
var STDIN_STATE_INTERRUPTED = 3;
|
|
452
|
+
function tryCreateStdinBuffer() {
|
|
453
|
+
try {
|
|
454
|
+
return new SharedArrayBuffer(STDIN_SAB_BYTES);
|
|
455
|
+
} catch {
|
|
622
456
|
return null;
|
|
623
457
|
}
|
|
624
|
-
const lines = source.split(/\r?\n/);
|
|
625
|
-
return line >= 1 && line <= lines.length ? lines[line - 1] : null;
|
|
626
458
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
"float",
|
|
632
|
-
"bool",
|
|
633
|
-
"len",
|
|
634
|
-
"list",
|
|
635
|
-
"sorted",
|
|
636
|
-
"round",
|
|
637
|
-
"abs",
|
|
638
|
-
"sum",
|
|
639
|
-
"min",
|
|
640
|
-
"max",
|
|
641
|
-
"type",
|
|
642
|
-
"repr"
|
|
643
|
-
]);
|
|
644
|
-
function callOn(line) {
|
|
645
|
-
const names = Array.from(
|
|
646
|
-
(line ?? "").matchAll(/([A-Za-z_][\w.]*)\s*\(/g),
|
|
647
|
-
(m) => m[1]
|
|
648
|
-
);
|
|
649
|
-
if (names.length === 0) {
|
|
650
|
-
return null;
|
|
459
|
+
function writeStdin(sab, request, bytes) {
|
|
460
|
+
const state = new Int32Array(sab);
|
|
461
|
+
if (Atomics.load(state, STDIN_REQUEST_INDEX) !== request || Atomics.load(state, STDIN_STATE_INDEX) !== STDIN_STATE_WAITING) {
|
|
462
|
+
return -1;
|
|
651
463
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
while (i < line.length) {
|
|
658
|
-
const c = line[i];
|
|
659
|
-
if (c === "#") break;
|
|
660
|
-
if (/\s/.test(c)) {
|
|
661
|
-
i++;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
const start = i;
|
|
665
|
-
if (c === '"' || c === "'") {
|
|
666
|
-
i++;
|
|
667
|
-
while (i < line.length && line[i] !== c) i += line[i] === "\\" ? 2 : 1;
|
|
668
|
-
i = Math.min(i + 1, line.length);
|
|
669
|
-
tokens.push({ kind: "str", text: line.slice(start, i), start, end: i });
|
|
670
|
-
} else if (/[A-Za-z_]/.test(c)) {
|
|
671
|
-
while (i < line.length && /\w/.test(line[i])) i++;
|
|
672
|
-
tokens.push({ kind: "name", text: line.slice(start, i), start, end: i });
|
|
673
|
-
} else if (/\d/.test(c)) {
|
|
674
|
-
while (i < line.length && /[\d.]/.test(line[i])) i++;
|
|
675
|
-
tokens.push({ kind: "num", text: line.slice(start, i), start, end: i });
|
|
676
|
-
} else if ("([{".includes(c)) {
|
|
677
|
-
i++;
|
|
678
|
-
tokens.push({ kind: "open", text: c, start, end: i });
|
|
679
|
-
} else if (")]}".includes(c)) {
|
|
680
|
-
i++;
|
|
681
|
-
tokens.push({ kind: "close", text: c, start, end: i });
|
|
682
|
-
} else {
|
|
683
|
-
const two = line.slice(i, i + 2);
|
|
684
|
-
const op = ["<=", ">=", "==", "!=", "**", "//"].includes(two) ? two : c;
|
|
685
|
-
i += op.length;
|
|
686
|
-
tokens.push({ kind: "op", text: op, start, end: i });
|
|
687
|
-
}
|
|
464
|
+
let n = 0;
|
|
465
|
+
if (bytes !== null) {
|
|
466
|
+
n = Math.min(bytes.length, sab.byteLength - STDIN_PAYLOAD_OFFSET);
|
|
467
|
+
new Uint8Array(sab, STDIN_PAYLOAD_OFFSET).set(bytes.subarray(0, n));
|
|
468
|
+
Atomics.store(state, STDIN_LENGTH_INDEX, n);
|
|
688
469
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
for (let i = open; i < tokens.length; i++) {
|
|
694
|
-
if (tokens[i].kind === "open") depth++;
|
|
695
|
-
if (tokens[i].kind === "close" && --depth === 0) return i;
|
|
470
|
+
const answer = bytes === null ? STDIN_STATE_EOF : STDIN_STATE_DATA;
|
|
471
|
+
const was = Atomics.compareExchange(state, STDIN_STATE_INDEX, STDIN_STATE_WAITING, answer);
|
|
472
|
+
if (was !== STDIN_STATE_WAITING) {
|
|
473
|
+
return -1;
|
|
696
474
|
}
|
|
697
|
-
|
|
475
|
+
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
476
|
+
return n;
|
|
698
477
|
}
|
|
699
|
-
function
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
let last = at;
|
|
704
|
-
for (let i = at + 1; i < tokens.length; ) {
|
|
705
|
-
if (tokens[i].text === "." && tokens[i + 1]?.kind === "name") {
|
|
706
|
-
last = i + 1;
|
|
707
|
-
i += 2;
|
|
708
|
-
} else if (tokens[i].kind === "open" && tokens[i].text !== "{") {
|
|
709
|
-
const shut = closing(tokens, i);
|
|
710
|
-
if (shut < 0) return null;
|
|
711
|
-
last = shut;
|
|
712
|
-
i = shut + 1;
|
|
713
|
-
} else {
|
|
714
|
-
break;
|
|
715
|
-
}
|
|
478
|
+
function interruptStdin(sab) {
|
|
479
|
+
const state = new Int32Array(sab);
|
|
480
|
+
if (Atomics.compareExchange(state, STDIN_STATE_INDEX, STDIN_STATE_WAITING, STDIN_STATE_INTERRUPTED) === STDIN_STATE_WAITING) {
|
|
481
|
+
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
716
482
|
}
|
|
717
|
-
return line.slice(tokens[at].start, tokens[last].end);
|
|
718
483
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
if (j < 0) return null;
|
|
731
|
-
first = j;
|
|
732
|
-
i = j - 1;
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (tokens[i].kind === "name" || tokens[i].kind === "num") {
|
|
736
|
-
first = i;
|
|
737
|
-
if (tokens[i - 1]?.text === "." && tokens[i - 2]?.kind === "name") {
|
|
738
|
-
i -= 2;
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
if (i === at) return null;
|
|
744
|
-
break;
|
|
484
|
+
var INTERRUPT_SAB_BYTES = 2;
|
|
485
|
+
var INTERRUPT_SIGINT = 2;
|
|
486
|
+
var INTERRUPT_ACK_INDEX = 1;
|
|
487
|
+
var INTERRUPT_RETRY_MS = 15;
|
|
488
|
+
var INTERRUPT_RETRY_LIMIT_MS = 1e4;
|
|
489
|
+
function tryCreateInterruptBuffer() {
|
|
490
|
+
try {
|
|
491
|
+
return new SharedArrayBuffer(INTERRUPT_SAB_BYTES);
|
|
492
|
+
} catch {
|
|
493
|
+
return null;
|
|
745
494
|
}
|
|
746
|
-
if (tokens[first].kind !== "name" && tokens[first].kind !== "num") return null;
|
|
747
|
-
return line.slice(tokens[first].start, tokens[at].end);
|
|
748
495
|
}
|
|
749
|
-
function
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (tokens[i + 1]?.text === "+") {
|
|
754
|
-
const value = operandFrom(line, tokens, i + 2);
|
|
755
|
-
if (value !== null) return { text: tokens[i].text, value, textFirst: true };
|
|
756
|
-
}
|
|
757
|
-
if (tokens[i - 1]?.text === "+" && i >= 2) {
|
|
758
|
-
const value = operandBefore(line, tokens, i - 2);
|
|
759
|
-
if (value !== null) return { text: tokens[i].text, value, textFirst: false };
|
|
760
|
-
}
|
|
496
|
+
function requestInterrupt(sab, stillRunning, retryMs = INTERRUPT_RETRY_MS, limitMs = INTERRUPT_RETRY_LIMIT_MS) {
|
|
497
|
+
const view = new Uint8Array(sab);
|
|
498
|
+
if (view.length > INTERRUPT_ACK_INDEX) {
|
|
499
|
+
Atomics.store(view, INTERRUPT_ACK_INDEX, 0);
|
|
761
500
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (line === null) return null;
|
|
766
|
-
const tokens = tokenize(line);
|
|
767
|
-
for (let i = 2; i < tokens.length; i++) {
|
|
768
|
-
if (tokens[i].text === attr && tokens[i - 1].text === ".") {
|
|
769
|
-
return operandBefore(line, tokens, i - 2);
|
|
770
|
-
}
|
|
501
|
+
Atomics.store(view, 0, INTERRUPT_SIGINT);
|
|
502
|
+
if (view.length <= INTERRUPT_ACK_INDEX) {
|
|
503
|
+
return;
|
|
771
504
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const tokens = tokenize(line);
|
|
777
|
-
for (let i = 1; i < tokens.length; i++) {
|
|
778
|
-
const before = tokens[i - 1];
|
|
779
|
-
if (tokens[i].text === "[" && (before.kind === "name" || before.text === ")" || before.text === "]")) {
|
|
780
|
-
return operandBefore(line, tokens, i - 1);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
function loopedOver(line) {
|
|
786
|
-
if (line === null) return null;
|
|
787
|
-
const tokens = tokenize(line);
|
|
788
|
-
const at = tokens.findIndex((t) => t.text === "in" && tokens[0]?.text === "for");
|
|
789
|
-
return at < 0 ? null : operandFrom(line, tokens, at + 1);
|
|
790
|
-
}
|
|
791
|
-
function operandBeside(line, operator, side) {
|
|
792
|
-
if (line === null) return null;
|
|
793
|
-
const tokens = tokenize(line);
|
|
794
|
-
const at = tokens.findIndex((t) => t.kind === "op" && t.text === operator);
|
|
795
|
-
if (at < 0) return null;
|
|
796
|
-
return side === "left" ? operandBefore(line, tokens, at - 1) : operandFrom(line, tokens, at + 1);
|
|
797
|
-
}
|
|
798
|
-
function listNames(names) {
|
|
799
|
-
const quoted = names.map((name) => `\`${name}\``);
|
|
800
|
-
if (quoted.length <= 1) {
|
|
801
|
-
return quoted.join("");
|
|
802
|
-
}
|
|
803
|
-
return `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}`;
|
|
804
|
-
}
|
|
805
|
-
function closestName(name, candidates) {
|
|
806
|
-
let best = null;
|
|
807
|
-
let bestDistance = Infinity;
|
|
808
|
-
for (const candidate of candidates) {
|
|
809
|
-
const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
|
|
810
|
-
if (distance < bestDistance) {
|
|
811
|
-
best = candidate;
|
|
812
|
-
bestDistance = distance;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (best === null) {
|
|
816
|
-
return null;
|
|
817
|
-
}
|
|
818
|
-
return bestDistance <= Math.max(1, Math.floor(best.length / 3)) ? best : null;
|
|
819
|
-
}
|
|
820
|
-
function tableRowLine(source, callLine, index) {
|
|
821
|
-
const lines = source.split(/\r?\n/);
|
|
822
|
-
if (callLine < 1 || callLine > lines.length) {
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
const call = /\b(?:table|Table)\s*\(/.exec(lines[callLine - 1]);
|
|
826
|
-
if (call === null) {
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
let offset = 0;
|
|
830
|
-
for (let i2 = 0; i2 < callLine - 1; i2++) {
|
|
831
|
-
offset += lines[i2].length + 1;
|
|
832
|
-
}
|
|
833
|
-
const text = lines.join("\n");
|
|
834
|
-
let i = offset + call.index + call[0].length;
|
|
835
|
-
let depth = 1;
|
|
836
|
-
let argument = 0;
|
|
837
|
-
let row = -1;
|
|
838
|
-
let rowsOpen = false;
|
|
839
|
-
let awaitingRow = false;
|
|
840
|
-
const lineAt = (at) => text.slice(0, at).split("\n").length;
|
|
841
|
-
while (i < text.length && depth > 0) {
|
|
842
|
-
const c = text[i];
|
|
843
|
-
if (c === '"' || c === "'") {
|
|
844
|
-
const quote = text.startsWith(c.repeat(3), i) ? c.repeat(3) : c;
|
|
845
|
-
i += quote.length;
|
|
846
|
-
while (i < text.length && !text.startsWith(quote, i)) {
|
|
847
|
-
i += text[i] === "\\" ? 2 : 1;
|
|
848
|
-
}
|
|
849
|
-
i += quote.length;
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (c === "#") {
|
|
853
|
-
while (i < text.length && text[i] !== "\n") i++;
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (/\s/.test(c)) {
|
|
857
|
-
i++;
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
if (awaitingRow && c !== "]") {
|
|
861
|
-
row++;
|
|
862
|
-
if (row === index) {
|
|
863
|
-
return lineAt(i);
|
|
864
|
-
}
|
|
865
|
-
awaitingRow = false;
|
|
505
|
+
const deadline = Date.now() + limitMs;
|
|
506
|
+
const retry = () => {
|
|
507
|
+
if (Atomics.load(view, INTERRUPT_ACK_INDEX) !== 0 || !stillRunning() || Date.now() > deadline) {
|
|
508
|
+
return;
|
|
866
509
|
}
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
if (depth === 2 && argument === 1 && c === "[" && !rowsOpen) {
|
|
870
|
-
rowsOpen = true;
|
|
871
|
-
awaitingRow = true;
|
|
872
|
-
}
|
|
873
|
-
} else if (c === ")" || c === "]" || c === "}") {
|
|
874
|
-
depth--;
|
|
875
|
-
if (rowsOpen && depth === 1) {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
} else if (c === ",") {
|
|
879
|
-
if (depth === 1) {
|
|
880
|
-
argument++;
|
|
881
|
-
if (argument > 1) return null;
|
|
882
|
-
} else if (depth === 2 && rowsOpen) {
|
|
883
|
-
awaitingRow = true;
|
|
884
|
-
}
|
|
885
|
-
} else if (depth === 1 && argument === 1 && !rowsOpen) {
|
|
886
|
-
return null;
|
|
510
|
+
if (Atomics.load(view, 0) === 0) {
|
|
511
|
+
Atomics.store(view, 0, INTERRUPT_SIGINT);
|
|
887
512
|
}
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
return null;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// src/common/analyzers/errorPlace.ts
|
|
894
|
-
var PLACED_BY_PYTHON = ["SyntaxError", "IndentationError", "TabError"];
|
|
895
|
-
function placeOf(input) {
|
|
896
|
-
const { error, fileName } = input;
|
|
897
|
-
if (PLACED_BY_PYTHON.includes(error.errorType) || error.frames.length === 0) {
|
|
898
|
-
const file = error.fileName ?? fileName;
|
|
899
|
-
return {
|
|
900
|
-
fileName: file,
|
|
901
|
-
lineNumber: error.lineNumber,
|
|
902
|
-
column: error.column,
|
|
903
|
-
text: error.text ?? lineOfRun(input, file, error.lineNumber)
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
const frame = innermostUserFrame(error);
|
|
907
|
-
if (frame === null) {
|
|
908
|
-
return { fileName, lineNumber: null, column: null, text: null };
|
|
909
|
-
}
|
|
910
|
-
return {
|
|
911
|
-
fileName: frame.fileName,
|
|
912
|
-
lineNumber: frame.line,
|
|
913
|
-
// A caret is about the frame that raised; only there is it theirs.
|
|
914
|
-
column: frame === error.frames[error.frames.length - 1] ? frame.column : null,
|
|
915
|
-
text: frameText(input, frame)
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
function frameText(input, frame) {
|
|
919
|
-
if (frame === null) return null;
|
|
920
|
-
return frame.text ?? lineOfRun(input, frame.fileName, frame.line);
|
|
921
|
-
}
|
|
922
|
-
function inRunFile(input, place) {
|
|
923
|
-
return place.fileName === input.fileName;
|
|
924
|
-
}
|
|
925
|
-
function lineOfRun(input, file, line) {
|
|
926
|
-
return file === input.fileName ? sourceLine(input.source, line) : null;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// src/common/analyzers/types.ts
|
|
930
|
-
function runtimeFindingFor(input, fields) {
|
|
931
|
-
const { error, level } = input;
|
|
932
|
-
const place = placeOf(input);
|
|
933
|
-
return {
|
|
934
|
-
errorType: error.errorType,
|
|
935
|
-
fileName: place.fileName,
|
|
936
|
-
lineNumber: place.lineNumber,
|
|
937
|
-
column: place.column,
|
|
938
|
-
nameToken: error.nameToken,
|
|
939
|
-
severity: "error",
|
|
940
|
-
origin: "runtime",
|
|
941
|
-
level,
|
|
942
|
-
...fields
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
function staticFindingFor(raw, level, fileName, fields) {
|
|
946
|
-
return {
|
|
947
|
-
id: raw.id,
|
|
948
|
-
errorType: raw.errorType,
|
|
949
|
-
fileName,
|
|
950
|
-
lineNumber: raw.lineNumber,
|
|
951
|
-
column: raw.column,
|
|
952
|
-
nameToken: raw.nameToken,
|
|
953
|
-
severity: raw.severity,
|
|
954
|
-
origin: "static",
|
|
955
|
-
level,
|
|
956
|
-
...fields
|
|
513
|
+
setTimeout(retry, retryMs);
|
|
957
514
|
};
|
|
515
|
+
setTimeout(retry, retryMs);
|
|
958
516
|
}
|
|
959
517
|
|
|
960
|
-
// src/
|
|
961
|
-
var
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const place = placeOf(input);
|
|
967
|
-
const context = { source: inRunFile(input, place) ? source : "", line: place.text };
|
|
968
|
-
return runtimeFindingFor(input, { id: "name-error", ...explainNameError(error, context) });
|
|
518
|
+
// src/pyodide/runtime.ts
|
|
519
|
+
var NOT_RUNNING = "Python is not running.";
|
|
520
|
+
var PythonLostError = class extends Error {
|
|
521
|
+
constructor() {
|
|
522
|
+
super("Python stopped completely.");
|
|
523
|
+
this.name = "PythonLostError";
|
|
969
524
|
}
|
|
970
525
|
};
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
case "module":
|
|
976
|
-
return "the file";
|
|
977
|
-
case "function":
|
|
978
|
-
case "lambda":
|
|
979
|
-
return "an enclosing function";
|
|
980
|
-
case "class":
|
|
981
|
-
return "an enclosing class";
|
|
982
|
-
case "comprehension":
|
|
983
|
-
return "an enclosing comprehension";
|
|
984
|
-
default:
|
|
985
|
-
return "an outer scope";
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
function bindingNoun(binding) {
|
|
989
|
-
switch (binding) {
|
|
990
|
-
case "argument":
|
|
991
|
-
return "parameter";
|
|
992
|
-
case "for":
|
|
993
|
-
return "loop variable";
|
|
994
|
-
case "functiondef":
|
|
995
|
-
return "function";
|
|
996
|
-
case "classdef":
|
|
997
|
-
return "class";
|
|
998
|
-
case "import":
|
|
999
|
-
case "importfrom":
|
|
1000
|
-
return "import";
|
|
1001
|
-
case "capture":
|
|
1002
|
-
return "name in a `case`";
|
|
1003
|
-
case "assign":
|
|
1004
|
-
case "annassign":
|
|
1005
|
-
case "augassign":
|
|
1006
|
-
case "walrus":
|
|
1007
|
-
case "with":
|
|
1008
|
-
return "variable";
|
|
1009
|
-
default:
|
|
1010
|
-
return "name";
|
|
526
|
+
var StoppedError = class extends Error {
|
|
527
|
+
constructor(message) {
|
|
528
|
+
super(message);
|
|
529
|
+
this.name = "StoppedError";
|
|
1011
530
|
}
|
|
531
|
+
};
|
|
532
|
+
function deliverDisplay(display, onEvent, fileName) {
|
|
533
|
+
onEvent(display.kind === "image" || display.kind === "table" ? { ...display, source: fileName } : display);
|
|
1012
534
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const outerNoun = outerScopeNoun(raw.outerScopeKind);
|
|
1017
|
-
const knownOuterLine = outerLine !== null && outerLine > 0;
|
|
1018
|
-
const inner = bindingNoun(raw.binding);
|
|
1019
|
-
const outer = bindingNoun(raw.outerBinding);
|
|
1020
|
-
if (knownOuterLine && raw.lineNumber !== null && raw.lineNumber !== void 0 && outerLine > raw.lineNumber) {
|
|
1021
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1022
|
-
headline: `\`${name}\` names two things: this ${inner}, and a ${outer} in ${outerNoun}, on line ${outerLine}.`,
|
|
1023
|
-
howToFix: [
|
|
1024
|
-
`Give one of them another name - this ${inner}, or the ${outer} on line ${outerLine} - so each name means one thing.`
|
|
1025
|
-
]
|
|
1026
|
-
});
|
|
535
|
+
var WorkerConnection = class {
|
|
536
|
+
constructor(options) {
|
|
537
|
+
this.options = options;
|
|
1027
538
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
default:
|
|
1056
|
-
return "the PLL libraries";
|
|
539
|
+
options;
|
|
540
|
+
worker = null;
|
|
541
|
+
nextId = 1;
|
|
542
|
+
pending = /* @__PURE__ */ new Map();
|
|
543
|
+
initPromise = null;
|
|
544
|
+
stdinBuffer = null;
|
|
545
|
+
interruptBuffer = null;
|
|
546
|
+
stdinHandler = null;
|
|
547
|
+
/** The stdin request being answered, or null; a Stop ends it. */
|
|
548
|
+
stdinRequest = null;
|
|
549
|
+
/** Stdin read but not yet given: what did not fit, or what came after a Stop. */
|
|
550
|
+
stdinCarry = new Uint8Array(0);
|
|
551
|
+
/** The handler's answer being waited for, which the next request shares. */
|
|
552
|
+
stdinNext = null;
|
|
553
|
+
packageNoteHandler = null;
|
|
554
|
+
pythonLostHandler = null;
|
|
555
|
+
/** Start the worker, once - or again, if the last attempt failed. */
|
|
556
|
+
async initialize() {
|
|
557
|
+
if (!this.initPromise) {
|
|
558
|
+
this.initPromise = this.doInitialize().catch((err) => {
|
|
559
|
+
this.initPromise = null;
|
|
560
|
+
this.worker?.terminate();
|
|
561
|
+
this.worker = null;
|
|
562
|
+
throw err;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
await this.initPromise;
|
|
1057
566
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const alias = `${root}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
1065
|
-
const written = raw.binding === "importfrom" ? `from ${raw.module} import ${name} as ${alias}` : `import ${name} as ${alias}`;
|
|
1066
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1067
|
-
headline: `\`${name}\` is already defined by ${noun}, and this import replaces it.`,
|
|
1068
|
-
howToFix: [
|
|
1069
|
-
`Import it under another name: \`${written}\`, and use \`${alias}\` where you mean that one.`
|
|
1070
|
-
]
|
|
567
|
+
async doInitialize() {
|
|
568
|
+
const indexUrl = this.options.indexUrl();
|
|
569
|
+
const worker = this.options.spawn({
|
|
570
|
+
onMessage: (msg) => this.handleMessage(msg, worker),
|
|
571
|
+
onError: (err) => this.rejectAllPending(err),
|
|
572
|
+
onExit: () => this.pythonLost(worker)
|
|
1071
573
|
});
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
574
|
+
this.worker = worker;
|
|
575
|
+
this.stdinBuffer = tryCreateStdinBuffer();
|
|
576
|
+
this.interruptBuffer = tryCreateInterruptBuffer();
|
|
577
|
+
const packageCacheDir = this.options.packageCacheDir?.(indexUrl);
|
|
578
|
+
const started = this.send("init", {
|
|
579
|
+
indexUrl,
|
|
580
|
+
...packageCacheDir ? { packageCacheDir } : {},
|
|
581
|
+
...this.stdinBuffer ? { stdinBuffer: this.stdinBuffer } : {},
|
|
582
|
+
...this.interruptBuffer ? { interruptBuffer: this.interruptBuffer } : {}
|
|
1080
583
|
});
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
`Pick a different name for your definition - for example \`my_${name}\`, \`${name}_value\`, or a word that describes what yours represents.`,
|
|
1086
|
-
`If you meant to use the library's \`${name}\`, call it directly instead of defining a new \`${name}\` of your own.`
|
|
1087
|
-
]
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// src/common/analyzers/static/reassignmentExplainer.ts
|
|
1092
|
-
function explainReassignment(raw, level, fileName) {
|
|
1093
|
-
const name = raw.nameToken ?? "this name";
|
|
1094
|
-
const firstLine = raw.firstLineNumber ?? null;
|
|
1095
|
-
const firstLineHint = firstLine !== null && firstLine > 0 ? ` (first assigned on line ${firstLine})` : "";
|
|
1096
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1097
|
-
headline: `\`${name}\` is already assigned${firstLineHint}.`,
|
|
1098
|
-
// Refused here, but not inside a function: then the fix is to move the
|
|
1099
|
-
// work into one.
|
|
1100
|
-
howToFix: !levelRefusesReassignment(level, "function") ? [
|
|
1101
|
-
`If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
|
|
1102
|
-
`If you're trying to update a value (\`x = x + 1\`, accumulators, running totals), wrap the work in a function and rebind \`${name}\` from the function's return value: \`${name} = compute_${name}(...)\`.`
|
|
1103
|
-
] : [
|
|
1104
|
-
`If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
|
|
1105
|
-
`If you're trying to update a value (\`x = x + 1\`, accumulators, running totals), use a built-in like \`sum(...)\`, \`max(...)\`, or a small helper function that returns the new value instead.`,
|
|
1106
|
-
// Inside a function this is exactly what `intermediate` exists
|
|
1107
|
-
// for, and a student has no way to know that from here.
|
|
1108
|
-
...raw.scopeKind === "function" && !levelRefusesReassignment(LEVEL_INTERMEDIATE, "function") ? [
|
|
1109
|
-
"`#level intermediate` allows changing a variable inside a function, which is what a running total needs."
|
|
1110
|
-
] : []
|
|
1111
|
-
]
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// src/common/analyzers/static/disallowedKeywordExplainer.ts
|
|
1116
|
-
function explainDisallowedKeyword(raw, level, fileName) {
|
|
1117
|
-
const keyword = raw.keyword === "nonlocal" ? "nonlocal" : "global";
|
|
1118
|
-
const names = Array.isArray(raw.names) ? raw.names : [];
|
|
1119
|
-
const formattedNames = names.map((n) => `\`${n}\``).join(", ");
|
|
1120
|
-
const namesPhrase = formattedNames.length > 0 ? ` ${formattedNames}` : "";
|
|
1121
|
-
const headline = `\`${keyword}\` is not allowed at the ${level} level.`;
|
|
1122
|
-
const howToFix = [];
|
|
1123
|
-
if (keyword === "global") {
|
|
1124
|
-
howToFix.push(
|
|
1125
|
-
`Pass${namesPhrase || " the value"} into this function as an argument instead of declaring it \`global\`.`,
|
|
1126
|
-
`If the function needs to *change* a value, return the new value and let the caller decide what to do with it (e.g. \`x = update(x)\`).`
|
|
1127
|
-
);
|
|
1128
|
-
} else {
|
|
1129
|
-
howToFix.push(
|
|
1130
|
-
`Return${namesPhrase || " the new value"} from this inner function and have the outer function rebind it explicitly (e.g. \`x = inner(x)\`).`,
|
|
1131
|
-
`If the inner function only needs to *read* the outer name, just use it directly - you don't need \`nonlocal\` for that.`
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
howToFix.push(
|
|
1135
|
-
`\`#level advanced\` allows \`${keyword}\`, if you have a reason to use it.`
|
|
1136
|
-
);
|
|
1137
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1138
|
-
headline,
|
|
1139
|
-
howToFix
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// src/common/analyzers/static/silenceExplainer.ts
|
|
1144
|
-
var TYPE_FOR = {
|
|
1145
|
-
table: "Table",
|
|
1146
|
-
reactor: "Reactor",
|
|
1147
|
-
row: "dict",
|
|
1148
|
-
string: "str",
|
|
1149
|
-
integer: "int",
|
|
1150
|
-
boolean: "bool",
|
|
1151
|
-
number: "float",
|
|
1152
|
-
Float: "float",
|
|
1153
|
-
Int: "int",
|
|
1154
|
-
Str: "str",
|
|
1155
|
-
Bool: "bool",
|
|
1156
|
-
image: "Image",
|
|
1157
|
-
Number: "float",
|
|
1158
|
-
String: "str",
|
|
1159
|
-
Boolean: "bool",
|
|
1160
|
-
Integer: "int"
|
|
1161
|
-
};
|
|
1162
|
-
var FUNCTION_NOT_TYPE = /* @__PURE__ */ new Set(["table", "reactor"]);
|
|
1163
|
-
function explainUnusedComparison(raw, level, fileName) {
|
|
1164
|
-
const shown = raw.expression ? `\`assert ${raw.expression}\`` : "`assert ...`";
|
|
1165
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1166
|
-
headline: "This comparison's answer is not used, so nothing checks it.",
|
|
1167
|
-
howToFix: [
|
|
1168
|
-
`Did you mean ${shown}? Without \`assert\` the comparison is worked out and thrown away, so the test always passes.`,
|
|
1169
|
-
"At the top level a line like this is displayed; inside a function nothing shows it."
|
|
1170
|
-
]
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
function explainUnusedValue(raw, level, fileName) {
|
|
1174
|
-
const expression = raw.expression ?? null;
|
|
1175
|
-
const field = expression !== null ? /^([A-Za-z_]\w*\.[A-Za-z_]\w*)\s*[-+*/]/.exec(expression) : null;
|
|
1176
|
-
const howToFix = expression === null ? ["Use it: `return` it, assign it to a name, or pass it to `print`."] : field !== null ? [
|
|
1177
|
-
`To change \`${field[1]}\`, assign the result to it: \`${field[1]} = ${expression}\`.`,
|
|
1178
|
-
`Or, if this is the function's answer, return it: \`return ${expression}\`.`
|
|
1179
|
-
] : [
|
|
1180
|
-
`Did you mean \`return ${expression}\`? On its own, the value is worked out and thrown away.`
|
|
1181
|
-
];
|
|
1182
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1183
|
-
headline: "This line works out a value and then throws it away.",
|
|
1184
|
-
howToFix: [...howToFix, "Nothing in Python changes as a result of this line."]
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
function explainAssertTuple(raw, level, fileName) {
|
|
1188
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1189
|
-
headline: "This `assert` has brackets around two things, so it is always true.",
|
|
1190
|
-
howToFix: [
|
|
1191
|
-
"`assert(a, b)` checks a pair, and a pair is never false - the test passes whatever happens.",
|
|
1192
|
-
"Write `assert a == b`, with no brackets after `assert`."
|
|
1193
|
-
]
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
function explainIsLiteral(raw, level, fileName) {
|
|
1197
|
-
const negated = raw.operator === "is not";
|
|
1198
|
-
const literal = raw.literal ?? "a value";
|
|
1199
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1200
|
-
headline: `\`${negated ? "is not" : "is"}\` asks whether two things are the very same object, not whether they are equal.`,
|
|
1201
|
-
howToFix: [
|
|
1202
|
-
`To compare with \`${literal}\`, write \`${negated ? "!=" : "=="}\` instead.`,
|
|
1203
|
-
"`is` is for `None`: `x is None`."
|
|
1204
|
-
]
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
function explainMethodNotCalled(raw, level, fileName) {
|
|
1208
|
-
const method = raw.nameToken ?? "the method";
|
|
1209
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1210
|
-
headline: `\`${method}\` is named here but never called.`,
|
|
1211
|
-
howToFix: [
|
|
1212
|
-
`Add the brackets: \`.${method}()\`.`,
|
|
1213
|
-
`Without them this is the method itself, which displays as \`<bound method ...>\`.`
|
|
1214
|
-
]
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
function explainAnnotationNotAType(raw, level, fileName) {
|
|
1218
|
-
const written = raw.nameToken ?? "";
|
|
1219
|
-
const type = TYPE_FOR[written] ?? null;
|
|
1220
|
-
const headline = FUNCTION_NOT_TYPE.has(written) ? `\`${written}\` is the function that makes a ${written}; the type is \`${type}\`.` : `\`${written}\` is not a type Python knows.`;
|
|
1221
|
-
const exists = FUNCTION_NOT_TYPE.has(written);
|
|
1222
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1223
|
-
headline,
|
|
1224
|
-
howToFix: type !== null ? [
|
|
1225
|
-
`Write \`${type}\` instead.`,
|
|
1226
|
-
exists ? "As it stands the annotation is accepted and nothing about this value is checked." : `There is nothing called \`${written}\`, so this line fails as soon as it runs.`
|
|
1227
|
-
] : ["Nothing about this value is being checked as a result."]
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
function explainTestNotNamed(raw, level, fileName) {
|
|
1231
|
-
const name = raw.nameToken ?? "this function";
|
|
1232
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1233
|
-
headline: `\`${name}\` has an \`assert\` in it, but nothing ever runs it.`,
|
|
1234
|
-
howToFix: [
|
|
1235
|
-
`Rename it \`test_${name}\` and it will run with the other tests.`,
|
|
1236
|
-
"Only functions whose names start with `test_` are run automatically."
|
|
1237
|
-
]
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
function explainFieldNoType(raw, level, fileName) {
|
|
1241
|
-
const name = raw.nameToken ?? "this field";
|
|
1242
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1243
|
-
headline: `The field \`${name}\` has no type, so it is not a field at all.`,
|
|
1244
|
-
howToFix: [
|
|
1245
|
-
`Every field of a dataclass needs a type: \`${name}: int\`, \`${name}: str\`, and so on.`,
|
|
1246
|
-
// Not the NameError it would lead to: this is reported before the
|
|
1247
|
-
// program runs, so that error is never reached.
|
|
1248
|
-
`On a line of its own, \`${name}\` only uses the name; it does not declare anything.`
|
|
1249
|
-
]
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
function explainFieldAssignedType(raw, level, fileName) {
|
|
1253
|
-
const name = raw.nameToken ?? "this field";
|
|
1254
|
-
const type = raw.writtenType ?? "int";
|
|
1255
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1256
|
-
headline: `\`${name} = ${type}\` sets \`${name}\` to the type itself; did you mean \`${name}: ${type}\`?`,
|
|
1257
|
-
howToFix: [
|
|
1258
|
-
"A field is declared with a colon, not an `=`.",
|
|
1259
|
-
`With an \`=\` there is no \`${name}\` field, and the constructor ends up with the wrong number of arguments.`
|
|
1260
|
-
]
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
function explainClassNeedsDataclass(raw, level, fileName) {
|
|
1264
|
-
const name = raw.nameToken ?? "this class";
|
|
1265
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1266
|
-
headline: `\`${name}\` lists fields but has no \`@dataclass\`, so \`${name}(...)\` takes no arguments.`,
|
|
1267
|
-
howToFix: [
|
|
1268
|
-
`Write \`@dataclass\` on the line above \`class ${name}\`.`,
|
|
1269
|
-
"Add `from dataclasses import dataclass` at the top of the file if it is not there."
|
|
1270
|
-
]
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
function explainComparedWithClass(raw, level, fileName) {
|
|
1274
|
-
const name = raw.nameToken ?? "a class";
|
|
1275
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1276
|
-
headline: `\`${name}\` is a class, so comparing a value with it is always False.`,
|
|
1277
|
-
howToFix: [
|
|
1278
|
-
`Use \`match\` to tell the kinds apart: \`case ${name}(...):\`.`,
|
|
1279
|
-
`\`${name}\` is the blueprint; a value made from it is never equal to it.`
|
|
1280
|
-
]
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// src/common/analyzers/static/duplicateDefinitionExplainer.ts
|
|
1285
|
-
function explainDuplicateDefinition(raw, level, fileName) {
|
|
1286
|
-
const name = raw.nameToken ?? "this name";
|
|
1287
|
-
const kind = raw.definitionKind === "class" ? "classes" : "functions";
|
|
1288
|
-
const first = raw.firstLineNumber;
|
|
1289
|
-
const where = typeof first === "number" && first > 0 ? ` (lines ${first} and ${raw.lineNumber})` : "";
|
|
1290
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1291
|
-
headline: `There are two ${kind} named \`${name}\`${where}.`,
|
|
1292
|
-
howToFix: [
|
|
1293
|
-
"Rename one of them. Python keeps only the second, so the first never runs.",
|
|
1294
|
-
kind === "functions" && name.startsWith("test_") ? "A test copied and not renamed vanishes this way: only the last one is run." : "Both definitions are kept in the file, but only the last name points at anything."
|
|
1295
|
-
]
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// src/common/analyzers/static/registry.ts
|
|
1300
|
-
var explainers = {
|
|
1301
|
-
shadowing: explainShadowing,
|
|
1302
|
-
"shadowing-builtin": explainShadowingBuiltin,
|
|
1303
|
-
"shadowing-library": explainShadowingLibrary,
|
|
1304
|
-
reassignment: explainReassignment,
|
|
1305
|
-
"disallowed-keyword": explainDisallowedKeyword,
|
|
1306
|
-
"unused-comparison": explainUnusedComparison,
|
|
1307
|
-
"unused-value": explainUnusedValue,
|
|
1308
|
-
"assert-tuple": explainAssertTuple,
|
|
1309
|
-
"method-not-called": explainMethodNotCalled,
|
|
1310
|
-
"annotation-not-a-type": explainAnnotationNotAType,
|
|
1311
|
-
"test-not-named": explainTestNotNamed,
|
|
1312
|
-
"field-no-type": explainFieldNoType,
|
|
1313
|
-
"field-assigned-type": explainFieldAssignedType,
|
|
1314
|
-
"class-needs-dataclass": explainClassNeedsDataclass,
|
|
1315
|
-
"compared-with-class": explainComparedWithClass,
|
|
1316
|
-
"is-literal": explainIsLiteral,
|
|
1317
|
-
"duplicate-definition": explainDuplicateDefinition
|
|
1318
|
-
};
|
|
1319
|
-
function fallbackExplainer(raw, level, fileName) {
|
|
1320
|
-
return staticFindingFor(raw, level, fileName, {
|
|
1321
|
-
headline: `PLL found a problem here (\`${raw.id}\`) but has no explanation for it.`,
|
|
1322
|
-
howToFix: []
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
function enrichStaticFindings(rawFindings, level, fileName) {
|
|
1326
|
-
return rawFindings.map((raw) => {
|
|
1327
|
-
const explainer = explainers[raw.id] ?? fallbackExplainer;
|
|
1328
|
-
return explainer(raw, level, fileName);
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// src/common/analyzers/checksFailedAnalyzer.ts
|
|
1333
|
-
var checksFailedAnalyzer = {
|
|
1334
|
-
handles: ["ChecksFailed"],
|
|
1335
|
-
analyze(input) {
|
|
1336
|
-
const checks = input.error.facts.checks;
|
|
1337
|
-
if (checks === void 0) {
|
|
1338
|
-
return null;
|
|
584
|
+
const limit = this.options.startTimeoutMs;
|
|
585
|
+
if (limit === void 0) {
|
|
586
|
+
await started;
|
|
587
|
+
return;
|
|
1339
588
|
}
|
|
1340
|
-
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
id: "checks-failed",
|
|
1347
|
-
errorType: "ChecksFailed",
|
|
1348
|
-
headline: headerProblem !== null ? `\`${file}\` was not imported: there is a problem with its \`#level\` line.` : `\`${file}\` was not imported: the checks of \`#level ${level}\` found ${count} in it.`,
|
|
1349
|
-
howToFix: [...problems, `Run \`${file}\` itself to see ${problems.length === 1 ? "it" : "each one"} where it is.`]
|
|
589
|
+
let timer;
|
|
590
|
+
const timedOut = new Promise((_, reject) => {
|
|
591
|
+
timer = setTimeout(
|
|
592
|
+
() => reject(new Error(`Python did not start within ${Math.round(limit / 1e3)} seconds`)),
|
|
593
|
+
limit
|
|
594
|
+
);
|
|
1350
595
|
});
|
|
596
|
+
try {
|
|
597
|
+
await Promise.race([started, timedOut]);
|
|
598
|
+
} finally {
|
|
599
|
+
clearTimeout(timer);
|
|
600
|
+
}
|
|
1351
601
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
var TYPE_WORDS = {
|
|
1356
|
-
int: "whole number",
|
|
1357
|
-
float: "number",
|
|
1358
|
-
complex: "complex number",
|
|
1359
|
-
str: "string",
|
|
1360
|
-
bool: "`True` or `False`",
|
|
1361
|
-
bytes: "bytes",
|
|
1362
|
-
list: "list",
|
|
1363
|
-
dict: "dictionary",
|
|
1364
|
-
set: "set",
|
|
1365
|
-
tuple: "tuple",
|
|
1366
|
-
range: "range",
|
|
1367
|
-
function: "function",
|
|
1368
|
-
None: "`None`",
|
|
1369
|
-
NoneType: "`None`"
|
|
1370
|
-
};
|
|
1371
|
-
function typeWords(type, { article = false } = {}) {
|
|
1372
|
-
const words = TYPE_WORDS[type];
|
|
1373
|
-
if (words === void 0 || !article || words.startsWith("`") || words === "bytes") {
|
|
1374
|
-
return words;
|
|
602
|
+
/** Whether Python has been started, or is starting. */
|
|
603
|
+
get started() {
|
|
604
|
+
return this.initPromise !== null;
|
|
1375
605
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
function punctuated(text) {
|
|
1382
|
-
const trimmed = text.trim();
|
|
1383
|
-
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// src/common/analyzers/runtimeErrorAnalyzer.ts
|
|
1387
|
-
function analyzeRuntimeError(input) {
|
|
1388
|
-
const { error } = input;
|
|
1389
|
-
const place = placeOf(input);
|
|
1390
|
-
const row = inRunFile(input, place) ? rowLine(input.source, place.lineNumber, error.message) : null;
|
|
1391
|
-
return runtimeFindingFor(input, {
|
|
1392
|
-
id: "runtime-error",
|
|
1393
|
-
headline: headlineFor(error.errorType, error.message, error.facts?.stop === true, raisedByStudent(input)),
|
|
1394
|
-
// Deliberately empty. A generic error has no generic remedy, and
|
|
1395
|
-
// inventing one would be worse than the message itself.
|
|
1396
|
-
howToFix: [],
|
|
1397
|
-
lineNumber: row ?? place.lineNumber,
|
|
1398
|
-
column: place.column
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
function rowLine(source, callLine, message) {
|
|
1402
|
-
if (callLine === null) {
|
|
1403
|
-
return null;
|
|
606
|
+
/** A request, once Python has started; `live` is where its output goes as it runs. */
|
|
607
|
+
async request(type, fields, live = null) {
|
|
608
|
+
await this.initialize();
|
|
609
|
+
return this.send(type, fields, live);
|
|
1404
610
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
return null;
|
|
611
|
+
/** `fn(*args)` in Python, and what it returns; `live` streams its output. */
|
|
612
|
+
async call(fn, args, live) {
|
|
613
|
+
return await this.request("call", { fn, args, live: live !== void 0 }, live ?? null);
|
|
1408
614
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1420
|
-
if (!text) {
|
|
1421
|
-
return `${errorType} while running your program.`;
|
|
1422
|
-
}
|
|
1423
|
-
const finish = theirs ? (sentence2) => sentence2 : punctuated;
|
|
1424
|
-
if (errorType === "KeyError") {
|
|
1425
|
-
const unquoted = text.match(/^'([\s\S]*)'$/) ?? text.match(/^"([\s\S]*)"$/);
|
|
1426
|
-
if (unquoted === null || !/\s/.test(unquoted[1])) {
|
|
1427
|
-
return text;
|
|
615
|
+
/**
|
|
616
|
+
* Ask a running program to stop, by raising `KeyboardInterrupt` at the
|
|
617
|
+
* interpreter's next bytecode check. Synchronous on purpose: the worker is
|
|
618
|
+
* blocked inside `runPython` and would not read a message until it
|
|
619
|
+
* returned. Returns false when there is no interrupt channel (no
|
|
620
|
+
* `SharedArrayBuffer`), so callers can say so rather than appear to work.
|
|
621
|
+
*/
|
|
622
|
+
interrupt() {
|
|
623
|
+
if (!this.interruptBuffer) {
|
|
624
|
+
return false;
|
|
1428
625
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// src/common/errors/stockMessageExplainer.ts
|
|
1435
|
-
function explainStockMessage(errorType, message, ctx) {
|
|
1436
|
-
for (const rule of RULES) {
|
|
1437
|
-
if (!rule.types.includes(errorType)) continue;
|
|
1438
|
-
const m = rule.pattern.exec(message);
|
|
1439
|
-
const explanation = m === null ? null : rule.explain(m, ctx, message);
|
|
1440
|
-
if (explanation !== null) {
|
|
1441
|
-
return explanation;
|
|
626
|
+
if (this.stdinBuffer && this.stdinRequest !== null) {
|
|
627
|
+
this.stdinRequest = null;
|
|
628
|
+
interruptStdin(this.stdinBuffer);
|
|
1442
629
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
return [...new Set(RULES.flatMap((rule) => rule.types))];
|
|
1448
|
-
}
|
|
1449
|
-
function friendly(type) {
|
|
1450
|
-
const name = lastSegment(type);
|
|
1451
|
-
return typeWords(name) ?? `\`${name}\``;
|
|
1452
|
-
}
|
|
1453
|
-
function lastSegment(type) {
|
|
1454
|
-
return type.split(".").pop();
|
|
1455
|
-
}
|
|
1456
|
-
function takesClause(names, count) {
|
|
1457
|
-
if (names !== null && names.length > 0) {
|
|
1458
|
-
return `takes ${plural(names.length, "argument")} (${listNames(names)})`;
|
|
1459
|
-
}
|
|
1460
|
-
return `takes ${plural(count, "argument")}`;
|
|
1461
|
-
}
|
|
1462
|
-
function quotedNames(message) {
|
|
1463
|
-
return Array.from(message.matchAll(/'(\w+)'/g), (m) => m[1]);
|
|
1464
|
-
}
|
|
1465
|
-
function definition(ctx, name) {
|
|
1466
|
-
return ctx.facts.definitions?.[name];
|
|
1467
|
-
}
|
|
1468
|
-
function parametersOf(ctx, name, which) {
|
|
1469
|
-
const found = definition(ctx, name);
|
|
1470
|
-
return found?.kind === "function" ? found[which] : null;
|
|
1471
|
-
}
|
|
1472
|
-
function isStudentsClass(ctx, name) {
|
|
1473
|
-
const found = definition(ctx, name);
|
|
1474
|
-
return found?.kind === "class" && found.students;
|
|
1475
|
-
}
|
|
1476
|
-
function fieldsOf(ctx, name) {
|
|
1477
|
-
const found = definition(ctx, name);
|
|
1478
|
-
return found?.kind === "class" && found.fields.length > 0 ? found.fields : null;
|
|
1479
|
-
}
|
|
1480
|
-
function unionMembersOf(ctx, name) {
|
|
1481
|
-
const found = definition(ctx, name);
|
|
1482
|
-
return found?.kind === "union" && found.members.length > 1 ? found.members : null;
|
|
1483
|
-
}
|
|
1484
|
-
var WHY_NONE = [
|
|
1485
|
-
"A function with no `return` gives back `None`.",
|
|
1486
|
-
"A method that changes something in place - `append`, `sort` - also gives back `None`."
|
|
1487
|
-
];
|
|
1488
|
-
function typedOperand(line, facts) {
|
|
1489
|
-
const tokens = tokenize(line);
|
|
1490
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
1491
|
-
if (tokens[i].text !== "+") continue;
|
|
1492
|
-
for (const [at, side] of [[i - 1, "left"], [i + 1, "right"]]) {
|
|
1493
|
-
const token = tokens[at];
|
|
1494
|
-
if (token?.kind !== "name" || tokens[at + 1]?.text === "(" || tokens[at - 1]?.text === ".") {
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
if (facts.assigned?.[token.text]?.call !== "input") {
|
|
1498
|
-
continue;
|
|
630
|
+
const running = new Set(this.pending.keys());
|
|
631
|
+
requestInterrupt(this.interruptBuffer, () => {
|
|
632
|
+
for (const id of running) {
|
|
633
|
+
if (this.pending.has(id)) return true;
|
|
1499
634
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
return { name: token.text, fixed: `${left} + ${right}` };
|
|
1504
|
-
}
|
|
635
|
+
return false;
|
|
636
|
+
});
|
|
637
|
+
return true;
|
|
1505
638
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (receiver !== null && /^[A-Za-z_][\w.]*$/.test(receiver)) {
|
|
1514
|
-
const subject = `\`${receiver}\` is \`None\``;
|
|
1515
|
-
const frames = ctx.frames.filter((frame) => frame.user);
|
|
1516
|
-
const inside = frames.at(-1)?.functionName ?? null;
|
|
1517
|
-
if (inside !== null && (frames.at(-1)?.parameters ?? []).includes(receiver)) {
|
|
1518
|
-
const caller = frames.at(-2);
|
|
1519
|
-
return {
|
|
1520
|
-
subject,
|
|
1521
|
-
howToFix: [
|
|
1522
|
-
`\`${receiver}\` is a parameter of \`${inside}\`, so the call` + (caller !== void 0 ? ` on line ${caller.line}` : "") + ` gave it \`None\`.`,
|
|
1523
|
-
...WHY_NONE
|
|
1524
|
-
]
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
const setFrom = ctx.facts.assigned?.[receiver] ?? null;
|
|
1528
|
-
return {
|
|
1529
|
-
subject,
|
|
1530
|
-
howToFix: setFrom === null ? WHY_NONE : [
|
|
1531
|
-
`\`${receiver}\` was set from \`${setFrom.call}(...)\` on line ${setFrom.line}, which gave back \`None\`.`,
|
|
1532
|
-
...WHY_NONE
|
|
1533
|
-
]
|
|
1534
|
-
};
|
|
639
|
+
/**
|
|
640
|
+
* Told when Python had to be replaced - its worker stopped, or it could
|
|
641
|
+
* no longer run - and so every session's names are gone. The request in
|
|
642
|
+
* flight fails with `PythonLostError`; the next starts a new Python.
|
|
643
|
+
*/
|
|
644
|
+
setPythonLostHandler(handler) {
|
|
645
|
+
this.pythonLostHandler = handler;
|
|
1535
646
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
function withoutJoiningSpace(literal, before) {
|
|
1545
|
-
const quote = literal[0];
|
|
1546
|
-
const inner = literal.slice(1, -1);
|
|
1547
|
-
const trimmed = before ? inner.replace(/ $/, "") : inner.replace(/^ /, "");
|
|
1548
|
-
return `${quote}${trimmed}${quote}`;
|
|
1549
|
-
}
|
|
1550
|
-
var CALLS_YOUR_FUNCTION = [...FUNCTION_TAKER_NAMES, ...HANDLER_KEYWORDS];
|
|
1551
|
-
var functionCalledNotPassed = {
|
|
1552
|
-
types: ["TypeError"],
|
|
1553
|
-
pattern: /^(\w+)\(\) missing \d+ required positional argument/,
|
|
1554
|
-
explain: (m, ctx) => {
|
|
1555
|
-
if (ctx.offendingLine === null) {
|
|
1556
|
-
return null;
|
|
1557
|
-
}
|
|
1558
|
-
const name = m[1];
|
|
1559
|
-
const line = ctx.offendingLine;
|
|
1560
|
-
const taker = CALLS_YOUR_FUNCTION.find(
|
|
1561
|
-
(candidate) => new RegExp(
|
|
1562
|
-
`\\b${candidate}\\b\\s*(?:\\(|=)\\s*(?:[^()]*,\\s*)?${name}\\s*\\(\\s*\\)`
|
|
1563
|
-
).test(line)
|
|
1564
|
-
);
|
|
1565
|
-
if (taker === void 0) {
|
|
1566
|
-
return null;
|
|
1567
|
-
}
|
|
1568
|
-
const keyword = HANDLER_KEYWORDS.includes(taker);
|
|
1569
|
-
return {
|
|
1570
|
-
headline: `\`${taker}\` calls \`${name}\` for you, so it needs the function itself.`,
|
|
1571
|
-
howToFix: [
|
|
1572
|
-
`Leave the brackets off: \`${keyword ? `${taker}=${name}` : `${taker}(${name})`}\`.`,
|
|
1573
|
-
`\`${name}()\` calls it right here, with nothing to work on.`
|
|
1574
|
-
]
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
};
|
|
1578
|
-
var tooFewArguments = {
|
|
1579
|
-
types: ["TypeError"],
|
|
1580
|
-
pattern: /^(\w+)\(\) missing (\d+) required positional argument/,
|
|
1581
|
-
explain: (m, ctx, message) => {
|
|
1582
|
-
const [, name, missingText] = m;
|
|
1583
|
-
const missing = Number(missingText);
|
|
1584
|
-
if (name.startsWith("test_")) {
|
|
1585
|
-
return {
|
|
1586
|
-
headline: `\`${name}\` is a test, so it cannot take any parameters.`,
|
|
1587
|
-
howToFix: [
|
|
1588
|
-
`Write \`def ${name}():\` with nothing in the brackets, and make the values it needs inside it.`
|
|
1589
|
-
]
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
const names = parametersOf(ctx, name, "required");
|
|
1593
|
-
if (names === null) {
|
|
1594
|
-
return {
|
|
1595
|
-
headline: needsMore(name, missing, message),
|
|
1596
|
-
howToFix: ["Check the call on this line - one value is needed for each parameter."]
|
|
1597
|
-
};
|
|
1598
|
-
}
|
|
1599
|
-
return {
|
|
1600
|
-
headline: `\`${name}\` ${takesClause(names, names.length)}, but got ${Math.max(names.length - missing, 0)}.`,
|
|
1601
|
-
howToFix: [
|
|
1602
|
-
"Check the call on this line - one value is needed for each parameter.",
|
|
1603
|
-
...libraryHint(name)
|
|
1604
|
-
]
|
|
1605
|
-
};
|
|
1606
|
-
}
|
|
1607
|
-
};
|
|
1608
|
-
function needsMore(name, missing, message) {
|
|
1609
|
-
const absent = quotedNames(message);
|
|
1610
|
-
return `\`${name}\` needs ${plural(missing, "more argument")}` + (absent.length > 0 ? `: ${listNames(absent)}` : "") + ".";
|
|
1611
|
-
}
|
|
1612
|
-
var tooManyArguments = {
|
|
1613
|
-
types: ["TypeError"],
|
|
1614
|
-
pattern: /^(\w+)\(\) takes (\d+) positional arguments? but (\d+) (?:was|were) given/,
|
|
1615
|
-
explain: (m, ctx) => {
|
|
1616
|
-
const [, name, takesText, givenText] = m;
|
|
1617
|
-
const names = parametersOf(ctx, name, "parameters");
|
|
1618
|
-
const extra = Number(givenText) - (names?.length ?? Number(takesText));
|
|
1619
|
-
return {
|
|
1620
|
-
headline: `\`${name}\` ${takesClause(names, Number(takesText))}, but got ${givenText}.`,
|
|
1621
|
-
howToFix: [
|
|
1622
|
-
extra > 1 ? `Check the call on this line - there are ${extra} values too many.` : "Check the call on this line - there is one value too many.",
|
|
1623
|
-
...libraryHint(name)
|
|
1624
|
-
]
|
|
1625
|
-
};
|
|
647
|
+
/**
|
|
648
|
+
* Who hears what Pyodide says while it loads a package: "Loading pytest,
|
|
649
|
+
* ..." or, `failed`, why it could not. Without one, the host's log.
|
|
650
|
+
*/
|
|
651
|
+
setPackageNoteHandler(handler) {
|
|
652
|
+
this.packageNoteHandler = handler;
|
|
1626
653
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
return {
|
|
1638
|
-
headline: `\`${className}\` lists fields (${listNames(fields)}) but has no \`@dataclass\`, so it takes no arguments.`,
|
|
1639
|
-
howToFix: [
|
|
1640
|
-
`Write \`@dataclass\` on the line above \`class ${className}\`.`,
|
|
1641
|
-
"Add `from dataclasses import dataclass` at the top of the file if it is not there."
|
|
1642
|
-
]
|
|
1643
|
-
};
|
|
654
|
+
/**
|
|
655
|
+
* Register the handler asked for the next of stdin when a running program
|
|
656
|
+
* reads it - `input()`, `sys.stdin.read()`. It resolves with the next text
|
|
657
|
+
* or bytes, exactly as the program should read them (a line typed is that
|
|
658
|
+
* line and its newline), or `null` at the end of stdin. The worker waits
|
|
659
|
+
* until it does, or until a Stop, which raises `KeyboardInterrupt` in the
|
|
660
|
+
* read.
|
|
661
|
+
*/
|
|
662
|
+
setStdinHandler(handler) {
|
|
663
|
+
this.stdinHandler = handler;
|
|
1644
664
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
if (fields === null) {
|
|
1653
|
-
return {
|
|
1654
|
-
headline: `\`${className}\` was given the wrong number of values.`,
|
|
1655
|
-
howToFix: [`Give one value for each field of \`class ${className}\`, in order.`]
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
const given = /takes \d+ positional arguments? but (\d+)/.exec(message);
|
|
1659
|
-
const got = given !== null ? Number(given[1]) - 1 : Math.max(fields.length - Number(m[2]), 0);
|
|
1660
|
-
return {
|
|
1661
|
-
headline: `\`${className}\` needs ${plural(fields.length, "value")} (${listNames(fields)}), but got ${got}.`,
|
|
1662
|
-
howToFix: [
|
|
1663
|
-
`Give one value for each field, in the order they are written in \`class ${className}\`.`
|
|
1664
|
-
]
|
|
1665
|
-
};
|
|
665
|
+
dispose() {
|
|
666
|
+
this.worker?.terminate();
|
|
667
|
+
this.worker = null;
|
|
668
|
+
this.initPromise = null;
|
|
669
|
+
this.stdinBuffer = null;
|
|
670
|
+
this.interruptBuffer = null;
|
|
671
|
+
this.rejectAllPending(new Error("Runtime disposed"));
|
|
1666
672
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
if (m[2] === "__init__") {
|
|
1673
|
-
return null;
|
|
1674
|
-
}
|
|
1675
|
-
const [, owner, method, missingText] = m;
|
|
1676
|
-
const missing = Number(missingText);
|
|
1677
|
-
const required = parametersOf(ctx, `${owner}.${method}`, "required");
|
|
1678
|
-
if (required === null) {
|
|
1679
|
-
return { headline: needsMore(method, missing, message), howToFix: [] };
|
|
673
|
+
/** Send a request to the worker as it is now, and resolve with its reply. */
|
|
674
|
+
send(type, fields, live = null) {
|
|
675
|
+
const worker = this.worker;
|
|
676
|
+
if (!worker) {
|
|
677
|
+
return Promise.reject(new Error(NOT_RUNNING));
|
|
1680
678
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
679
|
+
const id = this.nextId++;
|
|
680
|
+
return new Promise((resolve4, reject) => {
|
|
681
|
+
this.pending.set(id, { resolve: resolve4, reject, live });
|
|
682
|
+
worker.post({ id, type, ...fields });
|
|
683
|
+
});
|
|
1685
684
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
const type = m[1];
|
|
1692
|
-
const asked = m[2];
|
|
1693
|
-
if (lastSegment(type) === "NoneType") {
|
|
1694
|
-
const none = whyNone(ctx, attributeReceiver(ctx.offendingLine, asked));
|
|
1695
|
-
return { headline: `${none.subject}, so it has no \`${asked}\`.`, howToFix: none.howToFix };
|
|
1696
|
-
}
|
|
1697
|
-
const fields = fieldsOf(ctx, lastSegment(type));
|
|
1698
|
-
if (fields === null) {
|
|
1699
|
-
const name = lastSegment(type);
|
|
1700
|
-
if (isStudentsClass(ctx, name)) {
|
|
1701
|
-
return {
|
|
1702
|
-
headline: `\`${name}\` has no fields at all, so it has no \`${asked}\`.`,
|
|
1703
|
-
howToFix: [
|
|
1704
|
-
`A value that might be a \`${name}\` needs a \`case ${name}():\` before \`.${asked}\` is used.`
|
|
1705
|
-
]
|
|
1706
|
-
};
|
|
685
|
+
handleMessage(msg, worker) {
|
|
686
|
+
if (msg.type === "display") {
|
|
687
|
+
const live = this.pending.get(msg.requestId)?.live;
|
|
688
|
+
if (live) {
|
|
689
|
+
deliverDisplay(msg.payload, live.onEvent, live.fileName);
|
|
1707
690
|
}
|
|
1708
|
-
return
|
|
1709
|
-
headline: `A ${friendly(type)} has no \`${asked}\`.`,
|
|
1710
|
-
howToFix: []
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
const suggestion = closestName(asked, fields);
|
|
1714
|
-
return {
|
|
1715
|
-
headline: `\`${lastSegment(type)}\` has no field \`${asked}\` (its fields are ${listNames(fields)}).`,
|
|
1716
|
-
howToFix: suggestion !== null ? [`Did you mean \`${suggestion}\`?`] : []
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
var attributeOnClass = {
|
|
1721
|
-
types: ["AttributeError"],
|
|
1722
|
-
pattern: /^type object '(\w+)' has no attribute '(\w+)'/,
|
|
1723
|
-
explain: (m, ctx) => {
|
|
1724
|
-
const [, className, asked] = m;
|
|
1725
|
-
const fields = fieldsOf(ctx, className);
|
|
1726
|
-
if (fields === null) {
|
|
1727
|
-
return null;
|
|
1728
|
-
}
|
|
1729
|
-
return {
|
|
1730
|
-
headline: `\`${className}\` on its own is the class, not one made from it, so it has no \`${asked}\`.`,
|
|
1731
|
-
howToFix: [
|
|
1732
|
-
`\`${className}(...)\` makes one, with a value for each field (${listNames(fields)}).`,
|
|
1733
|
-
"A field belongs to the value, not to the class it was made from."
|
|
1734
|
-
]
|
|
1735
|
-
};
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
|
-
var notSubscriptable = {
|
|
1739
|
-
types: ["TypeError"],
|
|
1740
|
-
pattern: /^'([\w.]+)' object is not subscriptable/,
|
|
1741
|
-
explain: (m, ctx) => {
|
|
1742
|
-
const type = m[1];
|
|
1743
|
-
if (lastSegment(type) === "NoneType") {
|
|
1744
|
-
const none = whyNone(ctx, subscriptReceiver(ctx.offendingLine));
|
|
1745
|
-
return {
|
|
1746
|
-
headline: `${none.subject}, so square brackets cannot get anything out of it.`,
|
|
1747
|
-
howToFix: none.howToFix
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
const method = /([A-Za-z_][\w.]*)\.(\w+)\s*\[\s*([^\]]*)\]/.exec(ctx.offendingLine ?? "");
|
|
1751
|
-
if (lastSegment(type) === "method" && method !== null) {
|
|
1752
|
-
const [, holder2, name, inside] = method;
|
|
1753
|
-
return {
|
|
1754
|
-
headline: `\`${holder2}.${name}\` is a method, so it is called with round brackets, not indexed with square ones.`,
|
|
1755
|
-
howToFix: [
|
|
1756
|
-
name === "row" ? `Write \`${holder2}.row(${inside})\`: the row number goes in the round brackets.` : `Call it first: \`${holder2}.${name}()[${inside}]\`.`
|
|
1757
|
-
]
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
const fields = fieldsOf(ctx, lastSegment(type));
|
|
1761
|
-
if (fields === null) {
|
|
1762
|
-
const caller = libraryCaller(ctx.frames);
|
|
1763
|
-
const taker = caller !== null ? FUNCTION_TAKERS[caller] : void 0;
|
|
1764
|
-
return {
|
|
1765
|
-
headline: `Square brackets do not work on a ${friendly(type)}.`,
|
|
1766
|
-
howToFix: [
|
|
1767
|
-
...caller === "transform_column" && taker !== void 0 ? [`${taker.calls}, so what your function is given is already the value - there is nothing to take out of it.`] : [],
|
|
1768
|
-
"Square brackets are for lists, dictionaries and table rows."
|
|
1769
|
-
]
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
const subscript = /([A-Za-z_][\w.]*)\s*\[\s*["'](\w+)["']\s*\]/.exec(ctx.offendingLine ?? "");
|
|
1773
|
-
const holder = subscript !== null ? subscript[1] : "value";
|
|
1774
|
-
const field = subscript !== null ? subscript[2] : fields[0];
|
|
1775
|
-
return {
|
|
1776
|
-
headline: `Square brackets do not get a field out of \`${lastSegment(type)}\`.`,
|
|
1777
|
-
howToFix: [
|
|
1778
|
-
`Use a dot instead: \`${holder}.${field}\` rather than \`${holder}["${field}"]\`.`,
|
|
1779
|
-
"Square brackets are for lists, dictionaries and table rows."
|
|
1780
|
-
]
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
var notIterable = {
|
|
1785
|
-
types: ["TypeError"],
|
|
1786
|
-
pattern: /^'([\w.]+)' object is not iterable/,
|
|
1787
|
-
explain: (m, ctx) => {
|
|
1788
|
-
const over = /\bfor\s+\w+\s+in\s+len\s*\(\s*([A-Za-z_][\w.]*)\s*\)/.exec(
|
|
1789
|
-
ctx.offendingLine ?? ""
|
|
1790
|
-
);
|
|
1791
|
-
if (over !== null) {
|
|
1792
|
-
return {
|
|
1793
|
-
headline: `\`len(${over[1]})\` is a number, and a number is not something to loop over.`,
|
|
1794
|
-
howToFix: [
|
|
1795
|
-
`To visit each item, write \`for item in ${over[1]}:\`.`,
|
|
1796
|
-
`To count, write \`for i in range(len(${over[1]})):\` - but the first form is usually what you want.`
|
|
1797
|
-
]
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
if (lastSegment(m[1]) === "NoneType") {
|
|
1801
|
-
const none = whyNone(ctx, loopedOver(ctx.offendingLine));
|
|
1802
|
-
return { headline: `${none.subject}, so there is nothing to loop over.`, howToFix: none.howToFix };
|
|
1803
|
-
}
|
|
1804
|
-
const looped = loopedOver(ctx.offendingLine);
|
|
1805
|
-
if (lastSegment(m[1]) === "method" && looped !== null && /\.\w+$/.test(looped)) {
|
|
1806
|
-
return {
|
|
1807
|
-
headline: `\`${looped}\` is the method itself, not what it gives back - it has not been called.`,
|
|
1808
|
-
howToFix: [`Call it, with brackets: \`for ... in ${looped}():\`.`]
|
|
1809
|
-
};
|
|
691
|
+
return;
|
|
1810
692
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
};
|
|
1817
|
-
var concatMismatch = {
|
|
1818
|
-
types: ["TypeError"],
|
|
1819
|
-
pattern: /can only concatenate (str|list|tuple) \(not "([\w.]+)"\) to \1/,
|
|
1820
|
-
explain: (m, ctx) => {
|
|
1821
|
-
const [, container, other] = m;
|
|
1822
|
-
const headline = `A ${friendly(container)} and a ${friendly(other)} cannot be added together.`;
|
|
1823
|
-
if (container === "str") {
|
|
1824
|
-
const line = ctx.offendingLine ?? "";
|
|
1825
|
-
const pair = concatOperands(line);
|
|
1826
|
-
const howToFix = [];
|
|
1827
|
-
if (pair !== null) {
|
|
1828
|
-
const { text, value, textFirst } = pair;
|
|
1829
|
-
howToFix.push(
|
|
1830
|
-
`To join them as text, convert \`${value}\` with \`str\`: \`${textFirst ? `${text} + str(${value})` : `str(${value}) + ${text}`}\`.`
|
|
1831
|
-
);
|
|
1832
|
-
if (/^\s*print\s*\(/.test(line)) {
|
|
1833
|
-
const spaced = withoutJoiningSpace(text, textFirst);
|
|
1834
|
-
howToFix.push(
|
|
1835
|
-
`In \`print\`, a comma needs no conversion at all, and puts a space in for you: \`print(${textFirst ? `${spaced}, ${value}` : `${value}, ${spaced}`})\`.`
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
} else {
|
|
1839
|
-
const typed = typedOperand(line, ctx.facts);
|
|
1840
|
-
if (typed !== null) {
|
|
1841
|
-
return {
|
|
1842
|
-
headline,
|
|
1843
|
-
howToFix: [
|
|
1844
|
-
`\`${typed.name}\` came from \`input\`, which always gives back text - even when a number is typed.`,
|
|
1845
|
-
`Convert it first: \`${typed.fixed}\`.`
|
|
1846
|
-
]
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
|
-
howToFix.push(
|
|
1850
|
-
"To join them as text, convert the other value with `str(...)`.",
|
|
1851
|
-
"In `print`, separate them with commas instead, which needs no conversion."
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
howToFix.push("To add them as numbers instead, convert the text with `int(...)` or `float(...)`.");
|
|
1855
|
-
return { headline, howToFix };
|
|
693
|
+
if (msg.type === "stdinRequest") {
|
|
694
|
+
void this.handleStdinRequest(msg.request);
|
|
695
|
+
return;
|
|
1856
696
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
return {
|
|
1862
|
-
headline,
|
|
1863
|
-
howToFix: [
|
|
1864
|
-
`\`+\` joins a ${container} to another ${container}. To add one item, put it inside ${container === "list" ? "brackets" : "a tuple"}: \`${named} + ${open}item${close}\`.`,
|
|
1865
|
-
...container === "list" ? [`Or add it in place with \`${named}.append(item)\`, which changes \`${named}\` and returns nothing.`] : []
|
|
1866
|
-
]
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
};
|
|
1870
|
-
var operandMismatch = {
|
|
1871
|
-
types: ["TypeError"],
|
|
1872
|
-
pattern: /^unsupported operand type\(s\) for (\S+?): '([\w.]+)' and '([\w.]+)'/,
|
|
1873
|
-
explain: (m, ctx) => {
|
|
1874
|
-
const [, operator, left, right] = m;
|
|
1875
|
-
if (left !== "NoneType" && right !== "NoneType") {
|
|
1876
|
-
return {
|
|
1877
|
-
headline: `\`${operator}\` does not work between a ${friendly(left)} and a ${friendly(right)}.`,
|
|
1878
|
-
howToFix: left === "str" !== (right === "str") ? ["Convert one of them first - `int(...)` for a number, `str(...)` for text."] : []
|
|
1879
|
-
};
|
|
697
|
+
if (msg.type === "packageNote") {
|
|
698
|
+
if (this.packageNoteHandler) this.packageNoteHandler(msg.text, msg.failed);
|
|
699
|
+
else console.error(msg.text);
|
|
700
|
+
return;
|
|
1880
701
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
};
|
|
1886
|
-
var COMPARISONS = /* @__PURE__ */ new Set(["<", ">", "<=", ">=", "==", "!="]);
|
|
1887
|
-
var conditionNotFunction = {
|
|
1888
|
-
types: ["TypeError"],
|
|
1889
|
-
pattern: /not supported between instances of|unsupported operand type/,
|
|
1890
|
-
explain: (_m, ctx) => {
|
|
1891
|
-
if (ctx.offendingLine === null) {
|
|
1892
|
-
return null;
|
|
702
|
+
if (msg.type === "error" && msg.kind === "finished") {
|
|
703
|
+
this.pythonLost(worker);
|
|
704
|
+
return;
|
|
1893
705
|
}
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const taker = FUNCTION_TAKERS[tokens[i].text];
|
|
1898
|
-
if (taker?.each === void 0 || tokens[i].kind !== "name" || tokens[i + 1]?.text !== "(") {
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
1901
|
-
const shut = closing(tokens, i + 1);
|
|
1902
|
-
if (shut < 0) continue;
|
|
1903
|
-
const inside = tokens.slice(i + 2, shut);
|
|
1904
|
-
let depth = 0;
|
|
1905
|
-
let compares = false;
|
|
1906
|
-
let isFunction = false;
|
|
1907
|
-
for (const token of inside) {
|
|
1908
|
-
if (token.kind === "open") depth++;
|
|
1909
|
-
if (token.kind === "close") depth--;
|
|
1910
|
-
if (depth === 0 && COMPARISONS.has(token.text)) compares = true;
|
|
1911
|
-
if (token.text === "lambda") isFunction = true;
|
|
1912
|
-
}
|
|
1913
|
-
if (!compares || isFunction) continue;
|
|
1914
|
-
const name = tokens[i].text;
|
|
1915
|
-
const receiver = tokens[i - 1]?.text === "." ? tokens[i - 2]?.text ?? null : null;
|
|
1916
|
-
let condition = "";
|
|
1917
|
-
let named = null;
|
|
1918
|
-
let cursor = tokens[i + 2]?.start ?? 0;
|
|
1919
|
-
for (let k = 0; k < inside.length; k++) {
|
|
1920
|
-
const token = inside[k];
|
|
1921
|
-
const nextOp = inside[k + 1]?.text;
|
|
1922
|
-
const prevOp = inside[k - 1]?.text;
|
|
1923
|
-
condition += line.slice(cursor, token.start);
|
|
1924
|
-
if (token.kind === "str" && name !== "transform_column" && (COMPARISONS.has(nextOp ?? "") || COMPARISONS.has(prevOp ?? ""))) {
|
|
1925
|
-
condition += `r[${token.text}]`;
|
|
1926
|
-
named = named ?? token.text;
|
|
1927
|
-
} else {
|
|
1928
|
-
condition += token.text;
|
|
1929
|
-
}
|
|
1930
|
-
cursor = token.end;
|
|
1931
|
-
}
|
|
1932
|
-
condition = condition.trim();
|
|
1933
|
-
const call = receiver !== null ? `${receiver}.${name}(keep)` : `${name}(keep)`;
|
|
1934
|
-
return {
|
|
1935
|
-
headline: `\`${name}\` needs a function, but this gives it a condition.`,
|
|
1936
|
-
howToFix: [
|
|
1937
|
-
`\`${name}\` calls a function on ${taker.each} and ${taker.keeps}, so the condition belongs inside a function. Here it was worked out first - which is what the error is about.`,
|
|
1938
|
-
`Write \`def keep(${taker.param}) -> bool:\` with \`return ${condition}\` as its body, then \`${call}\`.`,
|
|
1939
|
-
...named !== null ? [
|
|
1940
|
-
`On its own, \`${named}\` is just text. The value in that column of a row is \`r[${named}]\`.`
|
|
1941
|
-
] : []
|
|
1942
|
-
]
|
|
1943
|
-
};
|
|
706
|
+
const pending = this.pending.get(msg.id);
|
|
707
|
+
if (!pending) {
|
|
708
|
+
return;
|
|
1944
709
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
var comparisonMismatch = {
|
|
1949
|
-
types: ["TypeError"],
|
|
1950
|
-
pattern: /^'(\S+)' not supported between instances of '([\w.]+)' and '([\w.]+)'/,
|
|
1951
|
-
explain: (m) => {
|
|
1952
|
-
const [, operator, left, right] = m;
|
|
1953
|
-
const howToFix = [];
|
|
1954
|
-
const text = left === "str" ? left : right === "str" ? right : null;
|
|
1955
|
-
const number = numericType(left) ?? numericType(right);
|
|
1956
|
-
if (text !== null && number !== null) {
|
|
1957
|
-
howToFix.push(
|
|
1958
|
-
`Convert the text to a number with \`${number}(...)\`, or the number to text with \`str(...)\` - whichever this comparison is meant to be about.`
|
|
1959
|
-
);
|
|
710
|
+
this.pending.delete(msg.id);
|
|
711
|
+
if (msg.type === "error") {
|
|
712
|
+
pending.reject(msg.kind === "interrupted" ? new StoppedError(msg.message) : new Error(msg.message));
|
|
1960
713
|
} else {
|
|
1961
|
-
|
|
1962
|
-
`\`${operator}\` only orders values of the same kind; there is no answer to this comparison.`
|
|
1963
|
-
);
|
|
1964
|
-
}
|
|
1965
|
-
return {
|
|
1966
|
-
headline: `\`${operator}\` cannot compare a ${friendly(left)} with a ${friendly(right)}.`,
|
|
1967
|
-
howToFix
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
};
|
|
1971
|
-
function numericType(type) {
|
|
1972
|
-
const name = lastSegment(type);
|
|
1973
|
-
return name === "int" || name === "float" ? name : null;
|
|
1974
|
-
}
|
|
1975
|
-
var badIntLiteral = {
|
|
1976
|
-
types: ["ValueError"],
|
|
1977
|
-
pattern: /^invalid literal for int\(\) with base 10: (.+)$/,
|
|
1978
|
-
explain: (m, ctx) => {
|
|
1979
|
-
const raw = m[1].trim();
|
|
1980
|
-
const empty = raw === "''" || raw === '""';
|
|
1981
|
-
const shown = /^'[^'"]*'$/.test(raw) ? `"${raw.slice(1, -1)}"` : raw;
|
|
1982
|
-
const howToFix = ['`int` only reads whole numbers written in digits, like `"19"`.'];
|
|
1983
|
-
if (/\binput\s*\(/.test(ctx.offendingLine ?? "")) {
|
|
1984
|
-
howToFix.push(
|
|
1985
|
-
empty ? "Nothing was typed: pressing Enter on its own makes `input` give back an empty string." : "`input` gives back exactly what was typed, as text, so a word cannot become a number."
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
return {
|
|
1989
|
-
headline: empty ? "`int` cannot turn an empty string into a whole number." : `\`int\` cannot turn ${shown} into a whole number.`,
|
|
1990
|
-
howToFix
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
};
|
|
1994
|
-
var indexRange = {
|
|
1995
|
-
types: ["IndexError"],
|
|
1996
|
-
pattern: /index out of range/,
|
|
1997
|
-
explain: (_m, ctx) => {
|
|
1998
|
-
const subscript = /([A-Za-z_][\w.]*)\s*\[\s*([^\]]+?)\s*\]/.exec(ctx.offendingLine ?? "");
|
|
1999
|
-
const named = subscript !== null ? `\`${subscript[1]}\`` : "This list";
|
|
2000
|
-
const { sequence, length } = ctx.facts;
|
|
2001
|
-
if (sequence !== void 0 && length !== void 0) {
|
|
2002
|
-
return {
|
|
2003
|
-
headline: `\`${sequence}\` has no item at that position.`,
|
|
2004
|
-
howToFix: [
|
|
2005
|
-
length === 0 ? `\`${sequence}\` is empty, so it has no items at all.` : `\`${sequence}\` has ${length} item${length === 1 ? "" : "s"}, numbered 0 to ${length - 1} - positions start at 0.`
|
|
2006
|
-
]
|
|
2007
|
-
};
|
|
714
|
+
pending.resolve(msg.value);
|
|
2008
715
|
}
|
|
2009
|
-
return {
|
|
2010
|
-
headline: `${named} has no item at that position.`,
|
|
2011
|
-
howToFix: [
|
|
2012
|
-
"Positions start at 0, so the last item of a list is at position `len(...) - 1`.",
|
|
2013
|
-
subscript !== null ? `\`len(${subscript[1]})\` gives the number of items.` : "`len(...)` gives the number of items."
|
|
2014
|
-
]
|
|
2015
|
-
};
|
|
2016
716
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
headline: `Brackets after \`${number}\` look like a function call, not multiplication.`,
|
|
2036
|
-
howToFix: [
|
|
2037
|
-
`To multiply, write the \`*\`: \`${number} * ${inside.trim() || "..."}\`, not \`${written}\`.`
|
|
2038
|
-
]
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
|
-
return {
|
|
2042
|
-
headline: `A ${friendly(m[1])} cannot be called like a function.`,
|
|
2043
|
-
howToFix: ["Brackets after a value mean a call; check whether a `*` or a comma is missing."]
|
|
2044
|
-
};
|
|
717
|
+
/**
|
|
718
|
+
* Python is gone: the worker exited (only a Node worker can say so), or
|
|
719
|
+
* the interpreter in it is finished and refuses everything. Either way
|
|
720
|
+
* nothing sent to it can succeed. Fail what was waiting, end the worker,
|
|
721
|
+
* and forget it, so that the next request starts a new Python.
|
|
722
|
+
*
|
|
723
|
+
* Every session's names went with the old one - but they were gone
|
|
724
|
+
* already: this follows Python stopping by itself, and never ends a
|
|
725
|
+
* Python that could still run. Whoever set `setPythonLostHandler` is told,
|
|
726
|
+
* so that each file can say so rather than only the one that was running.
|
|
727
|
+
*/
|
|
728
|
+
pythonLost(worker) {
|
|
729
|
+
if (this.worker !== worker) return;
|
|
730
|
+
this.rejectAllPending(new PythonLostError());
|
|
731
|
+
this.worker = null;
|
|
732
|
+
this.initPromise = null;
|
|
733
|
+
worker.terminate();
|
|
734
|
+
this.pythonLostHandler?.();
|
|
2045
735
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
pattern: /^reactor's `(\w+)` has to be a function/,
|
|
2050
|
-
explain: (m, ctx) => {
|
|
2051
|
-
if (ctx.offendingLine === null) {
|
|
2052
|
-
return null;
|
|
2053
|
-
}
|
|
2054
|
-
const handler = m[1];
|
|
2055
|
-
const line = ctx.offendingLine;
|
|
2056
|
-
const tokens = tokenize(line);
|
|
2057
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2058
|
-
if (tokens[i].text !== handler || tokens[i + 1]?.text !== "=") continue;
|
|
2059
|
-
const called = operandFrom(line, tokens, i + 2);
|
|
2060
|
-
const fn = tokens[i + 2]?.kind === "name" ? tokens[i + 2].text : null;
|
|
2061
|
-
if (called === null || fn === null || called === fn) continue;
|
|
2062
|
-
return {
|
|
2063
|
-
headline: `\`${handler}\` has to be given the function itself, not what calling it gives back.`,
|
|
2064
|
-
howToFix: [
|
|
2065
|
-
`Write \`${handler}=${fn}\`, not \`${handler}=${called}\`.`,
|
|
2066
|
-
`The reactor calls \`${fn}\` itself, with its state, every time it needs a picture.`
|
|
2067
|
-
]
|
|
2068
|
-
};
|
|
736
|
+
rejectAllPending(err) {
|
|
737
|
+
for (const pending of this.pending.values()) {
|
|
738
|
+
pending.reject(err);
|
|
2069
739
|
}
|
|
2070
|
-
|
|
2071
|
-
}
|
|
2072
|
-
};
|
|
2073
|
-
var patternArity = {
|
|
2074
|
-
types: ["TypeError"],
|
|
2075
|
-
pattern: /^(\w+)\(\) accepts (\d+) positional sub-patterns \((\d+) given\)/,
|
|
2076
|
-
explain: (m, ctx) => {
|
|
2077
|
-
const [, className, accepts, given] = m;
|
|
2078
|
-
const fields = fieldsOf(ctx, className);
|
|
2079
|
-
return {
|
|
2080
|
-
headline: `\`${className}\` has ${plural(Number(accepts), "field")}` + (fields !== null ? ` (${listNames(fields)})` : "") + `, but this pattern names ${given}.`,
|
|
2081
|
-
howToFix: fields !== null ? [`Write one name for each field: \`case ${className}(${fields.join(", ")}):\`.`] : ["Write one name for each field, in order."]
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
};
|
|
2085
|
-
var missingComma = {
|
|
2086
|
-
types: ["TypeError"],
|
|
2087
|
-
pattern: /list indices must be integers or slices, not tuple/,
|
|
2088
|
-
explain: () => {
|
|
2089
|
-
return {
|
|
2090
|
-
headline: "A comma is missing between two values in a list.",
|
|
2091
|
-
howToFix: [
|
|
2092
|
-
"`[a, b] [c, d]` with no comma reads as looking `c, d` up inside the first list.",
|
|
2093
|
-
"Write `[[a, b], [c, d]]`, with a comma after every row."
|
|
2094
|
-
]
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
};
|
|
2098
|
-
var recursionDepth = {
|
|
2099
|
-
types: ["RecursionError"],
|
|
2100
|
-
pattern: /maximum recursion depth/,
|
|
2101
|
-
explain: (_m, ctx) => {
|
|
2102
|
-
const repeated = repeatedFunction(ctx.frames);
|
|
2103
|
-
const named = repeated !== null ? `\`${repeated}\`` : "A function";
|
|
2104
|
-
return {
|
|
2105
|
-
headline: `${named} kept calling itself and never stopped.`,
|
|
2106
|
-
howToFix: [
|
|
2107
|
-
"Each call has to work on something smaller - the rest of the list, a smaller number.",
|
|
2108
|
-
"Check that there is a case that returns without calling again, and that it is reached."
|
|
2109
|
-
]
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
};
|
|
2113
|
-
function repeatedFunction(frames) {
|
|
2114
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2115
|
-
for (const { functionName } of frames) {
|
|
2116
|
-
if (functionName !== null) counts.set(functionName, (counts.get(functionName) ?? 0) + 1);
|
|
740
|
+
this.pending.clear();
|
|
2117
741
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
742
|
+
/**
|
|
743
|
+
* The next of stdin from the handler. One call at a time: a request a
|
|
744
|
+
* Stop ended leaves its call waiting, and the next request - a program
|
|
745
|
+
* that caught the `KeyboardInterrupt` and asked again - shares it, so
|
|
746
|
+
* the line typed answers that one.
|
|
747
|
+
*/
|
|
748
|
+
nextStdin() {
|
|
749
|
+
if (this.stdinNext === null) {
|
|
750
|
+
const call = (async () => {
|
|
751
|
+
try {
|
|
752
|
+
const next = this.stdinHandler ? await this.stdinHandler() : null;
|
|
753
|
+
return typeof next === "string" ? new TextEncoder().encode(next) : next;
|
|
754
|
+
} catch {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
})();
|
|
758
|
+
this.stdinNext = call;
|
|
759
|
+
void call.then(() => {
|
|
760
|
+
if (this.stdinNext === call) this.stdinNext = null;
|
|
761
|
+
});
|
|
2124
762
|
}
|
|
763
|
+
return this.stdinNext;
|
|
2125
764
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
if (!insidePandas) {
|
|
2149
|
-
return null;
|
|
2150
|
-
}
|
|
2151
|
-
const key = /^'([\s\S]*)'$/.exec(message.trim());
|
|
2152
|
-
if (key === null) {
|
|
2153
|
-
return null;
|
|
2154
|
-
}
|
|
2155
|
-
const frame = /([A-Za-z_][\w.]*)\s*\[/.exec(ctx.offendingLine ?? "");
|
|
2156
|
-
return {
|
|
2157
|
-
headline: `There is no column named \`${key[1]}\`.`,
|
|
2158
|
-
howToFix: [
|
|
2159
|
-
frame !== null ? `\`print(${frame[1]}.columns)\` lists the columns this DataFrame has.` : "`.columns` lists the columns a DataFrame has.",
|
|
2160
|
-
"Column names are case-sensitive, and spaces count."
|
|
2161
|
-
]
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
};
|
|
2165
|
-
var missingModule = {
|
|
2166
|
-
types: ["ModuleNotFoundError"],
|
|
2167
|
-
pattern: /^No module named/,
|
|
2168
|
-
explain: (_m, ctx) => {
|
|
2169
|
-
const module2 = ctx.facts.module;
|
|
2170
|
-
if (module2 === void 0) {
|
|
2171
|
-
return null;
|
|
2172
|
-
}
|
|
2173
|
-
const name = module2.name;
|
|
2174
|
-
switch (module2.kind) {
|
|
2175
|
-
case "leftOut":
|
|
2176
|
-
return {
|
|
2177
|
-
headline: `\`${name}.py\` is next to your program, but it was not loaded: ${module2.why}.`,
|
|
2178
|
-
howToFix: ["A file that was not loaded cannot be imported."]
|
|
2179
|
-
};
|
|
2180
|
-
case "notLoaded":
|
|
2181
|
-
return {
|
|
2182
|
-
headline: `\`${name}\` could not be loaded before your program ran.`,
|
|
2183
|
-
howToFix: [
|
|
2184
|
-
`PLL downloads \`${module2.package}\` the first time a program imports it, so this needs the internet once; after that it is kept.`,
|
|
2185
|
-
"Check the connection, and run the program again."
|
|
2186
|
-
]
|
|
2187
|
-
};
|
|
2188
|
-
case "notSeen":
|
|
2189
|
-
return {
|
|
2190
|
-
headline: `\`${name}\` was not loaded, because PLL did not see it imported.`,
|
|
2191
|
-
howToFix: [
|
|
2192
|
-
`PLL loads a package before the program runs, from its \`import\` lines - and this import is made another way, with a name worked out as it runs.`,
|
|
2193
|
-
`Add \`import ${name}\` near the top of the file.`
|
|
2194
|
-
]
|
|
2195
|
-
};
|
|
2196
|
-
case "missing":
|
|
2197
|
-
if (module2.close !== null) {
|
|
2198
|
-
return {
|
|
2199
|
-
headline: `There is no module called \`${name}\` - and no file \`${name}.py\` next to your program.`,
|
|
2200
|
-
howToFix: [`Did you mean \`${module2.close}\`, for \`${module2.close}.py\`?`]
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
return {
|
|
2204
|
-
headline: `There is no module called \`${name}\` here.`,
|
|
2205
|
-
howToFix: [
|
|
2206
|
-
`PLL runs Python with Pyodide, which has many packages - numpy, pandas, matplotlib, requests and more - but not \`${name}\`.`,
|
|
2207
|
-
`If \`${name}\` is a file of your own, put \`${name}.py\` next to your program.`
|
|
2208
|
-
]
|
|
2209
|
-
};
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
};
|
|
2213
|
-
var builtinOnTable = {
|
|
2214
|
-
types: ["TypeError"],
|
|
2215
|
-
pattern: /Table|^a table /,
|
|
2216
|
-
explain: (_m, ctx) => {
|
|
2217
|
-
const call = /\b(sum|min|max|len|sorted)\s*\(\s*([A-Za-z_][\w.]*)\s*,\s*("[^"]*"|'[^']*')\s*\)/.exec(
|
|
2218
|
-
ctx.offendingLine ?? ""
|
|
2219
|
-
);
|
|
2220
|
-
if (call === null) {
|
|
2221
|
-
return null;
|
|
2222
|
-
}
|
|
2223
|
-
const [, fn, table, column] = call;
|
|
2224
|
-
const method = fn === "len" ? "length" : fn === "sorted" ? "order_by" : fn;
|
|
2225
|
-
return {
|
|
2226
|
-
headline: `\`${fn}(${table}, ${column})\` is Python's own \`${fn}\`, which knows nothing of tables.`,
|
|
2227
|
-
howToFix: [`A table does this itself: \`${table}.${method}(${method === "length" ? "" : column})\`.`]
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
};
|
|
2231
|
-
var RULES = [
|
|
2232
|
-
missingModule,
|
|
2233
|
-
builtinOnTable,
|
|
2234
|
-
conditionNotFunction,
|
|
2235
|
-
handlerCalled,
|
|
2236
|
-
functionCalledNotPassed,
|
|
2237
|
-
missingDataclass,
|
|
2238
|
-
dataclassArity,
|
|
2239
|
-
tooFewArguments,
|
|
2240
|
-
tooManyArguments,
|
|
2241
|
-
methodArity,
|
|
2242
|
-
attributeOnClass,
|
|
2243
|
-
noSuchAttribute,
|
|
2244
|
-
notSubscriptable,
|
|
2245
|
-
notIterable,
|
|
2246
|
-
concatMismatch,
|
|
2247
|
-
operandMismatch,
|
|
2248
|
-
comparisonMismatch,
|
|
2249
|
-
badIntLiteral,
|
|
2250
|
-
indexRange,
|
|
2251
|
-
notCallable,
|
|
2252
|
-
patternArity,
|
|
2253
|
-
missingComma,
|
|
2254
|
-
recursionDepth,
|
|
2255
|
-
unevenColumns,
|
|
2256
|
-
pandasKeyError
|
|
2257
|
-
];
|
|
2258
|
-
|
|
2259
|
-
// src/common/analyzers/stockMessageAnalyzer.ts
|
|
2260
|
-
var stockMessageAnalyzer = {
|
|
2261
|
-
// Exactly what the rules are for: the list is theirs, not a copy of it.
|
|
2262
|
-
handles: stockErrorTypes(),
|
|
2263
|
-
analyze(input) {
|
|
2264
|
-
const { error } = input;
|
|
2265
|
-
const explanation = explainStockMessage(error.errorType, error.message, {
|
|
2266
|
-
// The line that raised, which several rules need - `for x in len(xs)`
|
|
2267
|
-
// is only told apart from any other `'int' object is not iterable` by
|
|
2268
|
-
// reading it.
|
|
2269
|
-
offendingLine: placeOf(input).text,
|
|
2270
|
-
frames: error.frames,
|
|
2271
|
-
facts: error.facts
|
|
2272
|
-
});
|
|
2273
|
-
if (explanation === null) {
|
|
2274
|
-
return null;
|
|
2275
|
-
}
|
|
2276
|
-
return {
|
|
2277
|
-
...analyzeRuntimeError(input),
|
|
2278
|
-
id: "stock-message",
|
|
2279
|
-
headline: explanation.headline,
|
|
2280
|
-
howToFix: explanation.howToFix
|
|
2281
|
-
};
|
|
2282
|
-
}
|
|
2283
|
-
};
|
|
2284
|
-
|
|
2285
|
-
// src/common/errors/syntaxExplainer.ts
|
|
2286
|
-
function explainSyntaxError(errorType, message, offendingLine) {
|
|
2287
|
-
const error = { errorType, message, line: (offendingLine ?? "").trim() };
|
|
2288
|
-
for (const rule of RULES2) {
|
|
2289
|
-
const explanation = rule(error);
|
|
2290
|
-
if (explanation !== null) {
|
|
2291
|
-
return explanation;
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
return {
|
|
2295
|
-
headline: sentence(message),
|
|
2296
|
-
howToFix: []
|
|
2297
|
-
};
|
|
2298
|
-
}
|
|
2299
|
-
var tabsAndSpaces = ({ errorType }) => errorType !== "TabError" ? null : {
|
|
2300
|
-
headline: "This line mixes tabs and spaces for its indentation.",
|
|
2301
|
-
howToFix: [
|
|
2302
|
-
"Use spaces throughout. In VS Code, the command **Convert Indentation to Spaces** fixes the whole file.",
|
|
2303
|
-
"A tab and four spaces look identical on screen, which is why this is hard to see."
|
|
2304
|
-
]
|
|
2305
|
-
};
|
|
2306
|
-
var badCharacter = ({ message }) => {
|
|
2307
|
-
const bad = /invalid character '(.)' \(U\+[0-9A-F]+\)/.exec(message);
|
|
2308
|
-
if (!bad) return null;
|
|
2309
|
-
const curly = "\u201C\u201D\u2018\u2019".includes(bad[1]);
|
|
2310
|
-
return {
|
|
2311
|
-
headline: `\`${bad[1]}\` is not a character Python can read here.`,
|
|
2312
|
-
howToFix: curly ? [
|
|
2313
|
-
"Use straight quotes (`\"` and `'`) rather than curly ones.",
|
|
2314
|
-
"Curly quotes come from copying out of a document or a web page; retyping the quotes fixes it."
|
|
2315
|
-
] : ["Retype the line; the character may have come from copying out of a document."]
|
|
2316
|
-
};
|
|
2317
|
-
};
|
|
2318
|
-
var elseIf = ({ line }) => !/^else\s+if\b/.test(line) ? null : {
|
|
2319
|
-
headline: "Python spells this `elif`, not `else if`.",
|
|
2320
|
-
howToFix: ["Write `elif` as one word, then the condition and a colon."]
|
|
2321
|
-
};
|
|
2322
|
-
var returnOutsideFunction = ({ message }) => !/'return' outside function/.test(message) ? null : {
|
|
2323
|
-
headline: "`return` only works inside a function.",
|
|
2324
|
-
howToFix: [
|
|
2325
|
-
"To show a value at the top level of a file, use `print(...)` or write the value on a line of its own.",
|
|
2326
|
-
"If this was meant to be inside a function, check that it is indented under the `def`."
|
|
2327
|
-
]
|
|
2328
|
-
};
|
|
2329
|
-
var assignedForCompared = ({ message, line }) => {
|
|
2330
|
-
if (!/Maybe you meant '=='/.test(message)) return null;
|
|
2331
|
-
const inAssert = /^assert\b/.test(line);
|
|
2332
|
-
return {
|
|
2333
|
-
headline: inAssert ? "A test compares with `==`, not `=`." : "This compares two values, so it needs `==`.",
|
|
2334
|
-
howToFix: [
|
|
2335
|
-
"`==` asks whether two values are equal; `=` gives a name a value."
|
|
2336
|
-
]
|
|
2337
|
-
};
|
|
2338
|
-
};
|
|
2339
|
-
var assertAssigns = ({ line }) => !(/^assert\b/.test(line) && /(^|[^=!<>])=([^=]|$)/.test(line)) ? null : {
|
|
2340
|
-
headline: "A test compares with `==`, not `=`.",
|
|
2341
|
-
howToFix: ["`==` asks whether two values are equal; `=` gives a name a value."]
|
|
2342
|
-
};
|
|
2343
|
-
var caseCapturesEverything = ({ message }) => {
|
|
2344
|
-
const capture = /name capture '(\w+)' makes remaining patterns unreachable/.exec(message);
|
|
2345
|
-
if (capture === null) return null;
|
|
2346
|
-
return {
|
|
2347
|
-
headline: `\`case ${capture[1]}:\` needs brackets: \`case ${capture[1]}():\`.`,
|
|
2348
|
-
howToFix: [
|
|
2349
|
-
`Without them, \`${capture[1]}\` is a new name that matches anything, so no \`case\` after it can ever run.`,
|
|
2350
|
-
`With them, \`case ${capture[1]}():\` matches only a \`${capture[1]}\`; put one name inside for each field.`
|
|
2351
|
-
]
|
|
2352
|
-
};
|
|
2353
|
-
};
|
|
2354
|
-
var keywordAsName = ({ line }) => {
|
|
2355
|
-
const keyword = keywordAssignedTo(line);
|
|
2356
|
-
if (keyword === null) return null;
|
|
2357
|
-
return {
|
|
2358
|
-
headline: `\`${keyword}\` is a word Python reserves, so it cannot be a name.`,
|
|
2359
|
-
howToFix: [`Pick another name, such as \`${keyword}_value\` or something describing what it holds.`]
|
|
2360
|
-
};
|
|
2361
|
-
};
|
|
2362
|
-
var RULES2 = [
|
|
2363
|
-
tabsAndSpaces,
|
|
2364
|
-
badCharacter,
|
|
2365
|
-
elseIf,
|
|
2366
|
-
returnOutsideFunction,
|
|
2367
|
-
assignedForCompared,
|
|
2368
|
-
assertAssigns,
|
|
2369
|
-
caseCapturesEverything,
|
|
2370
|
-
keywordAsName
|
|
2371
|
-
];
|
|
2372
|
-
var KEYWORDS = [
|
|
2373
|
-
"False",
|
|
2374
|
-
"None",
|
|
2375
|
-
"True",
|
|
2376
|
-
"and",
|
|
2377
|
-
"as",
|
|
2378
|
-
"assert",
|
|
2379
|
-
"async",
|
|
2380
|
-
"await",
|
|
2381
|
-
"break",
|
|
2382
|
-
"class",
|
|
2383
|
-
"continue",
|
|
2384
|
-
"def",
|
|
2385
|
-
"del",
|
|
2386
|
-
"elif",
|
|
2387
|
-
"else",
|
|
2388
|
-
"except",
|
|
2389
|
-
"finally",
|
|
2390
|
-
"for",
|
|
2391
|
-
"from",
|
|
2392
|
-
"global",
|
|
2393
|
-
"if",
|
|
2394
|
-
"import",
|
|
2395
|
-
"in",
|
|
2396
|
-
"is",
|
|
2397
|
-
"lambda",
|
|
2398
|
-
"nonlocal",
|
|
2399
|
-
"not",
|
|
2400
|
-
"or",
|
|
2401
|
-
"pass",
|
|
2402
|
-
"raise",
|
|
2403
|
-
"return",
|
|
2404
|
-
"try",
|
|
2405
|
-
"while",
|
|
2406
|
-
"with",
|
|
2407
|
-
"yield"
|
|
2408
|
-
];
|
|
2409
|
-
function keywordAssignedTo(line) {
|
|
2410
|
-
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=[^=]/.exec(line);
|
|
2411
|
-
if (assignment === null) {
|
|
2412
|
-
return null;
|
|
2413
|
-
}
|
|
2414
|
-
return KEYWORDS.includes(assignment[1]) ? assignment[1] : null;
|
|
2415
|
-
}
|
|
2416
|
-
function sentence(text) {
|
|
2417
|
-
return text.trim() ? punctuated(text) : "Python could not read this line.";
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
// src/common/analyzers/syntaxErrorAnalyzer.ts
|
|
2421
|
-
var HANDLED2 = ["SyntaxError", "IndentationError", "TabError"];
|
|
2422
|
-
var syntaxErrorAnalyzer = {
|
|
2423
|
-
handles: HANDLED2,
|
|
2424
|
-
analyze(input) {
|
|
2425
|
-
const { error } = input;
|
|
2426
|
-
const offendingLine = placeOf(input).text;
|
|
2427
|
-
return runtimeFindingFor(input, {
|
|
2428
|
-
id: "syntax-error",
|
|
2429
|
-
...explainSyntaxError(error.errorType, error.message, offendingLine),
|
|
2430
|
-
nameToken: null
|
|
2431
|
-
});
|
|
2432
|
-
}
|
|
2433
|
-
};
|
|
2434
|
-
|
|
2435
|
-
// src/common/errors/typeCheckExplainer.ts
|
|
2436
|
-
var MAIN_PREFIX = "__main__.";
|
|
2437
|
-
var COURSE_NAME_FOR = { Row: "dict" };
|
|
2438
|
-
function cleanTypeName(type) {
|
|
2439
|
-
const cleaned = type.split(MAIN_PREFIX).join("");
|
|
2440
|
-
return COURSE_NAME_FOR[cleaned] ?? cleaned;
|
|
2441
|
-
}
|
|
2442
|
-
function describeType(type) {
|
|
2443
|
-
const name = cleanTypeName(type);
|
|
2444
|
-
if (name === "None" || name === "NoneType") return "`None`";
|
|
2445
|
-
const friendly2 = typeWords(name, { article: true });
|
|
2446
|
-
return friendly2 ? `${friendly2} (\`${name}\`)` : `\`${name}\``;
|
|
2447
|
-
}
|
|
2448
|
-
function describeTypes(types) {
|
|
2449
|
-
const described2 = types.map(describeType);
|
|
2450
|
-
if (described2.length === 0) return "a different type";
|
|
2451
|
-
if (described2.length === 1) return described2[0];
|
|
2452
|
-
return `${described2.slice(0, -1).join(", ")} or ${described2[described2.length - 1]}`;
|
|
2453
|
-
}
|
|
2454
|
-
function boolAsNumberNote(expected, actual, level) {
|
|
2455
|
-
if (actual !== "bool" || expected.length === 0) return null;
|
|
2456
|
-
if (!expected.some((type) => type === "int" || type === "float")) return null;
|
|
2457
|
-
if (level !== void 0 && !levelRejectsBoolAsNumber(level)) return null;
|
|
2458
|
-
return "At `#level beginner` and `#level intermediate`, `True` and `False` are not accepted as numbers, even though Python counts them as `1` and `0`.";
|
|
2459
|
-
}
|
|
2460
|
-
function fieldBeingReplaced(line, name) {
|
|
2461
|
-
if (line === null || name === null) {
|
|
2462
|
-
return null;
|
|
2463
|
-
}
|
|
2464
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2465
|
-
const assigns = new RegExp(`^\\s*${escaped}\\s*=(?!=)(.*)$`).exec(line);
|
|
2466
|
-
if (assigns === null) {
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
const reads = new RegExp(`\\b${escaped}\\.([A-Za-z_]\\w*)\\b(?!\\s*\\()`).exec(assigns[1]);
|
|
2470
|
-
return reads === null ? null : reads[1];
|
|
2471
|
-
}
|
|
2472
|
-
function withArticle(described2) {
|
|
2473
|
-
if (!described2.startsWith("`") || described2 === "`None`") {
|
|
2474
|
-
return described2;
|
|
2475
|
-
}
|
|
2476
|
-
return `${/^`[AEIOUaeiou]/.test(described2) ? "an" : "a"} ${described2}`;
|
|
2477
|
-
}
|
|
2478
|
-
function classItself(actual) {
|
|
2479
|
-
const m = actual === null ? null : /^class (.+)$/.exec(actual);
|
|
2480
|
-
return m === null ? null : cleanTypeName(m[1]);
|
|
2481
|
-
}
|
|
2482
|
-
function conversionFor(expected, actual) {
|
|
2483
|
-
if (expected.length !== 1 || actual === null) return null;
|
|
2484
|
-
const to = expected[0];
|
|
2485
|
-
const numeric = actual === "int" || actual === "float" || actual === "bool";
|
|
2486
|
-
if (to === "int" && (actual === "str" || actual === "float")) return "int";
|
|
2487
|
-
if (to === "float" && actual === "str") return "float";
|
|
2488
|
-
if (to === "str" && numeric) return "str";
|
|
2489
|
-
return null;
|
|
2490
|
-
}
|
|
2491
|
-
function membersPhrase(element) {
|
|
2492
|
-
if (element.startsWith("the value for key")) return "every value in";
|
|
2493
|
-
if (element.startsWith("key ")) return "every key in";
|
|
2494
|
-
return "every item in";
|
|
2495
|
-
}
|
|
2496
|
-
function describeElement(element) {
|
|
2497
|
-
if (element.startsWith("value of key ")) {
|
|
2498
|
-
return `the value for key ${plainKey(element.slice("value of key ".length))}`;
|
|
2499
|
-
}
|
|
2500
|
-
if (element.startsWith("key ")) return `key ${plainKey(element.slice("key ".length))}`;
|
|
2501
|
-
if (element.startsWith("[")) return "one of the values";
|
|
2502
|
-
return element;
|
|
2503
|
-
}
|
|
2504
|
-
function plainKey(key) {
|
|
2505
|
-
const simple = /^'([^'"\\]*)'$/.exec(key);
|
|
2506
|
-
return simple ? `"${simple[1]}"` : key;
|
|
2507
|
-
}
|
|
2508
|
-
var UNFAMILIAR = { kind: "unknown", name: null, element: null, actual: null, expected: [] };
|
|
2509
|
-
function described(check) {
|
|
2510
|
-
if (check === void 0) return UNFAMILIAR;
|
|
2511
|
-
return { ...check, element: check.element === null ? null : describeElement(check.element) };
|
|
2512
|
-
}
|
|
2513
|
-
var RETURNS_NONE = [
|
|
2514
|
-
"append",
|
|
2515
|
-
"extend",
|
|
2516
|
-
"insert",
|
|
2517
|
-
"remove",
|
|
2518
|
-
"sort",
|
|
2519
|
-
"reverse",
|
|
2520
|
-
"clear",
|
|
2521
|
-
"add",
|
|
2522
|
-
"discard",
|
|
2523
|
-
"update"
|
|
2524
|
-
];
|
|
2525
|
-
function setFromVoidMethod(facts, name) {
|
|
2526
|
-
const assigned = facts?.assigned?.[name];
|
|
2527
|
-
if (assigned === void 0 || !assigned.call.includes(".")) {
|
|
2528
|
-
return null;
|
|
2529
|
-
}
|
|
2530
|
-
const method = assigned.call.slice(assigned.call.lastIndexOf(".") + 1);
|
|
2531
|
-
return RETURNS_NONE.includes(method) ? { method, line: assigned.line } : null;
|
|
2532
|
-
}
|
|
2533
|
-
function noneReturn(owner, wanted, context) {
|
|
2534
|
-
const annotate = `Or annotate the return type as \`None\` if ${owner} is not meant to return anything.`;
|
|
2535
|
-
const returned = context?.line != null ? /^\s*return\s+(\S.*?)\s*$/.exec(context.line) : null;
|
|
2536
|
-
if (returned !== null && context !== void 0) {
|
|
2537
|
-
const expression = returned[1];
|
|
2538
|
-
const name = /^[A-Za-z_]\w*$/.test(expression) ? expression : null;
|
|
2539
|
-
const void_ = name !== null ? setFromVoidMethod(context.facts, name) : null;
|
|
2540
|
-
if (void_ !== null) {
|
|
2541
|
-
return {
|
|
2542
|
-
headline: `${owner} returned \`None\`, because \`${name}\` was set to the result of \`.${void_.method}(...)\` on line ${void_.line}.`,
|
|
2543
|
-
howToFix: [
|
|
2544
|
-
`\`.${void_.method}(...)\` changes the list in place and gives back nothing.`,
|
|
2545
|
-
`Call it on its own line - \`${name}.${void_.method}(...)\` - rather than assigning its result back to \`${name}\`.`
|
|
2546
|
-
]
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
if (expression === "None") {
|
|
2550
|
-
return {
|
|
2551
|
-
headline: `${owner} returns \`None\` on this line, but it should return ${wanted}.`,
|
|
2552
|
-
howToFix: [`Return ${wanted} here, or annotate ${owner} as returning \`None\`.`]
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
return {
|
|
2556
|
-
headline: `${owner} should return ${wanted}, but \`${expression}\` is \`None\` here.`,
|
|
2557
|
-
howToFix: [
|
|
2558
|
-
"This line did return - what it returned was nothing.",
|
|
2559
|
-
`Check where \`${expression}\` was last set.`,
|
|
2560
|
-
annotate
|
|
2561
|
-
]
|
|
2562
|
-
};
|
|
2563
|
-
}
|
|
2564
|
-
const match = context?.facts?.returnedNone?.match ?? null;
|
|
2565
|
-
if (match !== null) {
|
|
2566
|
-
const howToFix2 = [];
|
|
2567
|
-
const missing = match.uncovered;
|
|
2568
|
-
if (missing.length > 0) {
|
|
2569
|
-
howToFix2.push(
|
|
2570
|
-
`There is no \`case\` for ${missing.map((name) => `\`${name}\``).join(" or ")}.`
|
|
2571
|
-
);
|
|
2572
|
-
} else {
|
|
2573
|
-
howToFix2.push(`Every possible value of \`${match.subject}\` needs a \`case\`.`);
|
|
2574
|
-
if (match.hasList && !match.patterns.some((pattern) => pattern.trim() === "[]")) {
|
|
2575
|
-
howToFix2.push("The empty list, `case []:`, is the one most often left out.");
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
for (const pattern of match.fixedLength) {
|
|
2579
|
-
const parts = pattern.slice(1, -1).split(",").map((part) => part.trim());
|
|
2580
|
-
howToFix2.push(
|
|
2581
|
-
`\`case ${pattern}:\` matches a list of exactly ${parts.length} items; for a first and a rest, write \`[${parts.slice(0, -1).join(", ")}, *${parts[parts.length - 1]}]\`.`
|
|
2582
|
-
);
|
|
2583
|
-
}
|
|
2584
|
-
return {
|
|
2585
|
-
headline: `No \`case\` in ${owner} fitted \`${match.subject}\`, so nothing was returned.`,
|
|
2586
|
-
howToFix: howToFix2
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
const printed = context?.facts?.returnedNone?.printed ?? null;
|
|
2590
|
-
if (printed !== null) {
|
|
2591
|
-
return {
|
|
2592
|
-
headline: `${owner} should return ${wanted}, but it finished without returning a value.`,
|
|
2593
|
-
howToFix: [
|
|
2594
|
-
`Line ${printed.line} ends its branch with \`print\`: did you mean ` + (printed.expression !== null ? `\`return ${printed.expression}\`?` : "`return` instead of `print`?"),
|
|
2595
|
-
"`print` shows a value on screen; `return` hands it back to whatever called the function."
|
|
2596
|
-
]
|
|
2597
|
-
};
|
|
2598
|
-
}
|
|
2599
|
-
const howToFix = [
|
|
2600
|
-
"Make sure every path through the function reaches a `return`.",
|
|
2601
|
-
"An `if` with no `else` falls off the end, and Python then returns `None`."
|
|
2602
|
-
];
|
|
2603
|
-
howToFix.push(annotate);
|
|
2604
|
-
return {
|
|
2605
|
-
headline: `${owner} should return ${wanted}, but it finished without returning a value.`,
|
|
2606
|
-
howToFix
|
|
2607
|
-
};
|
|
2608
|
-
}
|
|
2609
|
-
function explainTypeCheckError(message, functionName, level, context) {
|
|
2610
|
-
const parsed = described(context?.facts?.check);
|
|
2611
|
-
const c = {
|
|
2612
|
-
message,
|
|
2613
|
-
parsed,
|
|
2614
|
-
functionName,
|
|
2615
|
-
level,
|
|
2616
|
-
context,
|
|
2617
|
-
wanted: describeTypes(parsed.expected),
|
|
2618
|
-
owner: functionName ? `\`${functionName}\`` : "this function",
|
|
2619
|
-
named: parsed.name ? `\`${parsed.name}\`` : "a value",
|
|
2620
|
-
convert: conversionFor(parsed.expected, parsed.actual),
|
|
2621
|
-
boolNote: boolAsNumberNote(parsed.expected, parsed.actual, parsed.level ?? level),
|
|
2622
|
-
annotation: parsed.expected.length === 1 ? cleanTypeName(parsed.expected[0]) : null
|
|
2623
|
-
};
|
|
2624
|
-
return classGiven(c) ?? BY_KIND[parsed.kind](c);
|
|
2625
|
-
}
|
|
2626
|
-
var BY_KIND = {
|
|
2627
|
-
argument: explainArgument,
|
|
2628
|
-
field: explainField,
|
|
2629
|
-
return: explainReturn,
|
|
2630
|
-
variable: explainVariable,
|
|
2631
|
-
unknown: explainUnfamiliar
|
|
2632
|
-
};
|
|
2633
|
-
function classGiven({ parsed, owner, named }) {
|
|
2634
|
-
const classPassed = classItself(parsed.actual);
|
|
2635
|
-
if (classPassed === null || parsed.element !== null) {
|
|
2636
|
-
return null;
|
|
2637
|
-
}
|
|
2638
|
-
const where = parsed.kind === "return" ? `${owner} returned` : parsed.kind === "variable" ? `${named} was given` : `${owner} was given`;
|
|
2639
|
-
return {
|
|
2640
|
-
headline: `${where} the class \`${classPassed}\` itself, not one made from it.`,
|
|
2641
|
-
howToFix: [
|
|
2642
|
-
`\`${classPassed}\` on its own is the blueprint; \`${classPassed}(...)\` makes one.`,
|
|
2643
|
-
`Add the brackets and the values for its fields: \`${classPassed}(...)\`.`
|
|
2644
|
-
]
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
2647
|
-
function explainArgument({
|
|
2648
|
-
parsed,
|
|
2649
|
-
owner,
|
|
2650
|
-
named,
|
|
2651
|
-
wanted,
|
|
2652
|
-
convert,
|
|
2653
|
-
boolNote,
|
|
2654
|
-
annotation,
|
|
2655
|
-
context
|
|
2656
|
-
}) {
|
|
2657
|
-
if (parsed.element) {
|
|
2658
|
-
const value = context?.facts?.elementValue;
|
|
2659
|
-
const is = value !== void 0 ? ` is ${value}` : " is not";
|
|
2660
|
-
return {
|
|
2661
|
-
headline: `${owner} expects ${membersPhrase(parsed.element)} ${named} to be ${wanted}, but ${parsed.element}${is}.`,
|
|
2662
|
-
howToFix: [
|
|
2663
|
-
`Look at ${parsed.element} of the ${parsed.actual ?? "value"} you passed for ${named}.`,
|
|
2664
|
-
"Every one has to match the annotation, not just the first."
|
|
2665
|
-
]
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
const got = parsed.actual ? `, but got ${describeType(parsed.actual)}` : "";
|
|
2669
|
-
const supplied = context?.calledBy ?? null;
|
|
2670
|
-
if (supplied !== null) {
|
|
2671
|
-
const howToFix2 = supplied === "reactor" ? [
|
|
2672
|
-
`The reactor calls ${owner} with its state: the \`init\` value to start with, then whatever the handlers return. So ${named} is the reactor's state, not a value from this line.`
|
|
2673
|
-
] : [
|
|
2674
|
-
`${FUNCTION_TAKERS[supplied]?.calls ?? `\`${supplied}\` calls ${owner} for you`}, so ${named} is whatever it hands over - not a value from this line.`
|
|
2675
|
-
];
|
|
2676
|
-
if (annotation !== null && parsed.actual !== null) {
|
|
2677
|
-
howToFix2.push(
|
|
2678
|
-
`Annotate ${named} as \`${cleanTypeName(parsed.actual)}\` in ${owner}, or change what ${supplied === "reactor" ? "the reactor" : `\`${supplied}\``} is given so it hands over ${wanted}.`
|
|
2679
|
-
);
|
|
2680
|
-
}
|
|
2681
|
-
if (boolNote) howToFix2.push(boolNote);
|
|
2682
|
-
return { headline: `${owner} expects ${named} to be ${wanted}${got}.`, howToFix: howToFix2 };
|
|
2683
|
-
}
|
|
2684
|
-
const howToFix = [`Check the value you passed for ${named} on this line.`];
|
|
2685
|
-
if (boolNote) howToFix.push(boolNote);
|
|
2686
|
-
if (convert) {
|
|
2687
|
-
howToFix.push(`If it should be ${wanted}, convert it with \`${convert}(...)\`.`);
|
|
2688
|
-
}
|
|
2689
|
-
if (annotation && parsed.actual) {
|
|
2690
|
-
howToFix.push(
|
|
2691
|
-
`Or, if ${named} really should be ${describeType(parsed.actual)}, change its annotation in ${owner} from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
2692
|
-
);
|
|
2693
|
-
}
|
|
2694
|
-
return {
|
|
2695
|
-
headline: `${owner} expects ${named} to be ${wanted}${got}.`,
|
|
2696
|
-
howToFix
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
function explainField({
|
|
2700
|
-
parsed,
|
|
2701
|
-
wanted,
|
|
2702
|
-
convert,
|
|
2703
|
-
annotation,
|
|
2704
|
-
context
|
|
2705
|
-
}) {
|
|
2706
|
-
const owner = parsed.owner ?? "this class";
|
|
2707
|
-
const swappedWith = context?.facts?.swappedWith;
|
|
2708
|
-
if (swappedWith !== void 0) {
|
|
2709
|
-
return {
|
|
2710
|
-
headline: `The values for \`${parsed.name}\` and \`${swappedWith}\` of \`${owner}\` look swapped.`,
|
|
2711
|
-
howToFix: [
|
|
2712
|
-
`\`${parsed.name}\` got \`${parsed.value ?? "?"}\`, which fits \`${swappedWith}\` - and the other way round.`,
|
|
2713
|
-
`Give the values in the order the fields are written in \`class ${owner}\`.`
|
|
2714
|
-
]
|
|
2715
|
-
};
|
|
2716
|
-
}
|
|
2717
|
-
const got = parsed.value !== void 0 ? `\`${parsed.value}\`` : parsed.actual ? describeType(parsed.actual) : "something else";
|
|
2718
|
-
return {
|
|
2719
|
-
headline: `The \`${parsed.name}\` field of \`${owner}\` should be ${wanted}, but got ${got}.`,
|
|
2720
|
-
howToFix: [
|
|
2721
|
-
`Check the values given to \`${owner}(...)\`, in the order its fields are written.`,
|
|
2722
|
-
...convert !== null ? [`If it should be ${wanted}, convert it with \`${convert}(...)\`.`] : [],
|
|
2723
|
-
...annotation !== null && parsed.actual !== null ? [
|
|
2724
|
-
`Or change the field's annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
2725
|
-
] : []
|
|
2726
|
-
]
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
function explainReturn({
|
|
2730
|
-
parsed,
|
|
2731
|
-
owner,
|
|
2732
|
-
wanted,
|
|
2733
|
-
convert,
|
|
2734
|
-
boolNote,
|
|
2735
|
-
annotation,
|
|
2736
|
-
context
|
|
2737
|
-
}) {
|
|
2738
|
-
if (parsed.actual === "None" || parsed.actual === "NoneType") {
|
|
2739
|
-
return noneReturn(owner, wanted, context);
|
|
2740
|
-
}
|
|
2741
|
-
if (parsed.element) {
|
|
2742
|
-
const value = context?.facts?.elementValue;
|
|
2743
|
-
const element = describeElement(parsed.element);
|
|
2744
|
-
const container = parsed.actual ? cleanTypeName(parsed.actual) : "value";
|
|
2745
|
-
const howToFix2 = [
|
|
2746
|
-
`Look at ${element} of the ${container} this line returns: every one has to match the annotation, not just the first.`
|
|
2747
|
-
];
|
|
2748
|
-
const elementType = context?.facts?.elementType;
|
|
2749
|
-
const written = parsed.annotation ?? null;
|
|
2750
|
-
if (written !== null && annotation !== null && elementType !== void 0) {
|
|
2751
|
-
const type = new RegExp(`\\b${annotation.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g");
|
|
2752
|
-
if ((written.match(type) ?? []).length === 1) {
|
|
2753
|
-
howToFix2.push(
|
|
2754
|
-
`Or, if that is what it should hold, change the annotation from \`${written}\` to \`${written.replace(type, elementType)}\`.`
|
|
2755
|
-
);
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
return {
|
|
2759
|
-
headline: `${owner} says it returns ${written !== null ? `\`${written}\`` : `a ${container}`}, with ${membersPhrase(element)} it ${wanted}, but ${element}${value !== void 0 ? ` is ${value}` : " is not"}.`,
|
|
2760
|
-
howToFix: howToFix2
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
const got = parsed.actual ? describeType(parsed.actual) : "something else";
|
|
2764
|
-
const howToFix = [];
|
|
2765
|
-
const wantsNone = parsed.expected.length === 1 && /^(None|NoneType)$/.test(parsed.expected[0]);
|
|
2766
|
-
if (wantsNone) {
|
|
2767
|
-
howToFix.push("Use a bare `return`, or drop the `return` entirely.");
|
|
2768
|
-
} else if (convert) {
|
|
2769
|
-
howToFix.push(`Convert the returned value with \`${convert}(...)\`.`);
|
|
2770
|
-
} else {
|
|
2771
|
-
howToFix.push(`Return ${withArticle(wanted)} from this line.`);
|
|
2772
|
-
}
|
|
2773
|
-
if (boolNote) howToFix.push(boolNote);
|
|
2774
|
-
if (annotation && parsed.actual) {
|
|
2775
|
-
howToFix.push(
|
|
2776
|
-
`Or change ${owner}'s return annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
2777
|
-
);
|
|
2778
|
-
}
|
|
2779
|
-
return {
|
|
2780
|
-
headline: `${owner} says it returns ${wanted}, but this line returns ${got}.`,
|
|
2781
|
-
howToFix
|
|
2782
|
-
};
|
|
2783
|
-
}
|
|
2784
|
-
function explainVariable({
|
|
2785
|
-
parsed,
|
|
2786
|
-
named,
|
|
2787
|
-
wanted,
|
|
2788
|
-
convert,
|
|
2789
|
-
boolNote,
|
|
2790
|
-
annotation,
|
|
2791
|
-
context
|
|
2792
|
-
}) {
|
|
2793
|
-
if (parsed.element) {
|
|
2794
|
-
return {
|
|
2795
|
-
headline: `${named} is annotated so ${membersPhrase(parsed.element)} it is ${wanted}, but ${parsed.element} is not.`,
|
|
2796
|
-
howToFix: [`Look at ${parsed.element} of the value assigned to ${named}.`]
|
|
2797
|
-
};
|
|
2798
|
-
}
|
|
2799
|
-
const got = parsed.actual ? describeType(parsed.actual) : "a different type";
|
|
2800
|
-
const field = fieldBeingReplaced(context?.line ?? null, parsed.name);
|
|
2801
|
-
if (field !== null && parsed.name !== null) {
|
|
2802
|
-
const rhs = (/=\s*(.+)$/.exec(context?.line ?? "") ?? [, "..."])[1].trim();
|
|
2803
|
-
return {
|
|
2804
|
-
headline: `${named} is annotated as ${wanted}, but ${got} was assigned here.`,
|
|
2805
|
-
howToFix: [
|
|
2806
|
-
`To change the \`${field}\` field of ${named}, assign to the field: \`${parsed.name}.${field} = ${rhs}\`.`,
|
|
2807
|
-
`\`${parsed.name} = ...\` replaces the whole ${annotation !== null ? `\`${annotation}\`` : "value"}, field and all.`
|
|
2808
|
-
]
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
const howToFix = [`Assign ${withArticle(wanted)} to ${named}.`];
|
|
2812
|
-
if (boolNote) howToFix.push(boolNote);
|
|
2813
|
-
if (convert) {
|
|
2814
|
-
howToFix.push(`You can convert the value with \`${convert}(...)\`.`);
|
|
2815
|
-
}
|
|
2816
|
-
if (annotation && parsed.actual) {
|
|
2817
|
-
howToFix.push(
|
|
2818
|
-
`Or change ${named}'s annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
2819
|
-
);
|
|
2820
|
-
}
|
|
2821
|
-
return {
|
|
2822
|
-
headline: `${named} is annotated as ${wanted}, but ${got} was assigned here.`,
|
|
2823
|
-
howToFix
|
|
2824
|
-
};
|
|
2825
|
-
}
|
|
2826
|
-
function explainUnfamiliar({ message }) {
|
|
2827
|
-
return {
|
|
2828
|
-
headline: "A value does not match the type annotation written for it.",
|
|
2829
|
-
howToFix: [
|
|
2830
|
-
message.split(/\r?\n/)[0].trim(),
|
|
2831
|
-
"Either pass a value of the annotated type, or change the annotation."
|
|
2832
|
-
]
|
|
2833
|
-
};
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
// src/common/analyzers/typeCheckAnalyzer.ts
|
|
2837
|
-
var typeCheckAnalyzer = {
|
|
2838
|
-
handles: ["TypeCheckError"],
|
|
2839
|
-
analyze(input) {
|
|
2840
|
-
const { error, fileName, level } = input;
|
|
2841
|
-
const check = error.facts.check;
|
|
2842
|
-
const frames = userFrames(error);
|
|
2843
|
-
const innermost = frames.length > 0 ? frames[frames.length - 1] : null;
|
|
2844
|
-
const blamed = check?.kind === "argument" && frames.length >= 2 ? frames[frames.length - 2] : innermost;
|
|
2845
|
-
const explanation = explainTypeCheckError(
|
|
2846
|
-
error.message,
|
|
2847
|
-
innermost ? innermost.functionName : null,
|
|
2848
|
-
level,
|
|
2849
|
-
// The line the check fired on, which for a return is the `return`
|
|
2850
|
-
// itself - or, when the function ran off its end, is not one.
|
|
2851
|
-
{
|
|
2852
|
-
line: frameText(input, innermost),
|
|
2853
|
-
calledBy: libraryCaller(error.frames),
|
|
2854
|
-
facts: error.facts
|
|
2855
|
-
}
|
|
2856
|
-
);
|
|
2857
|
-
return runtimeFindingFor(input, {
|
|
2858
|
-
id: "type-check",
|
|
2859
|
-
// Not a real Python exception name; "TypeCheckError" means nothing to
|
|
2860
|
-
// a student, and "TypeError" would name a different Python error.
|
|
2861
|
-
errorType: "TypeMismatch",
|
|
2862
|
-
...explanation,
|
|
2863
|
-
fileName: blamed ? blamed.fileName : fileName,
|
|
2864
|
-
lineNumber: blamed ? blamed.line : error.lineNumber,
|
|
2865
|
-
column: null,
|
|
2866
|
-
nameToken: check?.name ?? null
|
|
2867
|
-
});
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
|
|
2871
|
-
// src/common/analyzers/registry.ts
|
|
2872
|
-
var analyzers = [
|
|
2873
|
-
nameErrorAnalyzer,
|
|
2874
|
-
typeCheckAnalyzer,
|
|
2875
|
-
syntaxErrorAnalyzer,
|
|
2876
|
-
checksFailedAnalyzer,
|
|
2877
|
-
// Last before the catch-all: it only reaches an error no analyzer
|
|
2878
|
-
// above recognised, and claims it only if it can say something better
|
|
2879
|
-
// than Python did.
|
|
2880
|
-
stockMessageAnalyzer
|
|
2881
|
-
];
|
|
2882
|
-
function findRuntimeFinding(source, fileName, level, error) {
|
|
2883
|
-
for (const analyzer of analyzers) {
|
|
2884
|
-
if (!analyzer.handles.includes(error.errorType)) {
|
|
2885
|
-
continue;
|
|
2886
|
-
}
|
|
2887
|
-
const finding = analyzer.analyze({ source, fileName, level, error });
|
|
2888
|
-
if (finding) {
|
|
2889
|
-
return finding;
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
return analyzeRuntimeError({ source, fileName, level, error });
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
// src/common/analyzers/runtimeFinding.ts
|
|
2896
|
-
function findingForErrorEvent(event, source, fileName, level) {
|
|
2897
|
-
return findRuntimeFinding(source, fileName, level, event.error);
|
|
2898
|
-
}
|
|
2899
|
-
function explainTestReport(event, source, fileName, level) {
|
|
2900
|
-
return {
|
|
2901
|
-
...event,
|
|
2902
|
-
tests: event.tests.map(
|
|
2903
|
-
({ error, ...test }) => error ? { ...test, finding: serializeFinding(findRuntimeFinding(source, fileName, level, error)) } : test
|
|
2904
|
-
)
|
|
2905
|
-
};
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
// src/common/examplarSource.ts
|
|
2909
|
-
var MAX_BUNDLE_BYTES = 4 * 1024 * 1024;
|
|
2910
|
-
var DIRECTIVE_RE = /^#\s*examplar\s+(\S+)\s*$/;
|
|
2911
|
-
function parseExamplarDirective(source) {
|
|
2912
|
-
const hits = [];
|
|
2913
|
-
const lines = source.split(/\r?\n/);
|
|
2914
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2915
|
-
const match = DIRECTIVE_RE.exec(lines[i].trim());
|
|
2916
|
-
if (match) {
|
|
2917
|
-
hits.push({ url: match[1], line: i + 1 });
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
if (hits.length === 0) {
|
|
2921
|
-
return { kind: "none" };
|
|
2922
|
-
}
|
|
2923
|
-
if (hits.length > 1) {
|
|
2924
|
-
return {
|
|
2925
|
-
kind: "error",
|
|
2926
|
-
line: hits[1].line,
|
|
2927
|
-
message: `there is more than one \`#examplar\` line (lines ${hits.map((h) => h.line).join(" and ")}). Keep the one you want.`
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
const { url, line } = hits[0];
|
|
2931
|
-
const problem = validateBundleUrl(url);
|
|
2932
|
-
return problem ? { kind: "error", message: problem, line } : { kind: "found", url, line };
|
|
2933
|
-
}
|
|
2934
|
-
function validateBundleUrl(url) {
|
|
2935
|
-
let parsed;
|
|
2936
|
-
try {
|
|
2937
|
-
parsed = new URL(url);
|
|
2938
|
-
} catch {
|
|
2939
|
-
return `\`${url}\` is not a URL.`;
|
|
2940
|
-
}
|
|
2941
|
-
if (parsed.protocol === "https:") {
|
|
2942
|
-
return null;
|
|
2943
|
-
}
|
|
2944
|
-
if (parsed.protocol === "http:") {
|
|
2945
|
-
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
2946
|
-
return local ? null : `\`${url}\` must use https (plain http is only allowed for localhost).`;
|
|
2947
|
-
}
|
|
2948
|
-
return `\`${url}\` must be an https address.`;
|
|
2949
|
-
}
|
|
2950
|
-
async function loadBundle(url, store, fetchImpl = globalThis.fetch) {
|
|
2951
|
-
const cached = await store.read(url).catch(() => void 0);
|
|
2952
|
-
let response;
|
|
2953
|
-
try {
|
|
2954
|
-
response = await fetchImpl(url, {
|
|
2955
|
-
headers: cached?.etag ? { "If-None-Match": cached.etag } : {}
|
|
2956
|
-
});
|
|
2957
|
-
} catch (err) {
|
|
2958
|
-
const why = errorText(err);
|
|
2959
|
-
return cached ? { json: cached.json, fromCache: true, note: `could not reach ${url} (${why}); using the cached copy` } : { fromCache: false, error: `could not reach ${url} (${why}), and nothing is cached` };
|
|
2960
|
-
}
|
|
2961
|
-
if (response.status === 304 && cached) {
|
|
2962
|
-
return { json: cached.json, fromCache: true };
|
|
2963
|
-
}
|
|
2964
|
-
if (!response.ok) {
|
|
2965
|
-
return cached ? {
|
|
2966
|
-
json: cached.json,
|
|
2967
|
-
fromCache: true,
|
|
2968
|
-
note: `${url} returned ${response.status}; using the cached copy`
|
|
2969
|
-
} : { fromCache: false, error: `${url} returned ${response.status}` };
|
|
2970
|
-
}
|
|
2971
|
-
const json = await response.text();
|
|
2972
|
-
if (json.length > MAX_BUNDLE_BYTES) {
|
|
2973
|
-
return cached ? { json: cached.json, fromCache: true, note: `${url} is unexpectedly large; using the cached copy` } : {
|
|
2974
|
-
fromCache: false,
|
|
2975
|
-
error: `${url} is ${json.length} bytes, past the ${MAX_BUNDLE_BYTES}-byte limit`
|
|
2976
|
-
};
|
|
2977
|
-
}
|
|
2978
|
-
if (!looksLikeBundle(json)) {
|
|
2979
|
-
return cached ? { json: cached.json, fromCache: true, note: `${url} did not return a bundle; using the cached copy` } : { fromCache: false, error: `${url} did not return an Examplar bundle` };
|
|
2980
|
-
}
|
|
2981
|
-
const etag = response.headers.get("etag") ?? void 0;
|
|
2982
|
-
try {
|
|
2983
|
-
await store.write(url, { json, etag });
|
|
2984
|
-
} catch (err) {
|
|
2985
|
-
return { json, fromCache: false, note: `could not keep a copy for offline use (${errorText(err)})` };
|
|
2986
|
-
}
|
|
2987
|
-
return { json, fromCache: false };
|
|
2988
|
-
}
|
|
2989
|
-
function looksLikeBundle(json) {
|
|
2990
|
-
try {
|
|
2991
|
-
const bundle = JSON.parse(json);
|
|
2992
|
-
return typeof bundle === "object" && bundle !== null && "examplar" in bundle && Array.isArray(bundle.wheats) && Array.isArray(bundle.chaffs);
|
|
2993
|
-
} catch {
|
|
2994
|
-
return false;
|
|
2995
|
-
}
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
// src/common/runtimeErrors.ts
|
|
2999
|
-
var PythonLostError = class extends Error {
|
|
3000
|
-
constructor() {
|
|
3001
|
-
super("Python stopped completely.");
|
|
3002
|
-
this.name = "PythonLostError";
|
|
3003
|
-
}
|
|
3004
|
-
};
|
|
3005
|
-
var StoppedError = class extends Error {
|
|
3006
|
-
constructor(message) {
|
|
3007
|
-
super(message);
|
|
3008
|
-
this.name = "StoppedError";
|
|
765
|
+
async handleStdinRequest(request) {
|
|
766
|
+
this.stdinRequest = request;
|
|
767
|
+
let bytes = this.stdinCarry;
|
|
768
|
+
if (bytes.length === 0) {
|
|
769
|
+
bytes = await this.nextStdin();
|
|
770
|
+
if (this.stdinRequest !== request) {
|
|
771
|
+
if (bytes !== null && this.stdinRequest === null) {
|
|
772
|
+
this.stdinCarry = concatBytes(this.stdinCarry, bytes);
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
this.stdinRequest = null;
|
|
778
|
+
if (!this.stdinBuffer) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const given = writeStdin(this.stdinBuffer, request, bytes);
|
|
782
|
+
if (bytes === null) {
|
|
783
|
+
this.stdinCarry = new Uint8Array(0);
|
|
784
|
+
} else {
|
|
785
|
+
this.stdinCarry = bytes.subarray(Math.max(given, 0));
|
|
786
|
+
}
|
|
3009
787
|
}
|
|
3010
788
|
};
|
|
789
|
+
function concatBytes(a, b) {
|
|
790
|
+
const out = new Uint8Array(a.length + b.length);
|
|
791
|
+
out.set(a);
|
|
792
|
+
out.set(b, a.length);
|
|
793
|
+
return out;
|
|
794
|
+
}
|
|
3011
795
|
|
|
3012
|
-
// src/
|
|
796
|
+
// src/run/examplar.ts
|
|
3013
797
|
async function runExamplarStep(runtime, bundles, host, code, fileName) {
|
|
3014
798
|
const bundle = await fetchBundle(bundles, host, code);
|
|
3015
799
|
if (bundle === null) {
|
|
@@ -3211,16 +995,13 @@ function timedOutProblem(stuck) {
|
|
|
3211
995
|
return `\`${stuck.test}\` ran for more than ${stuck.seconds} seconds against ${against}, so the check stopped. ` + (stuck.kind === "wheat" ? "A test that runs that long is usually stuck in a loop." : "That implementation may never finish on what this test gives it; tell your course staff.");
|
|
3212
996
|
}
|
|
3213
997
|
|
|
3214
|
-
// src/
|
|
3215
|
-
var
|
|
3216
|
-
var MAY_IMPORT_RE = /\bimport\b|__import__/;
|
|
998
|
+
// src/run/packages.ts
|
|
999
|
+
var MAY_NEED_PACKAGES_RE = /\bimport\b|__import__|\.\s*to_pandas\s*\(/;
|
|
3217
1000
|
function needsPackages(code, siblings = []) {
|
|
3218
|
-
return [code, ...siblings.map((sibling) => sibling.text)].some(
|
|
3219
|
-
(text) => MAY_IMPORT_RE.test(text) || PANDAS_METHOD_RE.test(text)
|
|
3220
|
-
);
|
|
1001
|
+
return [code, ...siblings.map((sibling) => sibling.text)].some((text) => MAY_NEED_PACKAGES_RE.test(text));
|
|
3221
1002
|
}
|
|
3222
1003
|
|
|
3223
|
-
// src/
|
|
1004
|
+
// src/core/files.ts
|
|
3224
1005
|
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
3225
1006
|
var MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
3226
1007
|
var MAX_FILES = 100;
|
|
@@ -3424,7 +1205,7 @@ function errorCode(err) {
|
|
|
3424
1205
|
return message.split("\n")[0].slice(0, 80);
|
|
3425
1206
|
}
|
|
3426
1207
|
|
|
3427
|
-
// src/
|
|
1208
|
+
// src/run/plan.ts
|
|
3428
1209
|
var STOPPED = {
|
|
3429
1210
|
beforeStart: "Stopped before the program started. Nothing was run.",
|
|
3430
1211
|
checking: "Stopped while checking your tests. Your own tests and the program were not run.",
|
|
@@ -3467,7 +1248,7 @@ async function runFilePlan(runtime, host, plan) {
|
|
|
3467
1248
|
}
|
|
3468
1249
|
host.status("Running...");
|
|
3469
1250
|
starting();
|
|
3470
|
-
const onEvent = tally.counting(programEvents(host,
|
|
1251
|
+
const onEvent = tally.counting(programEvents(host, fileName, level));
|
|
3471
1252
|
const program = () => runtime.runFile({ code, fileName, sessionKey, level, withTests }, onEvent);
|
|
3472
1253
|
await (host.aroundProgram ? host.aroundProgram(program) : program());
|
|
3473
1254
|
if (withTests) {
|
|
@@ -3530,13 +1311,12 @@ async function staticChecks(runtime, host, code, fileName, level, sessionKey) {
|
|
|
3530
1311
|
host.status("Checking...");
|
|
3531
1312
|
let findings;
|
|
3532
1313
|
try {
|
|
3533
|
-
|
|
1314
|
+
findings = await runtime.staticAnalyze({
|
|
3534
1315
|
code,
|
|
3535
1316
|
fileName,
|
|
3536
1317
|
level,
|
|
3537
1318
|
...input ? { sessionKey } : {}
|
|
3538
1319
|
});
|
|
3539
|
-
findings = enrichStaticFindings(raw, level, fileName);
|
|
3540
1320
|
} catch (err) {
|
|
3541
1321
|
if (err instanceof StoppedError) {
|
|
3542
1322
|
host.say(input ? STOPPED.input : STOPPED.beforeStart, "problem");
|
|
@@ -3671,12 +1451,12 @@ async function testsToRun(runtime, host, code) {
|
|
|
3671
1451
|
return false;
|
|
3672
1452
|
}
|
|
3673
1453
|
}
|
|
3674
|
-
function programEvents(host,
|
|
1454
|
+
function programEvents(host, fileName, level) {
|
|
3675
1455
|
return (event) => {
|
|
3676
1456
|
if (event.kind === "error") {
|
|
3677
|
-
host.runtimeFinding(
|
|
1457
|
+
host.runtimeFinding(findRuntimeFinding(fileName, level, event.error));
|
|
3678
1458
|
} else if (event.kind === "testReport") {
|
|
3679
|
-
host.event(explainTestReport(event,
|
|
1459
|
+
host.event(explainTestReport(event, fileName, level));
|
|
3680
1460
|
} else {
|
|
3681
1461
|
host.event(event);
|
|
3682
1462
|
}
|
|
@@ -4134,722 +1914,200 @@ async function verify(runtime, view, bundle, testsPath) {
|
|
|
4134
1914
|
}
|
|
4135
1915
|
for (const chaff of result.chaffs ?? []) {
|
|
4136
1916
|
const where = `chaff ${chaff.targets}/${chaff.id}`;
|
|
4137
|
-
if (!chaff.loaded) {
|
|
4138
|
-
bad += 1;
|
|
4139
|
-
view.problem(` BAD ${where}: ${chaff.errorMessage}`);
|
|
4140
|
-
continue;
|
|
4141
|
-
}
|
|
4142
|
-
if (caught(chaff)) {
|
|
4143
|
-
const by = Object.entries(chaff.tests).filter(([, t]) => t.outcome !== "pass").map(([name]) => name);
|
|
4144
|
-
view.note(` ok ${where}: caught by ${by.join(", ")}`);
|
|
4145
|
-
continue;
|
|
4146
|
-
}
|
|
4147
|
-
bad += 1;
|
|
4148
|
-
view.problem(` BAD ${where}: no test catches it - it would never count`);
|
|
4149
|
-
}
|
|
4150
|
-
const attributed = Object.values(result.attribution ?? {});
|
|
4151
|
-
for (const name of result.chaffsSkipped ?? []) {
|
|
4152
|
-
if (attributed.some((names) => names.includes(name))) {
|
|
4153
|
-
view.problem(` --- ${name} chaffs not checked: its tests have to pass first`);
|
|
4154
|
-
continue;
|
|
4155
|
-
}
|
|
4156
|
-
bad += 1;
|
|
4157
|
-
view.problem(
|
|
4158
|
-
` BAD your own suite has no tests for ${name}, so its chaffs are unproven`
|
|
4159
|
-
);
|
|
4160
|
-
}
|
|
4161
|
-
if (bad > 0) {
|
|
4162
|
-
view.problem(`pll: ${bad} problem(s) with this bundle; not written.`);
|
|
4163
|
-
return EXIT.testsFailed;
|
|
4164
|
-
}
|
|
4165
|
-
view.problem("verified.");
|
|
4166
|
-
return EXIT.ok;
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
// src/cli/runtime.ts
|
|
4170
|
-
var fs6 = __toESM(require("node:fs"));
|
|
4171
|
-
var path6 = __toESM(require("node:path"));
|
|
4172
|
-
|
|
4173
|
-
// src/desktop/pyodideRuntime.ts
|
|
4174
|
-
var fs5 = __toESM(require("node:fs"));
|
|
4175
|
-
var path5 = __toESM(require("node:path"));
|
|
4176
|
-
var import_node_worker_threads = require("node:worker_threads");
|
|
4177
|
-
|
|
4178
|
-
// src/common/fromPython.ts
|
|
4179
|
-
function camelName(name) {
|
|
4180
|
-
return name.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
4181
|
-
}
|
|
4182
|
-
function camelized(record) {
|
|
4183
|
-
return Object.fromEntries(
|
|
4184
|
-
Object.entries(record).map(([name, value]) => [camelName(name), value])
|
|
4185
|
-
);
|
|
4186
|
-
}
|
|
4187
|
-
function deliverRunResult(result, onEvent, fileName) {
|
|
4188
|
-
if (result.displays) {
|
|
4189
|
-
for (const display of result.displays) {
|
|
4190
|
-
deliverDisplay(display, onEvent, fileName);
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
if (result.result_repr !== null && result.result_repr !== void 0) {
|
|
4194
|
-
onEvent({ kind: "result", repr: result.result_repr });
|
|
4195
|
-
}
|
|
4196
|
-
const error = result.ok ? null : pythonErrorFrom(result);
|
|
4197
|
-
if (error !== null) {
|
|
4198
|
-
onEvent({ kind: "error", error, fileName });
|
|
4199
|
-
}
|
|
4200
|
-
if (result.tests) {
|
|
4201
|
-
onEvent(testReportFrom(result.tests, fileName));
|
|
4202
|
-
}
|
|
4203
|
-
const exitCode = result.exit_code;
|
|
4204
|
-
onEvent({
|
|
4205
|
-
kind: "done",
|
|
4206
|
-
...typeof exitCode === "number" ? { exitCode } : {},
|
|
4207
|
-
...result.stopped ? { stopped: true } : {}
|
|
4208
|
-
});
|
|
4209
|
-
}
|
|
4210
|
-
function deliverDisplay(display, onEvent, fileName) {
|
|
4211
|
-
switch (display.type) {
|
|
4212
|
-
case "stdout":
|
|
4213
|
-
onEvent({ kind: "stdout", text: display.text });
|
|
4214
|
-
break;
|
|
4215
|
-
case "stderr":
|
|
4216
|
-
onEvent({ kind: "stderr", text: display.text });
|
|
4217
|
-
break;
|
|
4218
|
-
case "image":
|
|
4219
|
-
onEvent({
|
|
4220
|
-
kind: "image",
|
|
4221
|
-
svg: display.data,
|
|
4222
|
-
width: display.width,
|
|
4223
|
-
height: display.height,
|
|
4224
|
-
source: fileName
|
|
4225
|
-
});
|
|
4226
|
-
break;
|
|
4227
|
-
case "reactor":
|
|
4228
|
-
onEvent({
|
|
4229
|
-
kind: "reactor",
|
|
4230
|
-
id: display.id,
|
|
4231
|
-
title: display.title,
|
|
4232
|
-
tickRate: display.tick_rate,
|
|
4233
|
-
ticking: display.ticking,
|
|
4234
|
-
wantsKeys: display.wants_keys,
|
|
4235
|
-
wantsMouse: display.wants_mouse,
|
|
4236
|
-
register: display.register,
|
|
4237
|
-
frame: display.frame,
|
|
4238
|
-
index: display.index,
|
|
4239
|
-
length: display.length,
|
|
4240
|
-
atEnd: display.at_end,
|
|
4241
|
-
stopped: display.stopped,
|
|
4242
|
-
valueRepr: display.value_repr
|
|
4243
|
-
});
|
|
4244
|
-
break;
|
|
4245
|
-
case "table":
|
|
4246
|
-
onEvent({
|
|
4247
|
-
kind: "table",
|
|
4248
|
-
columns: display.columns,
|
|
4249
|
-
rows: display.rows,
|
|
4250
|
-
rowCount: display.row_count,
|
|
4251
|
-
shownCount: display.shown_count,
|
|
4252
|
-
truncated: display.truncated,
|
|
4253
|
-
numeric: display.numeric,
|
|
4254
|
-
csv: display.csv,
|
|
4255
|
-
csvRows: display.csv_rows,
|
|
4256
|
-
source: fileName
|
|
4257
|
-
});
|
|
4258
|
-
break;
|
|
4259
|
-
}
|
|
4260
|
-
}
|
|
4261
|
-
function adaptTestCase(row) {
|
|
4262
|
-
return {
|
|
4263
|
-
name: String(row.name ?? ""),
|
|
4264
|
-
outcome: String(row.outcome ?? "failed"),
|
|
4265
|
-
lineNumber: typeof row.line_number === "number" ? row.line_number : null,
|
|
4266
|
-
message: row.message ?? null,
|
|
4267
|
-
stdout: row.stdout ?? null,
|
|
4268
|
-
error: pythonErrorFrom(row.error)
|
|
4269
|
-
};
|
|
4270
|
-
}
|
|
4271
|
-
function testReportFrom(result, fileName) {
|
|
4272
|
-
return {
|
|
4273
|
-
kind: "testReport",
|
|
4274
|
-
fileName,
|
|
4275
|
-
passed: result.passed ?? 0,
|
|
4276
|
-
failed: result.failed ?? 0,
|
|
4277
|
-
skipped: result.skipped ?? 0,
|
|
4278
|
-
errors: result.errors ?? 0,
|
|
4279
|
-
tests: Array.isArray(result.tests) ? result.tests.map(adaptTestCase) : [],
|
|
4280
|
-
...result.stopped ? { stopped: true, stoppedIn: result.stopped_in ?? null } : {}
|
|
4281
|
-
};
|
|
4282
|
-
}
|
|
4283
|
-
function pythonErrorFrom(wire) {
|
|
4284
|
-
if (!wire || !wire.error_type) {
|
|
4285
|
-
return null;
|
|
4286
|
-
}
|
|
4287
|
-
const facts = wire.error_facts ?? {};
|
|
4288
|
-
return {
|
|
4289
|
-
errorType: wire.error_type,
|
|
4290
|
-
message: wire.error_message ?? "",
|
|
4291
|
-
traceback: wire.traceback ?? "",
|
|
4292
|
-
fileName: wire.error_file ?? null,
|
|
4293
|
-
lineNumber: numberOrNull(wire.line_number),
|
|
4294
|
-
column: numberOrNull(wire.column),
|
|
4295
|
-
text: wire.error_text ?? null,
|
|
4296
|
-
nameToken: facts.name ?? null,
|
|
4297
|
-
frames: (wire.error_frames ?? []).map(frameFrom),
|
|
4298
|
-
facts: {
|
|
4299
|
-
name: facts.name ?? void 0,
|
|
4300
|
-
sequence: facts.sequence ?? void 0,
|
|
4301
|
-
length: numberOrNull(facts.length) ?? void 0,
|
|
4302
|
-
elementValue: facts.element_value ?? void 0,
|
|
4303
|
-
elementType: facts.element_type ?? void 0,
|
|
4304
|
-
swappedWith: facts.swapped_with ?? void 0,
|
|
4305
|
-
check: facts.check ? typeCheckFrom(facts.check) : void 0,
|
|
4306
|
-
definitions: facts.definitions ?? void 0,
|
|
4307
|
-
assigned: facts.assigned ?? void 0,
|
|
4308
|
-
returnedNone: facts.returned_none ? returnedNoneFrom(facts.returned_none) : void 0,
|
|
4309
|
-
stop: facts.stop === true ? true : void 0,
|
|
4310
|
-
module: facts.module ? {
|
|
4311
|
-
name: facts.module.name,
|
|
4312
|
-
kind: facts.module.kind,
|
|
4313
|
-
close: facts.module.close ?? null,
|
|
4314
|
-
why: facts.module.why ?? null,
|
|
4315
|
-
package: facts.module.package ?? null
|
|
4316
|
-
} : void 0,
|
|
4317
|
-
checks: facts.checks ? {
|
|
4318
|
-
file: facts.checks.file,
|
|
4319
|
-
level: isLevel(facts.checks.level) ? facts.checks.level : DEFAULT_LEVEL,
|
|
4320
|
-
findings: staticFindingsFrom(facts.checks.findings),
|
|
4321
|
-
headerProblem: facts.checks.header_problem ?? null
|
|
4322
|
-
} : void 0
|
|
4323
|
-
}
|
|
4324
|
-
};
|
|
4325
|
-
}
|
|
4326
|
-
function typeCheckFrom(check) {
|
|
4327
|
-
return {
|
|
4328
|
-
kind: check.kind,
|
|
4329
|
-
name: check.name ?? null,
|
|
4330
|
-
element: check.element ?? null,
|
|
4331
|
-
actual: check.actual ?? null,
|
|
4332
|
-
expected: check.expected ?? [],
|
|
4333
|
-
owner: check.owner ?? void 0,
|
|
4334
|
-
value: check.value ?? void 0,
|
|
4335
|
-
level: check.level && isLevel(check.level) ? check.level : void 0,
|
|
4336
|
-
annotation: check.annotation ?? null
|
|
4337
|
-
};
|
|
4338
|
-
}
|
|
4339
|
-
function returnedNoneFrom(wire) {
|
|
4340
|
-
const match = wire.match;
|
|
4341
|
-
return {
|
|
4342
|
-
match: match ? {
|
|
4343
|
-
subject: match.subject,
|
|
4344
|
-
patterns: match.patterns ?? [],
|
|
4345
|
-
fixedLength: match.fixed_length ?? [],
|
|
4346
|
-
hasList: match.has_list === true,
|
|
4347
|
-
uncovered: match.uncovered ?? []
|
|
4348
|
-
} : null,
|
|
4349
|
-
printed: wire.printed ?? null
|
|
4350
|
-
};
|
|
4351
|
-
}
|
|
4352
|
-
function frameFrom(frame) {
|
|
4353
|
-
return {
|
|
4354
|
-
fileName: frame.file,
|
|
4355
|
-
line: frame.line,
|
|
4356
|
-
column: numberOrNull(frame.column),
|
|
4357
|
-
functionName: frame.function ?? null,
|
|
4358
|
-
user: frame.user === true,
|
|
4359
|
-
text: frame.text ?? null,
|
|
4360
|
-
parameters: frame.parameters ?? null
|
|
4361
|
-
};
|
|
4362
|
-
}
|
|
4363
|
-
function numberOrNull(value) {
|
|
4364
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4365
|
-
}
|
|
4366
|
-
function replCheckFrom(raw) {
|
|
4367
|
-
return {
|
|
4368
|
-
status: raw.status,
|
|
4369
|
-
errorType: raw.error_type ?? void 0,
|
|
4370
|
-
message: raw.message ?? void 0,
|
|
4371
|
-
lineNumber: raw.lineno ?? void 0,
|
|
4372
|
-
offset: raw.offset ?? void 0
|
|
4373
|
-
};
|
|
4374
|
-
}
|
|
4375
|
-
function staticFindingsFrom(raw) {
|
|
4376
|
-
return (raw ?? []).map((finding) => camelized(finding));
|
|
4377
|
-
}
|
|
4378
|
-
function examplarOutcomeFrom(raw) {
|
|
4379
|
-
return {
|
|
4380
|
-
...camelized(raw),
|
|
4381
|
-
// `tests` and `attribution` are keyed by test name: their keys are the
|
|
4382
|
-
// student's, and stay as they are.
|
|
4383
|
-
wheats: raw.wheats?.map((impl) => camelized(impl)),
|
|
4384
|
-
chaffs: raw.chaffs?.map((impl) => camelized(impl))
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
function reactorStepFrom(raw) {
|
|
4388
|
-
if (raw.gone) {
|
|
4389
|
-
return { kind: "gone" };
|
|
4390
|
-
}
|
|
4391
|
-
if (!raw.ok || !raw.frame) {
|
|
4392
|
-
return { kind: "raised", error: pythonErrorFrom(raw) ?? pythonErrorFrom({ error_type: "Error" }) };
|
|
4393
|
-
}
|
|
4394
|
-
return {
|
|
4395
|
-
kind: "frame",
|
|
4396
|
-
frame: raw.frame,
|
|
4397
|
-
index: raw.index ?? 0,
|
|
4398
|
-
length: raw.length ?? 1,
|
|
4399
|
-
atEnd: raw.at_end === true,
|
|
4400
|
-
stopped: raw.stopped === true,
|
|
4401
|
-
valueRepr: raw.value_repr ?? "",
|
|
4402
|
-
messages: raw.messages ?? []
|
|
4403
|
-
};
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
// src/common/interruptBuffer.ts
|
|
4407
|
-
var INTERRUPT_SAB_BYTES = 2;
|
|
4408
|
-
var INTERRUPT_SIGINT = 2;
|
|
4409
|
-
var INTERRUPT_ACK_INDEX = 1;
|
|
4410
|
-
var INTERRUPT_RETRY_MS = 15;
|
|
4411
|
-
var INTERRUPT_RETRY_LIMIT_MS = 1e4;
|
|
4412
|
-
function tryCreateInterruptBuffer() {
|
|
4413
|
-
try {
|
|
4414
|
-
return new SharedArrayBuffer(INTERRUPT_SAB_BYTES);
|
|
4415
|
-
} catch {
|
|
4416
|
-
return null;
|
|
4417
|
-
}
|
|
4418
|
-
}
|
|
4419
|
-
function requestInterrupt(sab, stillRunning, retryMs = INTERRUPT_RETRY_MS, limitMs = INTERRUPT_RETRY_LIMIT_MS) {
|
|
4420
|
-
const view = new Uint8Array(sab);
|
|
4421
|
-
if (view.length > INTERRUPT_ACK_INDEX) {
|
|
4422
|
-
Atomics.store(view, INTERRUPT_ACK_INDEX, 0);
|
|
4423
|
-
}
|
|
4424
|
-
Atomics.store(view, 0, INTERRUPT_SIGINT);
|
|
4425
|
-
if (view.length <= INTERRUPT_ACK_INDEX) {
|
|
4426
|
-
return;
|
|
4427
|
-
}
|
|
4428
|
-
const deadline = Date.now() + limitMs;
|
|
4429
|
-
const retry = () => {
|
|
4430
|
-
if (Atomics.load(view, INTERRUPT_ACK_INDEX) !== 0 || !stillRunning() || Date.now() > deadline) {
|
|
4431
|
-
return;
|
|
4432
|
-
}
|
|
4433
|
-
if (Atomics.load(view, 0) === 0) {
|
|
4434
|
-
Atomics.store(view, 0, INTERRUPT_SIGINT);
|
|
4435
|
-
}
|
|
4436
|
-
setTimeout(retry, retryMs);
|
|
4437
|
-
};
|
|
4438
|
-
setTimeout(retry, retryMs);
|
|
4439
|
-
}
|
|
4440
|
-
|
|
4441
|
-
// src/common/stdinBuffer.ts
|
|
4442
|
-
var STDIN_SAB_BYTES = 64 * 1024;
|
|
4443
|
-
var STDIN_STATE_INDEX = 0;
|
|
4444
|
-
var STDIN_LENGTH_INDEX = 1;
|
|
4445
|
-
var STDIN_REQUEST_INDEX = 2;
|
|
4446
|
-
var STDIN_PAYLOAD_OFFSET = 16;
|
|
4447
|
-
var STDIN_STATE_WAITING = 0;
|
|
4448
|
-
var STDIN_STATE_DATA = 1;
|
|
4449
|
-
var STDIN_STATE_EOF = 2;
|
|
4450
|
-
var STDIN_STATE_INTERRUPTED = 3;
|
|
4451
|
-
function tryCreateStdinBuffer() {
|
|
4452
|
-
try {
|
|
4453
|
-
return new SharedArrayBuffer(STDIN_SAB_BYTES);
|
|
4454
|
-
} catch {
|
|
4455
|
-
return null;
|
|
4456
|
-
}
|
|
4457
|
-
}
|
|
4458
|
-
function writeStdin(sab, request, bytes) {
|
|
4459
|
-
const state = new Int32Array(sab);
|
|
4460
|
-
if (Atomics.load(state, STDIN_REQUEST_INDEX) !== request || Atomics.load(state, STDIN_STATE_INDEX) !== STDIN_STATE_WAITING) {
|
|
4461
|
-
return -1;
|
|
4462
|
-
}
|
|
4463
|
-
let n = 0;
|
|
4464
|
-
if (bytes !== null) {
|
|
4465
|
-
n = Math.min(bytes.length, sab.byteLength - STDIN_PAYLOAD_OFFSET);
|
|
4466
|
-
new Uint8Array(sab, STDIN_PAYLOAD_OFFSET).set(bytes.subarray(0, n));
|
|
4467
|
-
Atomics.store(state, STDIN_LENGTH_INDEX, n);
|
|
4468
|
-
}
|
|
4469
|
-
const answer = bytes === null ? STDIN_STATE_EOF : STDIN_STATE_DATA;
|
|
4470
|
-
const was = Atomics.compareExchange(state, STDIN_STATE_INDEX, STDIN_STATE_WAITING, answer);
|
|
4471
|
-
if (was !== STDIN_STATE_WAITING) {
|
|
4472
|
-
return -1;
|
|
4473
|
-
}
|
|
4474
|
-
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
4475
|
-
return n;
|
|
4476
|
-
}
|
|
4477
|
-
function interruptStdin(sab) {
|
|
4478
|
-
const state = new Int32Array(sab);
|
|
4479
|
-
if (Atomics.compareExchange(state, STDIN_STATE_INDEX, STDIN_STATE_WAITING, STDIN_STATE_INTERRUPTED) === STDIN_STATE_WAITING) {
|
|
4480
|
-
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
4481
|
-
}
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
// src/common/workerProtocol.ts
|
|
4485
|
-
var REPLY_TO = {
|
|
4486
|
-
init: "ready",
|
|
4487
|
-
runFile: "result",
|
|
4488
|
-
replEval: "result",
|
|
4489
|
-
checkSyntax: "syntax",
|
|
4490
|
-
loadPackages: "packagesReady",
|
|
4491
|
-
hasTests: "hasTests",
|
|
4492
|
-
loadPytest: "pytestReady",
|
|
4493
|
-
staticAnalyze: "static",
|
|
4494
|
-
mountWorkspace: "workspaceReady",
|
|
4495
|
-
collectWorkspace: "workspaceFiles",
|
|
4496
|
-
reactorStep: "reactorFrame",
|
|
4497
|
-
reactorSeek: "reactorFrame",
|
|
4498
|
-
reactorDispose: "reactorDisposed",
|
|
4499
|
-
endSession: "sessionEnded",
|
|
4500
|
-
examplarBuild: "examplarBuilt",
|
|
4501
|
-
examplarRun: "examplarRan"
|
|
4502
|
-
};
|
|
4503
|
-
|
|
4504
|
-
// src/common/workerRuntime.ts
|
|
4505
|
-
var NOT_RUNNING = "Python is not running.";
|
|
4506
|
-
var WorkerPythonRuntime = class {
|
|
4507
|
-
worker = null;
|
|
4508
|
-
nextId = 1;
|
|
4509
|
-
pending = /* @__PURE__ */ new Map();
|
|
4510
|
-
initPromise = null;
|
|
4511
|
-
stdinBuffer = null;
|
|
4512
|
-
interruptBuffer = null;
|
|
4513
|
-
stdinHandler = null;
|
|
4514
|
-
/** The stdin request being answered, or null; a Stop ends it. */
|
|
4515
|
-
stdinRequest = null;
|
|
4516
|
-
/** Stdin read but not yet given: what did not fit, or what came after a Stop. */
|
|
4517
|
-
stdinCarry = new Uint8Array(0);
|
|
4518
|
-
/** The handler's answer being waited for, which the next request shares. */
|
|
4519
|
-
stdinNext = null;
|
|
4520
|
-
packageNoteHandler = null;
|
|
4521
|
-
pythonLostHandler = null;
|
|
4522
|
-
/**
|
|
4523
|
-
* Longest Python may take to start, or null for no limit. Pyodide that
|
|
4524
|
-
* fails to load can report it and never settle; a host with no one to
|
|
4525
|
-
* press Stop - an autograder - sets one.
|
|
4526
|
-
*/
|
|
4527
|
-
startTimeoutMs = null;
|
|
4528
|
-
/** Where downloaded packages are kept, if not beside Pyodide's assets. */
|
|
4529
|
-
resolvePackageCacheDir(_indexUrl) {
|
|
4530
|
-
return null;
|
|
4531
|
-
}
|
|
4532
|
-
/** Start the worker, once - or again, if the last attempt failed. */
|
|
4533
|
-
async initialize() {
|
|
4534
|
-
if (!this.initPromise) {
|
|
4535
|
-
this.initPromise = this.doInitialize().catch((err) => {
|
|
4536
|
-
this.initPromise = null;
|
|
4537
|
-
this.worker?.terminate();
|
|
4538
|
-
this.worker = null;
|
|
4539
|
-
throw err;
|
|
4540
|
-
});
|
|
4541
|
-
}
|
|
4542
|
-
await this.initPromise;
|
|
4543
|
-
}
|
|
4544
|
-
async doInitialize() {
|
|
4545
|
-
const indexUrl = this.resolveIndexUrl();
|
|
4546
|
-
const worker = this.spawn({
|
|
4547
|
-
onMessage: (msg) => this.handleMessage(msg, worker),
|
|
4548
|
-
onError: (err) => this.rejectAllPending(err),
|
|
4549
|
-
onExit: () => this.pythonLost(worker)
|
|
4550
|
-
});
|
|
4551
|
-
this.worker = worker;
|
|
4552
|
-
this.stdinBuffer = tryCreateStdinBuffer();
|
|
4553
|
-
this.interruptBuffer = tryCreateInterruptBuffer();
|
|
4554
|
-
const packageCacheDir = this.resolvePackageCacheDir(indexUrl);
|
|
4555
|
-
const started = this.request({
|
|
4556
|
-
type: "init",
|
|
4557
|
-
indexUrl,
|
|
4558
|
-
...packageCacheDir ? { packageCacheDir } : {},
|
|
4559
|
-
...this.stdinBuffer ? { stdinBuffer: this.stdinBuffer } : {},
|
|
4560
|
-
...this.interruptBuffer ? { interruptBuffer: this.interruptBuffer } : {}
|
|
4561
|
-
});
|
|
4562
|
-
const limit = this.startTimeoutMs;
|
|
4563
|
-
if (limit === null) {
|
|
4564
|
-
await started;
|
|
4565
|
-
return;
|
|
4566
|
-
}
|
|
4567
|
-
let timer;
|
|
4568
|
-
const timedOut = new Promise((_, reject) => {
|
|
4569
|
-
timer = setTimeout(
|
|
4570
|
-
() => reject(new Error(`Python did not start within ${Math.round(limit / 1e3)} seconds`)),
|
|
4571
|
-
limit
|
|
4572
|
-
);
|
|
4573
|
-
});
|
|
4574
|
-
try {
|
|
4575
|
-
await Promise.race([started, timedOut]);
|
|
4576
|
-
} finally {
|
|
4577
|
-
clearTimeout(timer);
|
|
1917
|
+
if (!chaff.loaded) {
|
|
1918
|
+
bad += 1;
|
|
1919
|
+
view.problem(` BAD ${where}: ${chaff.errorMessage}`);
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (caught(chaff)) {
|
|
1923
|
+
const by = Object.entries(chaff.tests).filter(([, t]) => t.outcome !== "pass").map(([name]) => name);
|
|
1924
|
+
view.note(` ok ${where}: caught by ${by.join(", ")}`);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
bad += 1;
|
|
1928
|
+
view.problem(` BAD ${where}: no test catches it - it would never count`);
|
|
1929
|
+
}
|
|
1930
|
+
const attributed = Object.values(result.attribution ?? {});
|
|
1931
|
+
for (const name of result.chaffsSkipped ?? []) {
|
|
1932
|
+
if (attributed.some((names) => names.includes(name))) {
|
|
1933
|
+
view.problem(` --- ${name} chaffs not checked: its tests have to pass first`);
|
|
1934
|
+
continue;
|
|
4578
1935
|
}
|
|
1936
|
+
bad += 1;
|
|
1937
|
+
view.problem(
|
|
1938
|
+
` BAD your own suite has no tests for ${name}, so its chaffs are unproven`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
if (bad > 0) {
|
|
1942
|
+
view.problem(`pll: ${bad} problem(s) with this bundle; not written.`);
|
|
1943
|
+
return EXIT.testsFailed;
|
|
4579
1944
|
}
|
|
1945
|
+
view.problem("verified.");
|
|
1946
|
+
return EXIT.ok;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/cli/runtime.ts
|
|
1950
|
+
var fs6 = __toESM(require("node:fs"));
|
|
1951
|
+
var path6 = __toESM(require("node:path"));
|
|
1952
|
+
|
|
1953
|
+
// src/desktop/pyodideRuntime.ts
|
|
1954
|
+
var fs5 = __toESM(require("node:fs"));
|
|
1955
|
+
var path5 = __toESM(require("node:path"));
|
|
1956
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
1957
|
+
|
|
1958
|
+
// src/run/python.ts
|
|
1959
|
+
var Python = class extends WorkerConnection {
|
|
1960
|
+
/** Run a file: its output, errors and test report arrive through `onEvent`, then `done`. */
|
|
4580
1961
|
async runFile(request, onEvent) {
|
|
4581
|
-
|
|
4582
|
-
const live = { onEvent, fileName
|
|
4583
|
-
const
|
|
4584
|
-
deliverRunResult(result, onEvent,
|
|
1962
|
+
const { code, fileName, sessionKey, level = DEFAULT_LEVEL, withTests = false } = request;
|
|
1963
|
+
const live = { onEvent, fileName };
|
|
1964
|
+
const result = await this.call("_pll_run_file", [code, fileName, sessionKey, level, withTests], live);
|
|
1965
|
+
deliverRunResult(result, onEvent, fileName);
|
|
4585
1966
|
}
|
|
1967
|
+
/** Evaluate a prompt line against its file's session. */
|
|
4586
1968
|
async replEval(request, onEvent) {
|
|
4587
|
-
|
|
4588
|
-
const
|
|
1969
|
+
const { code, sessionKey, level = DEFAULT_LEVEL } = request;
|
|
1970
|
+
const result = await this.call("_pll_repl_eval", [code, sessionKey, level]);
|
|
4589
1971
|
deliverRunResult(result, onEvent, "<repl>");
|
|
4590
1972
|
}
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
1973
|
+
/**
|
|
1974
|
+
* Whether `code` is a complete prompt input (`codeop.compile_command`).
|
|
1975
|
+
* `whole` judges several lines submitted at once as one input (`exec`),
|
|
1976
|
+
* not as a shell line.
|
|
1977
|
+
*/
|
|
1978
|
+
checkReplComplete(code, whole = false) {
|
|
1979
|
+
return this.call("_pll_repl_check", [code, whole]);
|
|
4595
1980
|
}
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
return result;
|
|
1981
|
+
/** True if `code` contains pytest-style `test_*` functions or `Test*` classes. */
|
|
1982
|
+
hasTests(code) {
|
|
1983
|
+
return this.call("_pll_has_tests", [code]);
|
|
4600
1984
|
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Load any Pyodide packages the code imports (pandas, numpy, ...) - it or
|
|
1987
|
+
* the student's `.py` files beside it - wherever the import is. A no-op
|
|
1988
|
+
* when nothing imported maps to a known package; needs network the first
|
|
1989
|
+
* time it loads one.
|
|
1990
|
+
*/
|
|
4601
1991
|
async ensurePackages(code, siblings = []) {
|
|
4602
|
-
await this.
|
|
4603
|
-
|
|
1992
|
+
const found = await this.call("_pll_package_imports", [
|
|
1993
|
+
code,
|
|
1994
|
+
JSON.stringify(siblings)
|
|
1995
|
+
]);
|
|
1996
|
+
await this.request("load", found);
|
|
4604
1997
|
}
|
|
1998
|
+
/** Load pytest; needs network the first time. */
|
|
4605
1999
|
async ensurePytest() {
|
|
4606
|
-
await this.
|
|
4607
|
-
await this.request({ type: "loadPytest" });
|
|
2000
|
+
await this.request("load", { modules: ["pytest"], network: false });
|
|
4608
2001
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
2002
|
+
/**
|
|
2003
|
+
* The language level's static checks of a file, each finding worded.
|
|
2004
|
+
* None for a level without checks, or a file that does not parse - its
|
|
2005
|
+
* run says the SyntaxError.
|
|
2006
|
+
*/
|
|
2007
|
+
staticAnalyze(request) {
|
|
2008
|
+
const { code, level, fileName, sessionKey = null } = request;
|
|
2009
|
+
return this.call("_pll_static_findings", [code, level, fileName, sessionKey]);
|
|
4613
2010
|
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Copy the files under the program's folder into Pyodide's work directory
|
|
2013
|
+
* so `open("data/cars.csv")` / `pd.read_csv("cars.csv")` see them.
|
|
2014
|
+
* Replaces any files from a previous mount. `leftOut` are the files a
|
|
2015
|
+
* limit kept back, so reading one says why it is not there.
|
|
2016
|
+
*/
|
|
4614
2017
|
async mountWorkspaceFiles(files, leftOut = []) {
|
|
4615
|
-
await this.
|
|
4616
|
-
await this.request({ type: "mountWorkspace", files, leftOut });
|
|
2018
|
+
await this.request("mount", { files, leftOut });
|
|
4617
2019
|
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
return changes;
|
|
2020
|
+
/** What Python made, changed and deleted in the work directory since the last mount. */
|
|
2021
|
+
collectWorkspaceFiles() {
|
|
2022
|
+
return this.request("collect", {});
|
|
4622
2023
|
}
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* Compile Examplar wheats and chaffs into a bundle. `sources` is the JSON
|
|
2026
|
+
* of `{wheats: {id: source}, chaffs: {id: source}}`. Authoring only; the
|
|
2027
|
+
* bytecode is produced by *this* interpreter so its magic number matches
|
|
2028
|
+
* by construction.
|
|
2029
|
+
*/
|
|
2030
|
+
examplarBuild(sources) {
|
|
2031
|
+
return this.call("_pll_examplar_build", [sources]);
|
|
4627
2032
|
}
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
2033
|
+
/**
|
|
2034
|
+
* Run a student's tests against every implementation in a bundle.
|
|
2035
|
+
* `fileName` is the student's file, whose own errors are told from the
|
|
2036
|
+
* implementation's.
|
|
2037
|
+
*/
|
|
2038
|
+
examplarRun(testSource, bundle, fileName) {
|
|
2039
|
+
return this.call("_pll_examplar_run", [testSource, bundle, fileName]);
|
|
4632
2040
|
}
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
2041
|
+
/**
|
|
2042
|
+
* Apply one event to a running reactor and get the frame it produced.
|
|
2043
|
+
* `event` is the JSON of `{kind, ...}`; see `Reactor.react`. What its
|
|
2044
|
+
* handlers print or show arrives through `output`, as a run's does.
|
|
2045
|
+
*/
|
|
2046
|
+
reactorStep(reactorId, event, output) {
|
|
2047
|
+
return this.call("_pll_reactor_step", [reactorId, event], output);
|
|
4637
2048
|
}
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
return reactorStepFrom(result);
|
|
2049
|
+
/** Show an earlier or later recorded frame, applying no event. */
|
|
2050
|
+
reactorSeek(reactorId, index) {
|
|
2051
|
+
return this.call("_pll_reactor_seek", [reactorId, index]);
|
|
4642
2052
|
}
|
|
2053
|
+
/** Forget a reactor, so its recorded states can be collected. */
|
|
4643
2054
|
async reactorDispose(reactorId) {
|
|
4644
|
-
await this.
|
|
4645
|
-
await this.request({ type: "reactorDispose", reactorId });
|
|
2055
|
+
await this.call("_pll_reactor_dispose", [reactorId]);
|
|
4646
2056
|
}
|
|
2057
|
+
/** Forget a session and the names its runs defined. */
|
|
4647
2058
|
async endSession(sessionKey) {
|
|
4648
|
-
if (!this.
|
|
4649
|
-
await this.
|
|
4650
|
-
await this.request({ type: "endSession", sessionKey });
|
|
4651
|
-
}
|
|
4652
|
-
interrupt() {
|
|
4653
|
-
if (!this.interruptBuffer) {
|
|
4654
|
-
return false;
|
|
4655
|
-
}
|
|
4656
|
-
if (this.stdinBuffer && this.stdinRequest !== null) {
|
|
4657
|
-
this.stdinRequest = null;
|
|
4658
|
-
interruptStdin(this.stdinBuffer);
|
|
4659
|
-
}
|
|
4660
|
-
const running = new Set(this.pending.keys());
|
|
4661
|
-
requestInterrupt(this.interruptBuffer, () => {
|
|
4662
|
-
for (const id of running) {
|
|
4663
|
-
if (this.pending.has(id)) return true;
|
|
4664
|
-
}
|
|
4665
|
-
return false;
|
|
4666
|
-
});
|
|
4667
|
-
return true;
|
|
4668
|
-
}
|
|
4669
|
-
setPythonLostHandler(handler) {
|
|
4670
|
-
this.pythonLostHandler = handler;
|
|
4671
|
-
}
|
|
4672
|
-
setPackageNoteHandler(handler) {
|
|
4673
|
-
this.packageNoteHandler = handler;
|
|
4674
|
-
}
|
|
4675
|
-
setStdinHandler(handler) {
|
|
4676
|
-
this.stdinHandler = handler;
|
|
4677
|
-
}
|
|
4678
|
-
dispose() {
|
|
4679
|
-
this.worker?.terminate();
|
|
4680
|
-
this.worker = null;
|
|
4681
|
-
this.initPromise = null;
|
|
4682
|
-
this.stdinBuffer = null;
|
|
4683
|
-
this.interruptBuffer = null;
|
|
4684
|
-
this.rejectAllPending(new Error("Runtime disposed"));
|
|
4685
|
-
}
|
|
4686
|
-
/**
|
|
4687
|
-
* Send `msg` and resolve with its reply - the one `REPLY_TO` names for it.
|
|
4688
|
-
* `live` is where its output goes while it runs, for a request that
|
|
4689
|
-
* streams it.
|
|
4690
|
-
*/
|
|
4691
|
-
async request(msg, live = null) {
|
|
4692
|
-
const worker = this.worker;
|
|
4693
|
-
if (!worker) {
|
|
4694
|
-
throw new Error(NOT_RUNNING);
|
|
4695
|
-
}
|
|
4696
|
-
const id = this.nextId++;
|
|
4697
|
-
const reply = await new Promise((resolve4, reject) => {
|
|
4698
|
-
this.pending.set(id, { resolve: resolve4, reject, live });
|
|
4699
|
-
worker.post({ id, ...msg });
|
|
4700
|
-
});
|
|
4701
|
-
const expected = REPLY_TO[msg.type];
|
|
4702
|
-
if (reply.type !== expected) {
|
|
4703
|
-
throw new Error(`Pyodide worker replied "${reply.type}", expected "${expected}"`);
|
|
4704
|
-
}
|
|
4705
|
-
return reply;
|
|
4706
|
-
}
|
|
4707
|
-
handleMessage(msg, worker) {
|
|
4708
|
-
if (msg.type === "display") {
|
|
4709
|
-
const live = this.pending.get(msg.requestId)?.live;
|
|
4710
|
-
if (live) {
|
|
4711
|
-
deliverDisplay(msg.payload, live.onEvent, live.fileName);
|
|
4712
|
-
}
|
|
4713
|
-
return;
|
|
4714
|
-
}
|
|
4715
|
-
if (msg.type === "stdinRequest") {
|
|
4716
|
-
void this.handleStdinRequest(msg.request);
|
|
4717
|
-
return;
|
|
4718
|
-
}
|
|
4719
|
-
if (msg.type === "packageNote") {
|
|
4720
|
-
if (this.packageNoteHandler) this.packageNoteHandler(msg.text, msg.failed);
|
|
4721
|
-
else console.error(msg.text);
|
|
4722
|
-
return;
|
|
4723
|
-
}
|
|
4724
|
-
if (msg.type === "error" && msg.kind === "finished") {
|
|
4725
|
-
this.pythonLost(worker);
|
|
4726
|
-
return;
|
|
4727
|
-
}
|
|
4728
|
-
const pending = this.pending.get(msg.id);
|
|
4729
|
-
if (!pending) {
|
|
4730
|
-
return;
|
|
4731
|
-
}
|
|
4732
|
-
this.pending.delete(msg.id);
|
|
4733
|
-
if (msg.type === "error") {
|
|
4734
|
-
pending.reject(msg.kind === "interrupted" ? new StoppedError(msg.message) : new Error(msg.message));
|
|
4735
|
-
} else {
|
|
4736
|
-
pending.resolve(msg);
|
|
4737
|
-
}
|
|
2059
|
+
if (!this.started) return;
|
|
2060
|
+
await this.call("_pll_end_session", [sessionKey]);
|
|
4738
2061
|
}
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
* and forget it, so that the next request starts a new Python.
|
|
4744
|
-
*
|
|
4745
|
-
* Every session's names went with the old one - but they were gone
|
|
4746
|
-
* already: this follows Python stopping by itself, and never ends a
|
|
4747
|
-
* Python that could still run. Whoever set `setPythonLostHandler` is told,
|
|
4748
|
-
* so that each file can say so rather than only the one that was running.
|
|
4749
|
-
*/
|
|
4750
|
-
pythonLost(worker) {
|
|
4751
|
-
if (this.worker !== worker) return;
|
|
4752
|
-
this.rejectAllPending(new PythonLostError());
|
|
4753
|
-
this.worker = null;
|
|
4754
|
-
this.initPromise = null;
|
|
4755
|
-
worker.terminate();
|
|
4756
|
-
this.pythonLostHandler?.();
|
|
2062
|
+
};
|
|
2063
|
+
function deliverRunResult(result, onEvent, fileName) {
|
|
2064
|
+
for (const display of result.displays) {
|
|
2065
|
+
deliverDisplay(display, onEvent, fileName);
|
|
4757
2066
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
pending.reject(err);
|
|
4761
|
-
}
|
|
4762
|
-
this.pending.clear();
|
|
2067
|
+
if (result.resultRepr !== null) {
|
|
2068
|
+
onEvent({ kind: "result", repr: result.resultRepr });
|
|
4763
2069
|
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
* Stop ended leaves its call waiting, and the next request - a program
|
|
4767
|
-
* that caught the `KeyboardInterrupt` and asked again - shares it, so
|
|
4768
|
-
* the line typed answers that one.
|
|
4769
|
-
*/
|
|
4770
|
-
nextStdin() {
|
|
4771
|
-
if (this.stdinNext === null) {
|
|
4772
|
-
const call = (async () => {
|
|
4773
|
-
try {
|
|
4774
|
-
const next = this.stdinHandler ? await this.stdinHandler() : null;
|
|
4775
|
-
return typeof next === "string" ? new TextEncoder().encode(next) : next;
|
|
4776
|
-
} catch {
|
|
4777
|
-
return null;
|
|
4778
|
-
}
|
|
4779
|
-
})();
|
|
4780
|
-
this.stdinNext = call;
|
|
4781
|
-
void call.then(() => {
|
|
4782
|
-
if (this.stdinNext === call) this.stdinNext = null;
|
|
4783
|
-
});
|
|
4784
|
-
}
|
|
4785
|
-
return this.stdinNext;
|
|
2070
|
+
if (result.error !== null) {
|
|
2071
|
+
onEvent({ kind: "error", error: result.error, fileName });
|
|
4786
2072
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
let bytes = this.stdinCarry;
|
|
4790
|
-
if (bytes.length === 0) {
|
|
4791
|
-
bytes = await this.nextStdin();
|
|
4792
|
-
if (this.stdinRequest !== request) {
|
|
4793
|
-
if (bytes !== null && this.stdinRequest === null) {
|
|
4794
|
-
this.stdinCarry = concatBytes(this.stdinCarry, bytes);
|
|
4795
|
-
}
|
|
4796
|
-
return;
|
|
4797
|
-
}
|
|
4798
|
-
}
|
|
4799
|
-
this.stdinRequest = null;
|
|
4800
|
-
if (!this.stdinBuffer) {
|
|
4801
|
-
return;
|
|
4802
|
-
}
|
|
4803
|
-
const given = writeStdin(this.stdinBuffer, request, bytes);
|
|
4804
|
-
if (bytes === null) {
|
|
4805
|
-
this.stdinCarry = new Uint8Array(0);
|
|
4806
|
-
} else {
|
|
4807
|
-
this.stdinCarry = bytes.subarray(Math.max(given, 0));
|
|
4808
|
-
}
|
|
2073
|
+
if (result.tests !== null) {
|
|
2074
|
+
onEvent({ kind: "testReport", fileName, ...result.tests });
|
|
4809
2075
|
}
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
return out;
|
|
2076
|
+
onEvent({
|
|
2077
|
+
kind: "done",
|
|
2078
|
+
...result.exitCode !== null ? { exitCode: result.exitCode } : {},
|
|
2079
|
+
...result.stopped ? { stopped: true } : {}
|
|
2080
|
+
});
|
|
4816
2081
|
}
|
|
4817
2082
|
|
|
4818
2083
|
// src/desktop/pyodideRuntime.ts
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
2084
|
+
function desktopPython(paths) {
|
|
2085
|
+
return new Python({
|
|
2086
|
+
indexUrl() {
|
|
2087
|
+
const found = paths.indexUrlCandidates.find((dir) => fs5.existsSync(path5.join(dir, "pyodide.asm.wasm")));
|
|
2088
|
+
if (!found) {
|
|
2089
|
+
throw new Error(paths.missingAssetsHint);
|
|
2090
|
+
}
|
|
2091
|
+
return found;
|
|
2092
|
+
},
|
|
2093
|
+
packageCacheDir: paths.packageCacheDir,
|
|
2094
|
+
startTimeoutMs: paths.startTimeoutMs,
|
|
2095
|
+
spawn(handlers) {
|
|
2096
|
+
const worker = new import_node_worker_threads.Worker(paths.workerPath);
|
|
2097
|
+
worker.on("message", (msg) => handlers.onMessage(msg));
|
|
2098
|
+
worker.on("error", (err) => console.error("Pyodide worker failed:", err));
|
|
2099
|
+
worker.on("exit", () => handlers.onExit?.());
|
|
2100
|
+
return {
|
|
2101
|
+
post: (msg) => worker.postMessage(msg),
|
|
2102
|
+
terminate: () => void worker.terminate()
|
|
2103
|
+
};
|
|
4832
2104
|
}
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
resolvePackageCacheDir(indexUrl) {
|
|
4836
|
-
return this.paths.packageCacheDir?.(indexUrl) ?? null;
|
|
4837
|
-
}
|
|
4838
|
-
spawn(handlers) {
|
|
4839
|
-
const worker = new import_node_worker_threads.Worker(this.paths.workerPath);
|
|
4840
|
-
worker.on("message", (msg) => handlers.onMessage(msg));
|
|
4841
|
-
worker.on("error", (err) => console.error("Pyodide worker failed:", err));
|
|
4842
|
-
worker.on("exit", () => handlers.onExit?.());
|
|
4843
|
-
return {
|
|
4844
|
-
post: (msg) => worker.postMessage(msg),
|
|
4845
|
-
terminate: () => void worker.terminate()
|
|
4846
|
-
};
|
|
4847
|
-
}
|
|
4848
|
-
};
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
4849
2107
|
|
|
4850
2108
|
// src/cli/runtime.ts
|
|
4851
2109
|
function createCliRuntime() {
|
|
4852
|
-
return
|
|
2110
|
+
return desktopPython({
|
|
4853
2111
|
indexUrlCandidates: pyodideDirectory(),
|
|
4854
2112
|
workerPath: path6.join(__dirname, "worker.cjs"),
|
|
4855
2113
|
missingAssetsHint: "Could not find Pyodide's assets. Reinstall pll-python so its `pyodide` dependency is present.",
|
|
@@ -4933,26 +2191,6 @@ function createStdinReader() {
|
|
|
4933
2191
|
// src/cli/view.ts
|
|
4934
2192
|
var fs7 = __toESM(require("node:fs"));
|
|
4935
2193
|
var path7 = __toESM(require("node:path"));
|
|
4936
|
-
|
|
4937
|
-
// src/common/errorFormatter.ts
|
|
4938
|
-
function formatFriendlyError(finding) {
|
|
4939
|
-
const lines = [];
|
|
4940
|
-
lines.push(`${finding.errorType}: ${finding.headline}`);
|
|
4941
|
-
const location = findingLocation(finding);
|
|
4942
|
-
if (location) {
|
|
4943
|
-
lines.push(` at ${location.label}`);
|
|
4944
|
-
}
|
|
4945
|
-
if (finding.howToFix.length > 0) {
|
|
4946
|
-
lines.push("");
|
|
4947
|
-
lines.push("How to fix:");
|
|
4948
|
-
for (const item of finding.howToFix) {
|
|
4949
|
-
lines.push(` - ${item}`);
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
return lines;
|
|
4953
|
-
}
|
|
4954
|
-
|
|
4955
|
-
// src/cli/view.ts
|
|
4956
2194
|
var ESC = String.fromCharCode(27);
|
|
4957
2195
|
var CliView = class {
|
|
4958
2196
|
constructor(opts) {
|
|
@@ -5328,7 +2566,7 @@ ${USAGE_HINT}
|
|
|
5328
2566
|
return EXIT.ok;
|
|
5329
2567
|
}
|
|
5330
2568
|
if (args.version) {
|
|
5331
|
-
process.stdout.write("0.
|
|
2569
|
+
process.stdout.write("0.5.0\n");
|
|
5332
2570
|
return EXIT.ok;
|
|
5333
2571
|
}
|
|
5334
2572
|
if (args.error !== void 0) {
|