pll-python 0.1.0 → 0.1.2
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/README.md +124 -1
- package/cli.cjs +1493 -1183
- package/package.json +2 -2
- package/worker.cjs +21 -1
package/cli.cjs
CHANGED
|
@@ -24,1329 +24,1616 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli/main.ts
|
|
27
|
-
var
|
|
27
|
+
var path7 = __toESM(require("node:path"));
|
|
28
28
|
|
|
29
|
-
// src/cli/
|
|
30
|
-
var
|
|
29
|
+
// src/cli/examplar.ts
|
|
30
|
+
var fs3 = __toESM(require("node:fs/promises"));
|
|
31
|
+
var path3 = __toESM(require("node:path"));
|
|
31
32
|
|
|
32
|
-
// src/
|
|
33
|
-
var
|
|
34
|
-
var
|
|
35
|
-
var import_node_worker_threads = require("node:worker_threads");
|
|
33
|
+
// src/cli/run.ts
|
|
34
|
+
var fs2 = __toESM(require("node:fs/promises"));
|
|
35
|
+
var path2 = __toESM(require("node:path"));
|
|
36
36
|
|
|
37
|
-
// src/common/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
message: result.error_message ?? "",
|
|
52
|
-
traceback: result.traceback ?? "",
|
|
53
|
-
lineNumber: result.line_number,
|
|
54
|
-
column: result.column,
|
|
55
|
-
fileName
|
|
56
|
-
});
|
|
37
|
+
// src/common/analyzers/static/shadowingExplainer.ts
|
|
38
|
+
function outerScopeNoun(kind) {
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case "module":
|
|
41
|
+
return "the file";
|
|
42
|
+
case "function":
|
|
43
|
+
case "lambda":
|
|
44
|
+
return "an enclosing function";
|
|
45
|
+
case "class":
|
|
46
|
+
return "an enclosing class";
|
|
47
|
+
case "comprehension":
|
|
48
|
+
return "an enclosing comprehension";
|
|
49
|
+
default:
|
|
50
|
+
return "an outer scope";
|
|
57
51
|
}
|
|
58
|
-
onEvent({ kind: "done" });
|
|
59
52
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
function explainShadowing(raw, level, fileName) {
|
|
54
|
+
const name = raw.name_token ?? "this name";
|
|
55
|
+
const outerLine = raw.outer_line_number ?? null;
|
|
56
|
+
const outerNoun = outerScopeNoun(raw.outer_scope_kind);
|
|
57
|
+
const knownOuterLine = outerLine !== null && outerLine > 0;
|
|
58
|
+
const outerHint = knownOuterLine ? ` (first defined on line ${outerLine}, in ${outerNoun})` : ` in ${outerNoun}`;
|
|
59
|
+
return {
|
|
60
|
+
id: "shadowing",
|
|
61
|
+
errorType: "Shadowing",
|
|
62
|
+
message: raw.message,
|
|
63
|
+
headline: `\`${name}\` is already defined${outerHint}.`,
|
|
64
|
+
howToFix: [
|
|
65
|
+
`Rename the inner \`${name}\` to something distinct (e.g. \`${name}_inner\`, \`local_${name}\`, or whatever describes its role).`,
|
|
66
|
+
`Or, if you want to use the outer \`${name}\`, just read it directly without making a new variable.`
|
|
67
|
+
],
|
|
68
|
+
fileName,
|
|
69
|
+
lineNumber: raw.line_number,
|
|
70
|
+
column: raw.column,
|
|
71
|
+
nameToken: raw.name_token,
|
|
72
|
+
severity: "error",
|
|
73
|
+
raw: JSON.stringify(raw),
|
|
74
|
+
origin: "static",
|
|
75
|
+
level
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function explainShadowingBuiltin(raw, level, fileName) {
|
|
79
|
+
const name = raw.name_token ?? "this name";
|
|
80
|
+
const headline = `\`${name}\` is the name of a Python built-in.`;
|
|
81
|
+
return {
|
|
82
|
+
id: "shadowing-builtin",
|
|
83
|
+
errorType: "Shadowing",
|
|
84
|
+
message: raw.message,
|
|
85
|
+
headline,
|
|
86
|
+
howToFix: [
|
|
87
|
+
`Pick a different name. Common patterns: \`my_${name}\`, \`${name}_value\`, \`${name}s\` (plural), or a more specific noun describing what this value represents.`
|
|
88
|
+
],
|
|
89
|
+
fileName,
|
|
90
|
+
lineNumber: raw.line_number,
|
|
91
|
+
column: raw.column,
|
|
92
|
+
nameToken: raw.name_token,
|
|
93
|
+
severity: "error",
|
|
94
|
+
raw: JSON.stringify(raw),
|
|
95
|
+
origin: "static",
|
|
96
|
+
level
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function libraryNoun(library) {
|
|
100
|
+
switch (library) {
|
|
68
101
|
case "image":
|
|
69
|
-
|
|
70
|
-
kind: "image",
|
|
71
|
-
svg: display.data,
|
|
72
|
-
width: display.width,
|
|
73
|
-
height: display.height,
|
|
74
|
-
source: fileName
|
|
75
|
-
});
|
|
76
|
-
break;
|
|
77
|
-
case "reactor":
|
|
78
|
-
onEvent({
|
|
79
|
-
kind: "reactor",
|
|
80
|
-
id: display.id,
|
|
81
|
-
title: display.title,
|
|
82
|
-
tickRate: display.tick_rate,
|
|
83
|
-
ticking: display.ticking,
|
|
84
|
-
wantsKeys: display.wants_keys,
|
|
85
|
-
wantsMouse: display.wants_mouse,
|
|
86
|
-
register: display.register,
|
|
87
|
-
frame: display.frame,
|
|
88
|
-
index: display.index,
|
|
89
|
-
length: display.length,
|
|
90
|
-
atEnd: display.at_end,
|
|
91
|
-
stopped: display.stopped,
|
|
92
|
-
valueRepr: display.value_repr
|
|
93
|
-
});
|
|
94
|
-
break;
|
|
102
|
+
return "the image library";
|
|
95
103
|
case "table":
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
shownCount: display.shown_count,
|
|
102
|
-
truncated: display.truncated,
|
|
103
|
-
source: fileName
|
|
104
|
-
});
|
|
105
|
-
break;
|
|
104
|
+
return "the table library";
|
|
105
|
+
case "reactor":
|
|
106
|
+
return "the reactor library";
|
|
107
|
+
default:
|
|
108
|
+
return "the PLL libraries";
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
function explainShadowingLibrary(raw, level, fileName) {
|
|
112
|
+
const name = raw.name_token ?? "this name";
|
|
113
|
+
const noun = libraryNoun(raw.library);
|
|
114
|
+
const headline = `\`${name}\` is already defined by ${noun}.`;
|
|
115
|
+
return {
|
|
116
|
+
id: "shadowing-library",
|
|
117
|
+
errorType: "Shadowing",
|
|
118
|
+
message: raw.message,
|
|
119
|
+
headline,
|
|
120
|
+
howToFix: [
|
|
121
|
+
`Pick a different name for your definition - for example \`my_${name}\`, \`${name}_value\`, or a word that describes what yours represents.`,
|
|
122
|
+
`If you meant to use the library's \`${name}\`, call it directly instead of defining a new \`${name}\` of your own.`
|
|
123
|
+
],
|
|
124
|
+
fileName,
|
|
125
|
+
lineNumber: raw.line_number,
|
|
126
|
+
column: raw.column,
|
|
127
|
+
nameToken: raw.name_token,
|
|
128
|
+
severity: "error",
|
|
129
|
+
raw: JSON.stringify(raw),
|
|
130
|
+
origin: "static",
|
|
131
|
+
level
|
|
132
|
+
};
|
|
120
133
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
|
|
135
|
+
// src/common/analyzers/static/reassignmentExplainer.ts
|
|
136
|
+
function explainReassignment(raw, level, fileName) {
|
|
137
|
+
const name = raw.name_token ?? "this name";
|
|
138
|
+
const firstLine = raw.first_line_number ?? null;
|
|
139
|
+
const firstLineHint = firstLine !== null && firstLine > 0 ? ` (first assigned on line ${firstLine})` : "";
|
|
124
140
|
return {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
id: "reassignment",
|
|
142
|
+
errorType: "Reassignment",
|
|
143
|
+
message: raw.message,
|
|
144
|
+
headline: `\`${name}\` is already assigned${firstLineHint}.`,
|
|
145
|
+
howToFix: level === "intermediate" ? [
|
|
146
|
+
`If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
|
|
147
|
+
`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}(...)\`.`
|
|
148
|
+
] : [
|
|
149
|
+
`If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
|
|
150
|
+
`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.`
|
|
151
|
+
],
|
|
152
|
+
fileName,
|
|
153
|
+
lineNumber: raw.line_number,
|
|
154
|
+
column: raw.column,
|
|
155
|
+
nameToken: raw.name_token,
|
|
156
|
+
severity: "error",
|
|
157
|
+
raw: JSON.stringify(raw),
|
|
158
|
+
origin: "static",
|
|
159
|
+
level
|
|
130
160
|
};
|
|
131
161
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
|
|
163
|
+
// src/common/analyzers/static/disallowedKeywordExplainer.ts
|
|
164
|
+
function explainDisallowedKeyword(raw, level, fileName) {
|
|
165
|
+
const keyword = raw.keyword === "nonlocal" ? "nonlocal" : "global";
|
|
166
|
+
const names = Array.isArray(raw.names) ? raw.names : [];
|
|
167
|
+
const formattedNames = names.map((n) => `\`${n}\``).join(", ");
|
|
168
|
+
const namesPhrase = formattedNames.length > 0 ? ` ${formattedNames}` : "";
|
|
169
|
+
const headline = `\`${keyword}\` is not allowed at the ${level} level.`;
|
|
170
|
+
const howToFix = [];
|
|
171
|
+
if (keyword === "global") {
|
|
172
|
+
howToFix.push(
|
|
173
|
+
`Pass${namesPhrase || " the value"} into this function as an argument instead of declaring it \`global\`.`,
|
|
174
|
+
`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)\`).`
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
howToFix.push(
|
|
178
|
+
`Return${namesPhrase || " the new value"} from this inner function and have the outer function rebind it explicitly (e.g. \`x = inner(x)\`).`,
|
|
179
|
+
`If the inner function only needs to *read* the outer name, just use it directly - you don't need \`nonlocal\` for that.`
|
|
180
|
+
);
|
|
145
181
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
return {
|
|
183
|
+
id: "disallowed-keyword",
|
|
184
|
+
errorType: "DisallowedKeyword",
|
|
185
|
+
message: raw.message,
|
|
186
|
+
headline,
|
|
187
|
+
howToFix,
|
|
149
188
|
fileName,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
lineNumber: raw.line_number,
|
|
190
|
+
column: raw.column,
|
|
191
|
+
nameToken: raw.name_token,
|
|
192
|
+
severity: "error",
|
|
193
|
+
raw: JSON.stringify(raw),
|
|
194
|
+
origin: "static",
|
|
195
|
+
level
|
|
196
|
+
};
|
|
157
197
|
}
|
|
158
198
|
|
|
159
|
-
// src/common/
|
|
160
|
-
var
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
199
|
+
// src/common/analyzers/static/registry.ts
|
|
200
|
+
var explainers = {
|
|
201
|
+
shadowing: explainShadowing,
|
|
202
|
+
"shadowing-builtin": explainShadowingBuiltin,
|
|
203
|
+
"shadowing-library": explainShadowingLibrary,
|
|
204
|
+
reassignment: explainReassignment,
|
|
205
|
+
"disallowed-keyword": explainDisallowedKeyword
|
|
206
|
+
};
|
|
207
|
+
function fallbackExplainer(raw, level, fileName) {
|
|
208
|
+
return {
|
|
209
|
+
id: raw.id,
|
|
210
|
+
errorType: raw.error_type || "StaticError",
|
|
211
|
+
message: raw.message,
|
|
212
|
+
headline: raw.message,
|
|
213
|
+
howToFix: [],
|
|
214
|
+
fileName,
|
|
215
|
+
lineNumber: raw.line_number,
|
|
216
|
+
column: raw.column,
|
|
217
|
+
nameToken: raw.name_token,
|
|
218
|
+
severity: "error",
|
|
219
|
+
raw: JSON.stringify(raw),
|
|
220
|
+
origin: "static",
|
|
221
|
+
level
|
|
222
|
+
};
|
|
168
223
|
}
|
|
169
|
-
function
|
|
170
|
-
|
|
224
|
+
function enrichStaticFindings(rawFindings, level, fileName) {
|
|
225
|
+
return rawFindings.map((raw) => {
|
|
226
|
+
const explainer = explainers[raw.id] ?? fallbackExplainer;
|
|
227
|
+
return explainer(raw, level, fileName);
|
|
228
|
+
});
|
|
171
229
|
}
|
|
172
230
|
|
|
173
|
-
// src/common/
|
|
174
|
-
var
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
// src/common/errors/pythonErrorParser.ts
|
|
232
|
+
var NON_USER_FRAME = [
|
|
233
|
+
"<exec>",
|
|
234
|
+
"/pll_vendor",
|
|
235
|
+
"site-packages",
|
|
236
|
+
"/lib/python",
|
|
237
|
+
"_pytest",
|
|
238
|
+
"pluggy"
|
|
239
|
+
];
|
|
240
|
+
function userTracebackFrames(traceback) {
|
|
241
|
+
const frames = [];
|
|
242
|
+
for (const raw of traceback.split(/\r?\n/)) {
|
|
243
|
+
const m = raw.match(/^\s*File "([^"]+)", line (\d+)(?:, in (\S+))?/);
|
|
244
|
+
if (!m) continue;
|
|
245
|
+
const fileName = m[1];
|
|
246
|
+
if (NON_USER_FRAME.some((token) => fileName.includes(token))) continue;
|
|
247
|
+
frames.push({
|
|
248
|
+
fileName,
|
|
249
|
+
line: parseInt(m[2], 10),
|
|
250
|
+
functionName: m[3] && m[3] !== "<module>" ? m[3] : null
|
|
251
|
+
});
|
|
185
252
|
}
|
|
253
|
+
return frames;
|
|
186
254
|
}
|
|
187
|
-
function
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
255
|
+
function parsePythonError(rawTraceback) {
|
|
256
|
+
const traceback = rawTraceback.trimEnd();
|
|
257
|
+
const lines = traceback.split(/\r?\n/);
|
|
258
|
+
let errorType = "Error";
|
|
259
|
+
let message = traceback;
|
|
260
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
261
|
+
const line = lines[i];
|
|
262
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_.]*):\s?(.*)$/);
|
|
263
|
+
if (m && !line.startsWith(" ")) {
|
|
264
|
+
errorType = m[1];
|
|
265
|
+
message = m[2];
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (line.match(/^[A-Za-z_][A-Za-z0-9_.]*$/) && !line.startsWith(" ")) {
|
|
269
|
+
errorType = line;
|
|
270
|
+
message = "";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
193
273
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// src/common/workerRuntime.ts
|
|
205
|
-
var WorkerPythonRuntime = class {
|
|
206
|
-
worker = null;
|
|
207
|
-
nextId = 1;
|
|
208
|
-
pending = /* @__PURE__ */ new Map();
|
|
209
|
-
initPromise = null;
|
|
210
|
-
stdinBuffer = null;
|
|
211
|
-
interruptBuffer = null;
|
|
212
|
-
stdinHandler = null;
|
|
213
|
-
/** Event sink for the in-flight runFile, so live displays can stream. */
|
|
214
|
-
live = null;
|
|
215
|
-
async initialize() {
|
|
216
|
-
if (!this.initPromise) {
|
|
217
|
-
this.initPromise = this.doInitialize();
|
|
274
|
+
let fileName = null;
|
|
275
|
+
let lineNumber = null;
|
|
276
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
277
|
+
const line = lines[i];
|
|
278
|
+
const m = line.match(/^\s*File\s+"([^"]+)",\s+line\s+(\d+)/);
|
|
279
|
+
if (m) {
|
|
280
|
+
fileName = m[1];
|
|
281
|
+
lineNumber = parseInt(m[2], 10);
|
|
282
|
+
break;
|
|
218
283
|
}
|
|
219
|
-
await this.initPromise;
|
|
220
284
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
indexUrl,
|
|
233
|
-
...this.stdinBuffer ? { stdinBuffer: this.stdinBuffer } : {},
|
|
234
|
-
...this.interruptBuffer ? { interruptBuffer: this.interruptBuffer } : {}
|
|
235
|
-
},
|
|
236
|
-
"ready"
|
|
237
|
-
);
|
|
285
|
+
let column = null;
|
|
286
|
+
if (lineNumber !== null) {
|
|
287
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
288
|
+
if (/^\s*\^+\s*$/.test(lines[i + 1] ?? "")) {
|
|
289
|
+
const caretLine = lines[i + 1];
|
|
290
|
+
const caretIdx = caretLine.indexOf("^");
|
|
291
|
+
if (caretIdx >= 0) {
|
|
292
|
+
column = caretIdx;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
238
296
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
deliverRunResult(result, onEvent, request.fileName);
|
|
245
|
-
} finally {
|
|
246
|
-
this.live = null;
|
|
297
|
+
let nameToken = null;
|
|
298
|
+
if (errorType === "NameError") {
|
|
299
|
+
const nm = message.match(/name '([^']+)' is not defined/);
|
|
300
|
+
if (nm) {
|
|
301
|
+
nameToken = nm[1];
|
|
247
302
|
}
|
|
248
303
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
return {
|
|
305
|
+
errorType,
|
|
306
|
+
message,
|
|
307
|
+
traceback,
|
|
308
|
+
fileName,
|
|
309
|
+
lineNumber,
|
|
310
|
+
column,
|
|
311
|
+
nameToken
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/common/errors/nameErrorExplainer.ts
|
|
316
|
+
function explainNameError(parsed) {
|
|
317
|
+
const name = parsed.nameToken ?? "this name";
|
|
318
|
+
const didYouMean = extractDidYouMean(parsed.message);
|
|
319
|
+
const headline = `Python doesn't know what \`${name}\` means.`;
|
|
320
|
+
const howToFix = [
|
|
321
|
+
`Check the spelling of \`${name}\` (Python is case-sensitive).`,
|
|
322
|
+
`Make sure \`${name}\` is defined before this line runs.`,
|
|
323
|
+
`If \`${name}\` should be text, put it in quotes: \`"${name}"\`.`
|
|
324
|
+
];
|
|
325
|
+
if (didYouMean.length > 0) {
|
|
326
|
+
howToFix.unshift(
|
|
327
|
+
`Python suggested: ${didYouMean.map((s) => "`" + s + "`").join(", ")} - try one of those.`
|
|
328
|
+
);
|
|
253
329
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
330
|
+
return { headline, howToFix };
|
|
331
|
+
}
|
|
332
|
+
function extractDidYouMean(message) {
|
|
333
|
+
const m = message.match(/Did you mean:\s*([^?]+)\??/i);
|
|
334
|
+
if (!m) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
return m[1].split(/,|\bor\b/).map((s) => s.trim().replace(/^['"`]|['"`]$/g, "")).filter((s) => s.length > 0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/common/analyzers/nameErrorAnalyzer.ts
|
|
341
|
+
var nameErrorAnalyzer = {
|
|
342
|
+
handles: ["NameError"],
|
|
343
|
+
analyze(input) {
|
|
344
|
+
const { parsedError, fileName, level } = input;
|
|
345
|
+
if (parsedError.errorType !== "NameError") {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const explanation = explainNameError(parsedError);
|
|
257
349
|
return {
|
|
258
|
-
|
|
259
|
-
errorType:
|
|
260
|
-
message:
|
|
261
|
-
|
|
262
|
-
|
|
350
|
+
id: "name-error",
|
|
351
|
+
errorType: "NameError",
|
|
352
|
+
message: parsedError.message,
|
|
353
|
+
headline: explanation.headline,
|
|
354
|
+
howToFix: explanation.howToFix,
|
|
355
|
+
fileName,
|
|
356
|
+
lineNumber: parsedError.lineNumber,
|
|
357
|
+
column: parsedError.column,
|
|
358
|
+
nameToken: parsedError.nameToken,
|
|
359
|
+
severity: "error",
|
|
360
|
+
raw: parsedError.traceback,
|
|
361
|
+
origin: "runtime",
|
|
362
|
+
level
|
|
263
363
|
};
|
|
264
364
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await this.initialize();
|
|
293
|
-
const { result } = await this.request({ type: "staticAnalyze", ...request }, "static");
|
|
294
|
-
return result ?? [];
|
|
295
|
-
}
|
|
296
|
-
async mountWorkspaceFiles(files) {
|
|
297
|
-
await this.initialize();
|
|
298
|
-
await this.request({ type: "mountWorkspace", files }, "workspaceReady");
|
|
299
|
-
}
|
|
300
|
-
async collectWorkspaceFiles() {
|
|
301
|
-
await this.initialize();
|
|
302
|
-
const { files } = await this.request({ type: "collectWorkspace" }, "workspaceFiles");
|
|
303
|
-
return files;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/common/level.ts
|
|
368
|
+
var DEFAULT_LEVEL = "raw";
|
|
369
|
+
var HEADER_RE = /^#\s*level\s+([a-z]+)\s*$/;
|
|
370
|
+
var LEVEL_NAMES = [
|
|
371
|
+
"raw",
|
|
372
|
+
"beginner",
|
|
373
|
+
"intermediate",
|
|
374
|
+
"advanced"
|
|
375
|
+
];
|
|
376
|
+
function parseLevel(source) {
|
|
377
|
+
const lines = source.split(/\r?\n/);
|
|
378
|
+
for (const raw of lines) {
|
|
379
|
+
const line = raw.trim();
|
|
380
|
+
if (line.length === 0) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const match = HEADER_RE.exec(line);
|
|
384
|
+
if (!match) {
|
|
385
|
+
return DEFAULT_LEVEL;
|
|
386
|
+
}
|
|
387
|
+
const name = match[1];
|
|
388
|
+
for (const level of LEVEL_NAMES) {
|
|
389
|
+
if (name === level) return level;
|
|
390
|
+
}
|
|
391
|
+
return DEFAULT_LEVEL;
|
|
304
392
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
393
|
+
return DEFAULT_LEVEL;
|
|
394
|
+
}
|
|
395
|
+
function levelRejectsBoolAsNumber(level) {
|
|
396
|
+
return level === "beginner" || level === "intermediate";
|
|
397
|
+
}
|
|
398
|
+
function levelHasStaticChecks(level) {
|
|
399
|
+
return level === "beginner" || level === "intermediate";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/common/errors/typeCheckExplainer.ts
|
|
403
|
+
var FRIENDLY_TYPES = {
|
|
404
|
+
int: "a whole number",
|
|
405
|
+
float: "a number",
|
|
406
|
+
complex: "a complex number",
|
|
407
|
+
str: "a string",
|
|
408
|
+
bool: "`True` or `False`",
|
|
409
|
+
bytes: "bytes",
|
|
410
|
+
list: "a list",
|
|
411
|
+
dict: "a dictionary",
|
|
412
|
+
set: "a set",
|
|
413
|
+
tuple: "a tuple",
|
|
414
|
+
None: "`None`",
|
|
415
|
+
NoneType: "`None`"
|
|
416
|
+
};
|
|
417
|
+
function cleanTypeName(type) {
|
|
418
|
+
return type.startsWith("__main__.") ? type.slice("__main__.".length) : type;
|
|
419
|
+
}
|
|
420
|
+
function describeType(type) {
|
|
421
|
+
const name = cleanTypeName(type);
|
|
422
|
+
if (name === "None" || name === "NoneType") return "`None`";
|
|
423
|
+
const friendly = FRIENDLY_TYPES[name];
|
|
424
|
+
return friendly ? `${friendly} (\`${name}\`)` : `\`${name}\``;
|
|
425
|
+
}
|
|
426
|
+
function describeTypes(types) {
|
|
427
|
+
const described = types.map(describeType);
|
|
428
|
+
if (described.length === 0) return "a different type";
|
|
429
|
+
if (described.length === 1) return described[0];
|
|
430
|
+
return `${described.slice(0, -1).join(", ")} or ${described[described.length - 1]}`;
|
|
431
|
+
}
|
|
432
|
+
function boolAsNumberNote(expected, actual, level) {
|
|
433
|
+
if (actual !== "bool" || expected.length === 0) return null;
|
|
434
|
+
if (!expected.some((type) => type === "int" || type === "float")) return null;
|
|
435
|
+
if (level !== void 0 && !levelRejectsBoolAsNumber(level)) return null;
|
|
436
|
+
return "At `#level beginner` and `#level intermediate`, `True` and `False` are not accepted as numbers, even though Python counts them as `1` and `0`.";
|
|
437
|
+
}
|
|
438
|
+
function conversionFor(expected, actual) {
|
|
439
|
+
if (expected.length !== 1 || actual === null) return null;
|
|
440
|
+
const to = expected[0];
|
|
441
|
+
const numeric = actual === "int" || actual === "float" || actual === "bool";
|
|
442
|
+
if (to === "int" && (actual === "str" || actual === "float")) return "int";
|
|
443
|
+
if (to === "float" && actual === "str") return "float";
|
|
444
|
+
if (to === "str" && numeric) return "str";
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
var ELEMENT_RE = /^(item \d+|key .+|value of key .+|\[.+\]) of (.+)$/;
|
|
448
|
+
function membersPhrase(element) {
|
|
449
|
+
if (element.startsWith("the value for key")) return "every value in";
|
|
450
|
+
if (element.startsWith("key ")) return "every key in";
|
|
451
|
+
return "every item in";
|
|
452
|
+
}
|
|
453
|
+
function describeElement(element) {
|
|
454
|
+
if (element.startsWith("value of key ")) {
|
|
455
|
+
return `the value for key ${element.slice("value of key ".length)}`;
|
|
312
456
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
457
|
+
if (element.startsWith("[")) return "one of the values";
|
|
458
|
+
return element;
|
|
459
|
+
}
|
|
460
|
+
function parseTypeCheckMessage(message) {
|
|
461
|
+
const unknown = {
|
|
462
|
+
kind: "unknown",
|
|
463
|
+
name: null,
|
|
464
|
+
element: null,
|
|
465
|
+
actual: null,
|
|
466
|
+
expected: []
|
|
467
|
+
};
|
|
468
|
+
const lines = message.split(/\r?\n/);
|
|
469
|
+
const first = lines[0].trim();
|
|
470
|
+
let expected = [];
|
|
471
|
+
let subject = null;
|
|
472
|
+
const unionAt = first.indexOf(" did not match any element in the union:");
|
|
473
|
+
if (unionAt >= 0) {
|
|
474
|
+
subject = first.slice(0, unionAt);
|
|
475
|
+
for (const raw of lines.slice(1)) {
|
|
476
|
+
const m = raw.match(/^\s*([A-Za-z_][A-Za-z0-9_.\[\], ]*):/);
|
|
477
|
+
if (m) expected.push(m[1].trim());
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
const predicates = [
|
|
481
|
+
[/^(.*) is not an instance of (.+)$/, (m) => [m[2]]],
|
|
482
|
+
[/^(.*) is neither float or int$/, () => ["float"]],
|
|
483
|
+
[/^(.*) is not None$/, () => ["None"]],
|
|
484
|
+
[/^(.*) is not a ([A-Za-z_][A-Za-z0-9_]*)$/, (m) => [m[2]]]
|
|
485
|
+
];
|
|
486
|
+
for (const [re, pick] of predicates) {
|
|
487
|
+
const m = first.match(re);
|
|
488
|
+
if (m) {
|
|
489
|
+
subject = m[1];
|
|
490
|
+
expected = pick(m);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
320
494
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
495
|
+
if (subject === null) return unknown;
|
|
496
|
+
let actual = null;
|
|
497
|
+
const withActual = subject.match(/^(.*) \(([^()]*)\)$/);
|
|
498
|
+
if (withActual) {
|
|
499
|
+
subject = withActual[1];
|
|
500
|
+
actual = withActual[2];
|
|
324
501
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return true;
|
|
502
|
+
let element = null;
|
|
503
|
+
const elementMatch = subject.match(ELEMENT_RE);
|
|
504
|
+
if (elementMatch) {
|
|
505
|
+
element = describeElement(elementMatch[1]);
|
|
506
|
+
subject = elementMatch[2];
|
|
331
507
|
}
|
|
332
|
-
|
|
333
|
-
|
|
508
|
+
if (subject === "the return value") {
|
|
509
|
+
return { kind: "return", name: null, element, actual, expected };
|
|
334
510
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
this.initPromise = null;
|
|
339
|
-
this.stdinBuffer = null;
|
|
340
|
-
this.interruptBuffer = null;
|
|
341
|
-
this.live = null;
|
|
342
|
-
this.rejectAllPending(new Error("Runtime disposed"));
|
|
511
|
+
const argument = subject.match(/^argument "(.+)"$/);
|
|
512
|
+
if (argument) {
|
|
513
|
+
return { kind: "argument", name: argument[1], element, actual, expected };
|
|
343
514
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (!worker) {
|
|
348
|
-
throw new Error("Worker not initialized");
|
|
349
|
-
}
|
|
350
|
-
const id = this.nextId++;
|
|
351
|
-
const reply = await new Promise((resolve3, reject) => {
|
|
352
|
-
this.pending.set(id, { resolve: resolve3, reject });
|
|
353
|
-
worker.post({ id, ...msg });
|
|
354
|
-
});
|
|
355
|
-
if (reply.type !== expected) {
|
|
356
|
-
throw new Error(`Pyodide worker replied "${reply.type}", expected "${expected}"`);
|
|
357
|
-
}
|
|
358
|
-
return reply;
|
|
515
|
+
const assigned = subject.match(/^value assigned to (.+)$/);
|
|
516
|
+
if (assigned) {
|
|
517
|
+
return { kind: "variable", name: assigned[1], element, actual, expected };
|
|
359
518
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
519
|
+
return { ...unknown, element, actual, expected };
|
|
520
|
+
}
|
|
521
|
+
function explainTypeCheckError(message, functionName, level) {
|
|
522
|
+
const parsed = parseTypeCheckMessage(message);
|
|
523
|
+
const wanted = describeTypes(parsed.expected);
|
|
524
|
+
const owner = functionName ? `\`${functionName}\`` : "this function";
|
|
525
|
+
const named = parsed.name ? `\`${parsed.name}\`` : "a value";
|
|
526
|
+
const convert = conversionFor(parsed.expected, parsed.actual);
|
|
527
|
+
const boolNote = boolAsNumberNote(parsed.expected, parsed.actual, level);
|
|
528
|
+
const annotation = parsed.expected.length === 1 ? cleanTypeName(parsed.expected[0]) : null;
|
|
529
|
+
if (parsed.kind === "argument") {
|
|
530
|
+
if (parsed.element) {
|
|
531
|
+
return {
|
|
532
|
+
headline: `${owner} expects ${membersPhrase(parsed.element)} ${named} to be ${wanted}, but ${parsed.element} is not.`,
|
|
533
|
+
howToFix: [
|
|
534
|
+
`Look at ${parsed.element} of the ${parsed.actual ?? "value"} you passed for ${named}.`,
|
|
535
|
+
"Every one has to match the annotation, not just the first."
|
|
536
|
+
]
|
|
537
|
+
};
|
|
374
538
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
539
|
+
const got = parsed.actual ? `, but got ${describeType(parsed.actual)}` : "";
|
|
540
|
+
const howToFix = [`Check the value you passed for ${named} on this line.`];
|
|
541
|
+
if (boolNote) howToFix.push(boolNote);
|
|
542
|
+
if (convert) {
|
|
543
|
+
howToFix.push(`If it should be ${wanted}, convert it with \`${convert}(...)\`.`);
|
|
380
544
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
545
|
+
if (annotation && parsed.actual) {
|
|
546
|
+
howToFix.push(
|
|
547
|
+
`Or, if ${named} really should be ${describeType(parsed.actual)}, change its annotation in ${owner} from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
548
|
+
);
|
|
385
549
|
}
|
|
386
|
-
|
|
550
|
+
return {
|
|
551
|
+
headline: `${owner} expects ${named} to be ${wanted}${got}.`,
|
|
552
|
+
howToFix
|
|
553
|
+
};
|
|
387
554
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
555
|
+
if (parsed.kind === "return") {
|
|
556
|
+
if (parsed.actual === "None" || parsed.actual === "NoneType") {
|
|
557
|
+
return {
|
|
558
|
+
headline: `${owner} should return ${wanted}, but it finished without returning a value.`,
|
|
559
|
+
howToFix: [
|
|
560
|
+
"Make sure every path through the function reaches a `return`.",
|
|
561
|
+
"An `if` with no `else` falls off the end, and Python then returns `None`.",
|
|
562
|
+
`Or annotate the return type as \`None\` if ${owner} is not meant to return anything.`
|
|
563
|
+
]
|
|
564
|
+
};
|
|
394
565
|
}
|
|
395
|
-
|
|
396
|
-
|
|
566
|
+
const got = parsed.actual ? describeType(parsed.actual) : "something else";
|
|
567
|
+
const howToFix = [];
|
|
568
|
+
const wantsNone = parsed.expected.length === 1 && /^(None|NoneType)$/.test(parsed.expected[0]);
|
|
569
|
+
if (wantsNone) {
|
|
570
|
+
howToFix.push("Use a bare `return`, or drop the `return` entirely.");
|
|
571
|
+
} else if (convert) {
|
|
572
|
+
howToFix.push(`Convert the returned value with \`${convert}(...)\`.`);
|
|
573
|
+
} else {
|
|
574
|
+
howToFix.push(`Return ${wanted} from this line.`);
|
|
397
575
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
constructor(paths) {
|
|
404
|
-
super();
|
|
405
|
-
this.paths = paths;
|
|
406
|
-
}
|
|
407
|
-
paths;
|
|
408
|
-
resolveIndexUrl() {
|
|
409
|
-
const found = this.paths.indexUrlCandidates.find(
|
|
410
|
-
(dir) => fs.existsSync(path.join(dir, "pyodide.asm.wasm"))
|
|
411
|
-
);
|
|
412
|
-
if (!found) {
|
|
413
|
-
throw new Error(this.paths.missingAssetsHint);
|
|
576
|
+
if (boolNote) howToFix.push(boolNote);
|
|
577
|
+
if (annotation && parsed.actual) {
|
|
578
|
+
howToFix.push(
|
|
579
|
+
`Or change ${owner}'s return annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
580
|
+
);
|
|
414
581
|
}
|
|
415
|
-
return found;
|
|
416
|
-
}
|
|
417
|
-
spawn(handlers) {
|
|
418
|
-
const worker = new import_node_worker_threads.Worker(this.paths.workerPath);
|
|
419
|
-
worker.on("message", (msg) => handlers.onMessage(msg));
|
|
420
|
-
worker.on(
|
|
421
|
-
"error",
|
|
422
|
-
(err) => handlers.onError(err instanceof Error ? err : new Error(String(err)))
|
|
423
|
-
);
|
|
424
582
|
return {
|
|
425
|
-
|
|
426
|
-
|
|
583
|
+
headline: `${owner} says it returns ${wanted}, but this line returns ${got}.`,
|
|
584
|
+
howToFix
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (parsed.kind === "variable") {
|
|
588
|
+
if (parsed.element) {
|
|
589
|
+
return {
|
|
590
|
+
headline: `${named} is annotated so ${membersPhrase(parsed.element)} it is ${wanted}, but ${parsed.element} is not.`,
|
|
591
|
+
howToFix: [`Look at ${parsed.element} of the value assigned to ${named}.`]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const got = parsed.actual ? describeType(parsed.actual) : "a different type";
|
|
595
|
+
const howToFix = [`Assign ${wanted} to ${named}.`];
|
|
596
|
+
if (boolNote) howToFix.push(boolNote);
|
|
597
|
+
if (convert) {
|
|
598
|
+
howToFix.push(`You can convert the value with \`${convert}(...)\`.`);
|
|
599
|
+
}
|
|
600
|
+
if (annotation && parsed.actual) {
|
|
601
|
+
howToFix.push(
|
|
602
|
+
`Or change ${named}'s annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
headline: `${named} is annotated as ${wanted}, but ${got} was assigned here.`,
|
|
607
|
+
howToFix
|
|
427
608
|
};
|
|
428
609
|
}
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
// src/cli/runtime.ts
|
|
432
|
-
function createCliRuntime() {
|
|
433
|
-
return new DesktopPyodideRuntime({
|
|
434
|
-
// `pyodide` is external in the bundle, so this resolves at run time
|
|
435
|
-
// from the installed package rather than being inlined.
|
|
436
|
-
indexUrlCandidates: [path2.dirname(require.resolve("pyodide"))],
|
|
437
|
-
workerPath: path2.join(__dirname, "worker.cjs"),
|
|
438
|
-
missingAssetsHint: "Could not find Pyodide's assets. Reinstall pll-python so its `pyodide` dependency is present."
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/cli/run.ts
|
|
443
|
-
var fs3 = __toESM(require("node:fs/promises"));
|
|
444
|
-
var path4 = __toESM(require("node:path"));
|
|
445
|
-
|
|
446
|
-
// src/common/analyzers/static/shadowingExplainer.ts
|
|
447
|
-
function outerScopeNoun(kind) {
|
|
448
|
-
switch (kind) {
|
|
449
|
-
case "module":
|
|
450
|
-
return "the file";
|
|
451
|
-
case "function":
|
|
452
|
-
case "lambda":
|
|
453
|
-
return "an enclosing function";
|
|
454
|
-
case "class":
|
|
455
|
-
return "an enclosing class";
|
|
456
|
-
case "comprehension":
|
|
457
|
-
return "an enclosing comprehension";
|
|
458
|
-
default:
|
|
459
|
-
return "an outer scope";
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
function explainShadowing(raw, level, fileName) {
|
|
463
|
-
const name = raw.name_token ?? "this name";
|
|
464
|
-
const outerLine = raw.outer_line_number ?? null;
|
|
465
|
-
const outerNoun = outerScopeNoun(raw.outer_scope_kind);
|
|
466
|
-
const knownOuterLine = outerLine !== null && outerLine > 0;
|
|
467
|
-
const outerHint = knownOuterLine ? ` (first defined on line ${outerLine}, in ${outerNoun})` : ` in ${outerNoun}`;
|
|
468
|
-
return {
|
|
469
|
-
id: "shadowing",
|
|
470
|
-
errorType: "Shadowing",
|
|
471
|
-
message: raw.message,
|
|
472
|
-
headline: `\`${name}\` is already defined${outerHint}.`,
|
|
473
|
-
howToFix: [
|
|
474
|
-
`Rename the inner \`${name}\` to something distinct (e.g. \`${name}_inner\`, \`local_${name}\`, or whatever describes its role).`,
|
|
475
|
-
`Or, if you want to use the outer \`${name}\`, just read it directly without making a new variable.`
|
|
476
|
-
],
|
|
477
|
-
fileName,
|
|
478
|
-
lineNumber: raw.line_number,
|
|
479
|
-
column: raw.column,
|
|
480
|
-
nameToken: raw.name_token,
|
|
481
|
-
severity: "error",
|
|
482
|
-
raw: JSON.stringify(raw),
|
|
483
|
-
origin: "static",
|
|
484
|
-
level
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
function explainShadowingBuiltin(raw, level, fileName) {
|
|
488
|
-
const name = raw.name_token ?? "this name";
|
|
489
|
-
const headline = `\`${name}\` is the name of a Python built-in.`;
|
|
490
610
|
return {
|
|
491
|
-
|
|
492
|
-
errorType: "Shadowing",
|
|
493
|
-
message: raw.message,
|
|
494
|
-
headline,
|
|
611
|
+
headline: "A value does not match the type annotation written for it.",
|
|
495
612
|
howToFix: [
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
lineNumber: raw.line_number,
|
|
500
|
-
column: raw.column,
|
|
501
|
-
nameToken: raw.name_token,
|
|
502
|
-
severity: "error",
|
|
503
|
-
raw: JSON.stringify(raw),
|
|
504
|
-
origin: "static",
|
|
505
|
-
level
|
|
613
|
+
message.split(/\r?\n/)[0].trim(),
|
|
614
|
+
"Either pass a value of the annotated type, or change the annotation."
|
|
615
|
+
]
|
|
506
616
|
};
|
|
507
617
|
}
|
|
508
618
|
|
|
509
|
-
// src/common/analyzers/
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
fileName,
|
|
527
|
-
lineNumber: raw.line_number,
|
|
528
|
-
column: raw.column,
|
|
529
|
-
nameToken: raw.name_token,
|
|
530
|
-
severity: "error",
|
|
531
|
-
raw: JSON.stringify(raw),
|
|
532
|
-
origin: "static",
|
|
533
|
-
level
|
|
534
|
-
};
|
|
619
|
+
// src/common/analyzers/typeCheckAnalyzer.ts
|
|
620
|
+
var HANDLED = ["TypeCheckError", "typeguard.TypeCheckError"];
|
|
621
|
+
function messageWithDetail(traceback, fallback) {
|
|
622
|
+
const lines = traceback.split(/\r?\n/);
|
|
623
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
624
|
+
const line = lines[i];
|
|
625
|
+
const marker = line.indexOf("TypeCheckError:");
|
|
626
|
+
if (line.startsWith(" ") || marker < 0) continue;
|
|
627
|
+
const detail = [line.slice(marker + "TypeCheckError:".length).trim()];
|
|
628
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
629
|
+
if (lines[j].trim() === "") continue;
|
|
630
|
+
if (!lines[j].startsWith(" ")) break;
|
|
631
|
+
detail.push(lines[j]);
|
|
632
|
+
}
|
|
633
|
+
return detail.join("\n");
|
|
634
|
+
}
|
|
635
|
+
return fallback;
|
|
535
636
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
`Return${namesPhrase || " the new value"} from this inner function and have the outer function rebind it explicitly (e.g. \`x = inner(x)\`).`,
|
|
553
|
-
`If the inner function only needs to *read* the outer name, just use it directly - you don't need \`nonlocal\` for that.`
|
|
637
|
+
var typeCheckAnalyzer = {
|
|
638
|
+
handles: HANDLED,
|
|
639
|
+
analyze(input) {
|
|
640
|
+
const { parsedError, fileName, level } = input;
|
|
641
|
+
if (!HANDLED.includes(parsedError.errorType)) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const message = messageWithDetail(parsedError.traceback, parsedError.message);
|
|
645
|
+
const parsed = parseTypeCheckMessage(message);
|
|
646
|
+
const frames = userTracebackFrames(parsedError.traceback);
|
|
647
|
+
const innermost = frames.length > 0 ? frames[frames.length - 1] : null;
|
|
648
|
+
const blamed = parsed.kind === "argument" && frames.length >= 2 ? frames[frames.length - 2] : innermost;
|
|
649
|
+
const explanation = explainTypeCheckError(
|
|
650
|
+
message,
|
|
651
|
+
innermost ? innermost.functionName : null,
|
|
652
|
+
level
|
|
554
653
|
);
|
|
654
|
+
return {
|
|
655
|
+
id: "type-check",
|
|
656
|
+
// Not a real Python exception name; "TypeCheckError" means nothing to
|
|
657
|
+
// a student, and "TypeError" would name a different Python error.
|
|
658
|
+
errorType: "TypeMismatch",
|
|
659
|
+
message,
|
|
660
|
+
headline: explanation.headline,
|
|
661
|
+
howToFix: explanation.howToFix,
|
|
662
|
+
fileName: blamed ? blamed.fileName : fileName,
|
|
663
|
+
lineNumber: blamed ? blamed.line : parsedError.lineNumber,
|
|
664
|
+
column: null,
|
|
665
|
+
nameToken: parsed.name,
|
|
666
|
+
severity: "error",
|
|
667
|
+
raw: parsedError.traceback,
|
|
668
|
+
origin: "runtime",
|
|
669
|
+
level
|
|
670
|
+
};
|
|
555
671
|
}
|
|
556
|
-
return {
|
|
557
|
-
id: "disallowed-keyword",
|
|
558
|
-
errorType: "DisallowedKeyword",
|
|
559
|
-
message: raw.message,
|
|
560
|
-
headline,
|
|
561
|
-
howToFix,
|
|
562
|
-
fileName,
|
|
563
|
-
lineNumber: raw.line_number,
|
|
564
|
-
column: raw.column,
|
|
565
|
-
nameToken: raw.name_token,
|
|
566
|
-
severity: "error",
|
|
567
|
-
raw: JSON.stringify(raw),
|
|
568
|
-
origin: "static",
|
|
569
|
-
level
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// src/common/analyzers/static/registry.ts
|
|
574
|
-
var explainers = {
|
|
575
|
-
shadowing: explainShadowing,
|
|
576
|
-
"shadowing-builtin": explainShadowingBuiltin,
|
|
577
|
-
reassignment: explainReassignment,
|
|
578
|
-
"disallowed-keyword": explainDisallowedKeyword
|
|
579
672
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
level
|
|
595
|
-
};
|
|
673
|
+
|
|
674
|
+
// src/common/analyzers/registry.ts
|
|
675
|
+
var analyzers = [nameErrorAnalyzer, typeCheckAnalyzer];
|
|
676
|
+
function findRuntimeFinding(source, fileName, level, parsedError) {
|
|
677
|
+
for (const analyzer of analyzers) {
|
|
678
|
+
if (!analyzer.handles.includes(parsedError.errorType)) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const finding = analyzer.analyze({ source, fileName, level, parsedError });
|
|
682
|
+
if (finding) {
|
|
683
|
+
return finding;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
596
687
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
688
|
+
|
|
689
|
+
// src/common/analyzers/runtimeFinding.ts
|
|
690
|
+
function findingForErrorEvent(event, source, fileName, level) {
|
|
691
|
+
const traceback = event.traceback || `${event.errorType}: ${event.message}`;
|
|
692
|
+
const parsed = parsePythonError(traceback);
|
|
693
|
+
if (parsed.lineNumber === null && event.lineNumber !== null) {
|
|
694
|
+
parsed.lineNumber = event.lineNumber;
|
|
695
|
+
}
|
|
696
|
+
if (parsed.column === null && event.column !== null) {
|
|
697
|
+
parsed.column = event.column;
|
|
698
|
+
}
|
|
699
|
+
if (parsed.fileName === null && event.fileName) {
|
|
700
|
+
parsed.fileName = event.fileName;
|
|
701
|
+
}
|
|
702
|
+
return findRuntimeFinding(source, fileName, level, parsed);
|
|
602
703
|
}
|
|
603
704
|
|
|
604
|
-
// src/common/
|
|
605
|
-
var
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
705
|
+
// src/common/pyodideRunner.ts
|
|
706
|
+
var ANY_IMPORT_RE = /(^|\n)[ \t]*(import|from)[ \t]+\S/;
|
|
707
|
+
|
|
708
|
+
// src/cli/files.ts
|
|
709
|
+
var fs = __toESM(require("node:fs/promises"));
|
|
710
|
+
var path = __toESM(require("node:path"));
|
|
711
|
+
|
|
712
|
+
// src/common/workspaceFilePolicy.ts
|
|
713
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
714
|
+
var MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
715
|
+
var MAX_FILES = 50;
|
|
716
|
+
var MOUNT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
717
|
+
".csv",
|
|
718
|
+
".txt",
|
|
719
|
+
".tsv",
|
|
720
|
+
".json",
|
|
721
|
+
".md",
|
|
722
|
+
".dat",
|
|
723
|
+
".xml",
|
|
724
|
+
".py"
|
|
725
|
+
]);
|
|
726
|
+
var WRITEBACK_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
727
|
+
".csv",
|
|
728
|
+
".txt",
|
|
729
|
+
".tsv",
|
|
730
|
+
".json",
|
|
731
|
+
".md",
|
|
732
|
+
".dat",
|
|
733
|
+
".xml"
|
|
734
|
+
]);
|
|
735
|
+
function extensionOf(name) {
|
|
736
|
+
const dot = name.lastIndexOf(".");
|
|
737
|
+
if (dot <= 0) {
|
|
738
|
+
return "";
|
|
739
|
+
}
|
|
740
|
+
return name.slice(dot).toLowerCase();
|
|
741
|
+
}
|
|
742
|
+
function isSafeBasename(name) {
|
|
743
|
+
if (!name || name === "." || name === "..") {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
if (name.startsWith(".")) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
|
750
|
+
return false;
|
|
625
751
|
}
|
|
626
|
-
return
|
|
752
|
+
return true;
|
|
627
753
|
}
|
|
628
|
-
function
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
break;
|
|
754
|
+
function isMountableName(name) {
|
|
755
|
+
return isSafeBasename(name) && MOUNT_EXTENSIONS.has(extensionOf(name));
|
|
756
|
+
}
|
|
757
|
+
function isWritebackName(name) {
|
|
758
|
+
return isSafeBasename(name) && WRITEBACK_EXTENSIONS.has(extensionOf(name));
|
|
759
|
+
}
|
|
760
|
+
function utf8ByteLength(text) {
|
|
761
|
+
return new TextEncoder().encode(text).byteLength;
|
|
762
|
+
}
|
|
763
|
+
function decodeMountContents(data) {
|
|
764
|
+
if (data.byteLength > MAX_FILE_BYTES) {
|
|
765
|
+
return void 0;
|
|
766
|
+
}
|
|
767
|
+
for (let i = 0; i < data.byteLength; i++) {
|
|
768
|
+
if (data[i] === 0) {
|
|
769
|
+
return void 0;
|
|
645
770
|
}
|
|
646
771
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
lineNumber = parseInt(m[2], 10);
|
|
772
|
+
return new TextDecoder("utf-8").decode(data);
|
|
773
|
+
}
|
|
774
|
+
function selectMountableFiles(candidates) {
|
|
775
|
+
const selected = [];
|
|
776
|
+
let total = 0;
|
|
777
|
+
for (const candidate of candidates) {
|
|
778
|
+
if (selected.length >= MAX_FILES) {
|
|
655
779
|
break;
|
|
656
780
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
781
|
+
if (!isMountableName(candidate.name)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
let text;
|
|
785
|
+
if (typeof candidate.contents === "string") {
|
|
786
|
+
if (utf8ByteLength(candidate.contents) > MAX_FILE_BYTES) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (candidate.contents.includes("\0")) {
|
|
790
|
+
continue;
|
|
667
791
|
}
|
|
792
|
+
text = candidate.contents;
|
|
793
|
+
} else {
|
|
794
|
+
const decoded = decodeMountContents(candidate.contents);
|
|
795
|
+
if (decoded === void 0) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
text = decoded;
|
|
799
|
+
}
|
|
800
|
+
const size = utf8ByteLength(text);
|
|
801
|
+
if (total + size > MAX_TOTAL_BYTES) {
|
|
802
|
+
continue;
|
|
668
803
|
}
|
|
804
|
+
selected.push({ name: candidate.name, contents: text });
|
|
805
|
+
total += size;
|
|
669
806
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
807
|
+
return selected;
|
|
808
|
+
}
|
|
809
|
+
function selectWritebackFiles(files) {
|
|
810
|
+
const selected = [];
|
|
811
|
+
let total = 0;
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
if (selected.length >= MAX_FILES) {
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
if (!isWritebackName(file.name)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const size = utf8ByteLength(file.contents);
|
|
820
|
+
if (size > MAX_FILE_BYTES || total + size > MAX_TOTAL_BYTES) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (file.contents.includes("\0")) {
|
|
824
|
+
continue;
|
|
675
825
|
}
|
|
826
|
+
selected.push(file);
|
|
827
|
+
total += size;
|
|
676
828
|
}
|
|
677
|
-
return
|
|
678
|
-
errorType,
|
|
679
|
-
message,
|
|
680
|
-
traceback,
|
|
681
|
-
fileName,
|
|
682
|
-
lineNumber,
|
|
683
|
-
column,
|
|
684
|
-
nameToken
|
|
685
|
-
};
|
|
829
|
+
return selected;
|
|
686
830
|
}
|
|
687
831
|
|
|
688
|
-
// src/
|
|
689
|
-
function
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
`If \`${name}\` should be text, put it in quotes: \`"${name}"\`.`
|
|
697
|
-
];
|
|
698
|
-
if (didYouMean.length > 0) {
|
|
699
|
-
howToFix.unshift(
|
|
700
|
-
`Python suggested: ${didYouMean.map((s) => "`" + s + "`").join(", ")} - try one of those.`
|
|
701
|
-
);
|
|
832
|
+
// src/cli/files.ts
|
|
833
|
+
async function collectSiblingFiles(scriptPath) {
|
|
834
|
+
const folder = path.dirname(path.resolve(scriptPath));
|
|
835
|
+
let entries;
|
|
836
|
+
try {
|
|
837
|
+
entries = await fs.readdir(folder, { withFileTypes: true });
|
|
838
|
+
} catch {
|
|
839
|
+
return [];
|
|
702
840
|
}
|
|
703
|
-
|
|
841
|
+
const candidates = [];
|
|
842
|
+
for (const entry of entries) {
|
|
843
|
+
if (!entry.isFile() || !isMountableName(entry.name)) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
candidates.push({ name: entry.name, contents: await fs.readFile(path.join(folder, entry.name)) });
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return selectMountableFiles(candidates);
|
|
704
852
|
}
|
|
705
|
-
function
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
853
|
+
async function writeBackSiblingFiles(scriptPath, files) {
|
|
854
|
+
const folder = path.dirname(path.resolve(scriptPath));
|
|
855
|
+
const written = [];
|
|
856
|
+
for (const file of selectWritebackFiles(files)) {
|
|
857
|
+
try {
|
|
858
|
+
await fs.writeFile(path.join(folder, file.name), file.contents, "utf8");
|
|
859
|
+
written.push(file.name);
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
709
862
|
}
|
|
710
|
-
return
|
|
863
|
+
return written;
|
|
711
864
|
}
|
|
712
865
|
|
|
713
|
-
// src/
|
|
714
|
-
var
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
866
|
+
// src/cli/run.ts
|
|
867
|
+
var EXIT = {
|
|
868
|
+
ok: 0,
|
|
869
|
+
/** The program raised. */
|
|
870
|
+
programError: 1,
|
|
871
|
+
/** Level checks found problems, so the file was not run. */
|
|
872
|
+
blocked: 2,
|
|
873
|
+
/** The program ran, but its in-file tests did not all pass. */
|
|
874
|
+
testsFailed: 3,
|
|
875
|
+
/** Bad usage, or PLL itself could not run. */
|
|
876
|
+
usage: 64
|
|
877
|
+
};
|
|
878
|
+
async function runFile(runtime, view, opts) {
|
|
879
|
+
const fileName = path2.basename(opts.file);
|
|
880
|
+
let source;
|
|
881
|
+
try {
|
|
882
|
+
source = await fs2.readFile(opts.file, "utf8");
|
|
883
|
+
} catch (err) {
|
|
884
|
+
view.problem(`Cannot read ${opts.file}: ${errText(err)}`);
|
|
885
|
+
return EXIT.usage;
|
|
886
|
+
}
|
|
887
|
+
const level = parseLevel(source);
|
|
888
|
+
view.note(`${fileName} [${level}]`);
|
|
889
|
+
if (levelHasStaticChecks(level)) {
|
|
890
|
+
const blocked = await staticChecks(runtime, view, source, fileName, level);
|
|
891
|
+
if (blocked) {
|
|
892
|
+
return EXIT.blocked;
|
|
720
893
|
}
|
|
721
|
-
const explanation = explainNameError(parsedError);
|
|
722
|
-
return {
|
|
723
|
-
id: "name-error",
|
|
724
|
-
errorType: "NameError",
|
|
725
|
-
message: parsedError.message,
|
|
726
|
-
headline: explanation.headline,
|
|
727
|
-
howToFix: explanation.howToFix,
|
|
728
|
-
fileName,
|
|
729
|
-
lineNumber: parsedError.lineNumber,
|
|
730
|
-
column: parsedError.column,
|
|
731
|
-
nameToken: parsedError.nameToken,
|
|
732
|
-
severity: "error",
|
|
733
|
-
raw: parsedError.traceback,
|
|
734
|
-
origin: "runtime",
|
|
735
|
-
level
|
|
736
|
-
};
|
|
737
894
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
895
|
+
await ensurePackages(runtime, view, source);
|
|
896
|
+
try {
|
|
897
|
+
await runtime.mountWorkspaceFiles(await collectSiblingFiles(opts.file));
|
|
898
|
+
} catch (err) {
|
|
899
|
+
view.note(`Could not load files next to this script (${errText(err)}).`);
|
|
900
|
+
}
|
|
901
|
+
const onEvent = (event) => {
|
|
902
|
+
if (event.kind === "error") {
|
|
903
|
+
const finding = findingForErrorEvent(event, source, fileName, level);
|
|
904
|
+
if (finding) {
|
|
905
|
+
view.runtimeFinding(finding);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
view.handle(event);
|
|
910
|
+
};
|
|
911
|
+
if (opts.runTests) {
|
|
912
|
+
await maybeRunTests(runtime, view, source, fileName, level, onEvent);
|
|
913
|
+
}
|
|
914
|
+
await runtime.runFile({ code: source, fileName, sessionKey: opts.file, level }, onEvent);
|
|
915
|
+
await writeBack(runtime, view, opts.file);
|
|
916
|
+
if (view.sawError) return EXIT.programError;
|
|
917
|
+
if (view.testFailures > 0) return EXIT.testsFailed;
|
|
918
|
+
return EXIT.ok;
|
|
919
|
+
}
|
|
920
|
+
async function staticChecks(runtime, view, source, fileName, level) {
|
|
921
|
+
let raw;
|
|
922
|
+
try {
|
|
923
|
+
raw = await runtime.staticAnalyze({ code: source, fileName, level });
|
|
924
|
+
} catch (err) {
|
|
925
|
+
view.note(`Static analysis failed (${errText(err)}); running anyway.`);
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
const findings = enrichStaticFindings(raw, level, fileName);
|
|
929
|
+
if (findings.length === 0) {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
view.findings(findings);
|
|
933
|
+
view.problem(`Static analysis found ${findings.length} problem(s). File not run.`);
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
async function ensurePackages(runtime, view, source) {
|
|
937
|
+
if (!ANY_IMPORT_RE.test(source)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
await runtime.ensurePackages(source);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
const message = errText(err);
|
|
944
|
+
if (!/syntaxerror|invalid syntax/i.test(message)) {
|
|
945
|
+
view.note(`Could not load libraries (${message}). Continuing.`);
|
|
759
946
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function maybeRunTests(runtime, view, source, fileName, level, onEvent) {
|
|
950
|
+
try {
|
|
951
|
+
if (!await runtime.hasTests(source)) {
|
|
952
|
+
return;
|
|
763
953
|
}
|
|
764
|
-
|
|
954
|
+
await runtime.ensurePytest();
|
|
955
|
+
} catch (err) {
|
|
956
|
+
view.note(`Could not check for tests (${errText(err)}); skipping them.`);
|
|
957
|
+
return;
|
|
765
958
|
}
|
|
766
|
-
|
|
959
|
+
await runtime.runTests({ code: source, fileName, sessionKey: fileName, level }, onEvent);
|
|
767
960
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
961
|
+
async function writeBack(runtime, view, file) {
|
|
962
|
+
try {
|
|
963
|
+
const changed = await runtime.collectWorkspaceFiles();
|
|
964
|
+
if (changed.length === 0) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const written = await writeBackSiblingFiles(file, changed);
|
|
968
|
+
if (written.length > 0) {
|
|
969
|
+
view.note(`Saved ${written.join(", ")} next to ${path2.basename(file)}.`);
|
|
970
|
+
}
|
|
971
|
+
} catch (err) {
|
|
972
|
+
view.note(`Could not save files next to this script (${errText(err)}).`);
|
|
973
|
+
}
|
|
770
974
|
}
|
|
771
|
-
function
|
|
772
|
-
return
|
|
975
|
+
function errText(err) {
|
|
976
|
+
return err instanceof Error ? err.message : String(err);
|
|
773
977
|
}
|
|
774
978
|
|
|
775
|
-
// src/
|
|
776
|
-
var
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
979
|
+
// src/cli/examplar.ts
|
|
980
|
+
var EXAMPLAR_USAGE = `pll examplar - author Examplar bundles
|
|
981
|
+
|
|
982
|
+
pll examplar build <dir> [-o bundle.json] [--verify tests.py]
|
|
983
|
+
|
|
984
|
+
<dir> holds the implementations, one file each:
|
|
985
|
+
|
|
986
|
+
<dir>/wheats/reference.py known-good; every test must pass on these
|
|
987
|
+
<dir>/wheats/alternative.py more than one stops tests over-fitting
|
|
988
|
+
<dir>/chaffs/initials/1.py known-bad; each must be caught by some test
|
|
989
|
+
<dir>/chaffs/longest/1.py
|
|
990
|
+
|
|
991
|
+
A chaff goes in a directory named after the function it breaks, because a
|
|
992
|
+
student's report is per function. Its id is its filename, and that id is the
|
|
993
|
+
only thing a student sees about a chaff they missed - so number them rather
|
|
994
|
+
than naming them after the bug. Every function needs at least one.
|
|
995
|
+
|
|
996
|
+
Only bytecode is written to the bundle, so the sources stay in your
|
|
997
|
+
repository.
|
|
998
|
+
|
|
999
|
+
The bytecode is compiled by the Pyodide this package pins, so it always
|
|
1000
|
+
matches the interpreter that will run it - which is the reason to build
|
|
1001
|
+
bundles with this tool rather than a local python.
|
|
1002
|
+
|
|
1003
|
+
-o FILE write here instead of stdout
|
|
1004
|
+
--verify TESTS check the bundle with your own suite: it must pass on
|
|
1005
|
+
every wheat and fail on every chaff. A chaff no test can
|
|
1006
|
+
catch is a broken chaff, and better found now. Chaffs are
|
|
1007
|
+
only checked once the wheats pass, since a suite that is
|
|
1008
|
+
wrong fails on everything.
|
|
1009
|
+
`;
|
|
1010
|
+
function parseExamplarArgs(argv) {
|
|
1011
|
+
const args = { help: false };
|
|
1012
|
+
if (argv[0] !== "build") {
|
|
1013
|
+
args.error = argv.length === 0 ? "examplar needs a subcommand" : `unknown examplar subcommand ${argv[0]}`;
|
|
1014
|
+
if (argv[0] === "-h" || argv[0] === "--help") {
|
|
1015
|
+
args.error = void 0;
|
|
1016
|
+
args.help = true;
|
|
1017
|
+
}
|
|
1018
|
+
return args;
|
|
1019
|
+
}
|
|
1020
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1021
|
+
const arg = argv[i];
|
|
1022
|
+
if (arg === "-h" || arg === "--help") args.help = true;
|
|
1023
|
+
else if (arg === "-o") {
|
|
1024
|
+
args.out = argv[++i];
|
|
1025
|
+
if (args.out === void 0) {
|
|
1026
|
+
args.error = "-o needs a file";
|
|
1027
|
+
return args;
|
|
1028
|
+
}
|
|
1029
|
+
} else if (arg === "--verify") {
|
|
1030
|
+
args.verify = argv[++i];
|
|
1031
|
+
if (args.verify === void 0) {
|
|
1032
|
+
args.error = "--verify needs a test file";
|
|
1033
|
+
return args;
|
|
1034
|
+
}
|
|
1035
|
+
} else if (arg.startsWith("-")) {
|
|
1036
|
+
args.error = `unknown option ${arg}`;
|
|
1037
|
+
return args;
|
|
1038
|
+
} else if (args.dir === void 0) {
|
|
1039
|
+
args.dir = arg;
|
|
1040
|
+
} else {
|
|
1041
|
+
args.error = `unexpected extra argument ${arg}`;
|
|
1042
|
+
return args;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!args.help && args.dir === void 0) {
|
|
1046
|
+
args.error = "no directory given";
|
|
1047
|
+
}
|
|
1048
|
+
return args;
|
|
819
1049
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1050
|
+
async function readDir(folder) {
|
|
1051
|
+
try {
|
|
1052
|
+
return await fs3.readdir(folder, { withFileTypes: true });
|
|
1053
|
+
} catch {
|
|
1054
|
+
throw new Error(`${folder} does not exist. See \`pll examplar --help\`.`);
|
|
1055
|
+
}
|
|
825
1056
|
}
|
|
826
|
-
function
|
|
827
|
-
|
|
828
|
-
|
|
1057
|
+
async function readWheats(dir) {
|
|
1058
|
+
const folder = path3.join(dir, "wheats");
|
|
1059
|
+
const out = {};
|
|
1060
|
+
for (const entry of await readDir(folder)) {
|
|
1061
|
+
if (!entry.isFile() || !entry.name.endsWith(".py")) continue;
|
|
1062
|
+
out[entry.name.slice(0, -3)] = await fs3.readFile(path3.join(folder, entry.name), "utf8");
|
|
829
1063
|
}
|
|
830
|
-
if (
|
|
831
|
-
|
|
1064
|
+
if (Object.keys(out).length === 0) {
|
|
1065
|
+
throw new Error(`${folder} has no .py files in it.`);
|
|
1066
|
+
}
|
|
1067
|
+
return out;
|
|
832
1068
|
}
|
|
833
|
-
function
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
const lines = message.split(/\r?\n/);
|
|
842
|
-
const first = lines[0].trim();
|
|
843
|
-
let expected = [];
|
|
844
|
-
let subject = null;
|
|
845
|
-
const unionAt = first.indexOf(" did not match any element in the union:");
|
|
846
|
-
if (unionAt >= 0) {
|
|
847
|
-
subject = first.slice(0, unionAt);
|
|
848
|
-
for (const raw of lines.slice(1)) {
|
|
849
|
-
const m = raw.match(/^\s*([A-Za-z_][A-Za-z0-9_.\[\], ]*):/);
|
|
850
|
-
if (m) expected.push(m[1].trim());
|
|
1069
|
+
async function readChaffs(dir) {
|
|
1070
|
+
const folder = path3.join(dir, "chaffs");
|
|
1071
|
+
const out = {};
|
|
1072
|
+
const loose = [];
|
|
1073
|
+
for (const entry of await readDir(folder)) {
|
|
1074
|
+
if (entry.isFile() && entry.name.endsWith(".py")) {
|
|
1075
|
+
loose.push(entry.name);
|
|
1076
|
+
continue;
|
|
851
1077
|
}
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
[
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (m) {
|
|
862
|
-
subject = m[1];
|
|
863
|
-
expected = pick(m);
|
|
864
|
-
break;
|
|
865
|
-
}
|
|
1078
|
+
if (!entry.isDirectory()) continue;
|
|
1079
|
+
const inner = path3.join(folder, entry.name);
|
|
1080
|
+
const group = {};
|
|
1081
|
+
for (const file of await fs3.readdir(inner, { withFileTypes: true })) {
|
|
1082
|
+
if (!file.isFile() || !file.name.endsWith(".py")) continue;
|
|
1083
|
+
group[file.name.slice(0, -3)] = await fs3.readFile(path3.join(inner, file.name), "utf8");
|
|
1084
|
+
}
|
|
1085
|
+
if (Object.keys(group).length > 0) {
|
|
1086
|
+
out[entry.name] = group;
|
|
866
1087
|
}
|
|
867
1088
|
}
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
subject = withActual[1];
|
|
873
|
-
actual = withActual[2];
|
|
1089
|
+
if (loose.length > 0) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`${path3.join(folder, loose[0])} is not inside a function's directory. Chaffs go in \`chaffs/<function>/\`, named after the function they break - move it to \`chaffs/<function>/${loose[0]}\`.`
|
|
1092
|
+
);
|
|
874
1093
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (elementMatch) {
|
|
878
|
-
element = describeElement(elementMatch[1]);
|
|
879
|
-
subject = elementMatch[2];
|
|
1094
|
+
if (Object.keys(out).length === 0) {
|
|
1095
|
+
throw new Error(`${folder} has no \`<function>/*.py\` in it.`);
|
|
880
1096
|
}
|
|
881
|
-
|
|
882
|
-
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
async function runExamplar(runtime, view, argv) {
|
|
1100
|
+
const args = parseExamplarArgs(argv);
|
|
1101
|
+
if (args.help) {
|
|
1102
|
+
process.stdout.write(EXAMPLAR_USAGE);
|
|
1103
|
+
return EXIT.ok;
|
|
883
1104
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1105
|
+
if (args.error !== void 0) {
|
|
1106
|
+
process.stderr.write(`pll: ${args.error}
|
|
1107
|
+
|
|
1108
|
+
${EXAMPLAR_USAGE}`);
|
|
1109
|
+
return EXIT.usage;
|
|
887
1110
|
}
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
1111
|
+
const dir = path3.resolve(args.dir);
|
|
1112
|
+
let sources;
|
|
1113
|
+
try {
|
|
1114
|
+
sources = { wheats: await readWheats(dir), chaffs: await readChaffs(dir) };
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
view.problem(`pll: ${err instanceof Error ? err.message : String(err)}`);
|
|
1117
|
+
return EXIT.usage;
|
|
891
1118
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
return
|
|
905
|
-
headline: `${owner} expects ${membersPhrase(parsed.element)} ${named} to be ${wanted}, but ${parsed.element} is not.`,
|
|
906
|
-
howToFix: [
|
|
907
|
-
`Look at ${parsed.element} of the ${parsed.actual ?? "value"} you passed for ${named}.`,
|
|
908
|
-
"Every one has to match the annotation, not just the first."
|
|
909
|
-
]
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
const got = parsed.actual ? `, but got ${describeType(parsed.actual)}` : "";
|
|
913
|
-
const howToFix = [`Check the value you passed for ${named} on this line.`];
|
|
914
|
-
if (boolNote) howToFix.push(boolNote);
|
|
915
|
-
if (convert) {
|
|
916
|
-
howToFix.push(`If it should be ${wanted}, convert it with \`${convert}(...)\`.`);
|
|
917
|
-
}
|
|
918
|
-
if (annotation && parsed.actual) {
|
|
919
|
-
howToFix.push(
|
|
920
|
-
`Or, if ${named} really should be ${describeType(parsed.actual)}, change its annotation in ${owner} from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
921
|
-
);
|
|
1119
|
+
const built = await runtime.examplarBuild(JSON.stringify(sources));
|
|
1120
|
+
if (!built.ok || !built.bundle) {
|
|
1121
|
+
view.problem(`pll: ${built.error ?? "could not build the bundle"}`);
|
|
1122
|
+
return EXIT.usage;
|
|
1123
|
+
}
|
|
1124
|
+
const bundle = built.bundle;
|
|
1125
|
+
view.note(
|
|
1126
|
+
`built for Python ${bundle.built.python}: ${bundle.wheats.length} wheat(s), ${bundle.chaffs.length} chaff(s), providing ${bundle.provides.join(", ")}`
|
|
1127
|
+
);
|
|
1128
|
+
if (args.verify !== void 0) {
|
|
1129
|
+
const code = await verify(runtime, view, bundle, args.verify);
|
|
1130
|
+
if (code !== EXIT.ok) {
|
|
1131
|
+
return code;
|
|
922
1132
|
}
|
|
923
|
-
return {
|
|
924
|
-
headline: `${owner} expects ${named} to be ${wanted}${got}.`,
|
|
925
|
-
howToFix
|
|
926
|
-
};
|
|
927
1133
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1134
|
+
const json = JSON.stringify(bundle);
|
|
1135
|
+
if (args.out === void 0) {
|
|
1136
|
+
process.stdout.write(json + "\n");
|
|
1137
|
+
} else {
|
|
1138
|
+
await fs3.writeFile(path3.resolve(args.out), json + "\n", "utf8");
|
|
1139
|
+
view.note(`wrote ${args.out} (${json.length} bytes)`);
|
|
1140
|
+
}
|
|
1141
|
+
return EXIT.ok;
|
|
1142
|
+
}
|
|
1143
|
+
function caught(impl) {
|
|
1144
|
+
return !impl.loaded || Object.values(impl.tests).some((t) => t.outcome !== "pass");
|
|
1145
|
+
}
|
|
1146
|
+
async function verify(runtime, view, bundle, testsPath) {
|
|
1147
|
+
let testSource;
|
|
1148
|
+
try {
|
|
1149
|
+
testSource = await fs3.readFile(path3.resolve(testsPath), "utf8");
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
view.problem(`pll: cannot read ${testsPath}: ${err instanceof Error ? err.message : err}`);
|
|
1152
|
+
return EXIT.usage;
|
|
1153
|
+
}
|
|
1154
|
+
try {
|
|
1155
|
+
await runtime.ensurePytest();
|
|
1156
|
+
} catch {
|
|
1157
|
+
view.note("could not load pytest; assertion messages will be terse.");
|
|
1158
|
+
}
|
|
1159
|
+
const result = await runtime.examplarRun(
|
|
1160
|
+
testSource,
|
|
1161
|
+
JSON.stringify(bundle)
|
|
1162
|
+
);
|
|
1163
|
+
if (!result.ok) {
|
|
1164
|
+
view.problem(`pll: ${result.error ?? "the bundle could not be run"}`);
|
|
1165
|
+
return EXIT.usage;
|
|
1166
|
+
}
|
|
1167
|
+
let bad = 0;
|
|
1168
|
+
view.problem(`verifying with ${path3.basename(testsPath)}:`);
|
|
1169
|
+
for (const wheat of result.wheats ?? []) {
|
|
1170
|
+
const failures = Object.entries(wheat.tests).filter(([, t]) => t.outcome !== "pass");
|
|
1171
|
+
if (wheat.loaded && failures.length === 0) {
|
|
1172
|
+
view.note(` ok wheat ${wheat.id}: all ${Object.keys(wheat.tests).length} pass`);
|
|
1173
|
+
continue;
|
|
938
1174
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
howToFix.push("Use a bare `return`, or drop the `return` entirely.");
|
|
944
|
-
} else if (convert) {
|
|
945
|
-
howToFix.push(`Convert the returned value with \`${convert}(...)\`.`);
|
|
946
|
-
} else {
|
|
947
|
-
howToFix.push(`Return ${wanted} from this line.`);
|
|
1175
|
+
bad += 1;
|
|
1176
|
+
view.problem(` BAD wheat ${wheat.id}: your own tests do not all pass on it`);
|
|
1177
|
+
if (!wheat.loaded) {
|
|
1178
|
+
view.problem(` ${wheat.error_type}: ${wheat.error_message}`);
|
|
948
1179
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
howToFix.push(
|
|
952
|
-
`Or change ${owner}'s return annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
|
|
953
|
-
);
|
|
1180
|
+
for (const [name, t] of failures) {
|
|
1181
|
+
view.problem(` ${name}: ${(t.message ?? t.outcome).split("\n")[0]}`);
|
|
954
1182
|
}
|
|
955
|
-
return {
|
|
956
|
-
headline: `${owner} says it returns ${wanted}, but this line returns ${got}.`,
|
|
957
|
-
howToFix
|
|
958
|
-
};
|
|
959
1183
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1184
|
+
for (const chaff of result.chaffs ?? []) {
|
|
1185
|
+
const where = `chaff ${chaff.targets}/${chaff.id}`;
|
|
1186
|
+
if (caught(chaff)) {
|
|
1187
|
+
const by = Object.entries(chaff.tests).filter(([, t]) => t.outcome !== "pass").map(([name]) => name);
|
|
1188
|
+
view.note(` ok ${where}: caught by ${by.join(", ")}`);
|
|
1189
|
+
continue;
|
|
966
1190
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1191
|
+
bad += 1;
|
|
1192
|
+
view.problem(` BAD ${where}: no test catches it - it would never count`);
|
|
1193
|
+
}
|
|
1194
|
+
const attributed = Object.values(result.attribution ?? {});
|
|
1195
|
+
for (const name of result.chaffs_skipped ?? []) {
|
|
1196
|
+
if (attributed.some((names) => names.includes(name))) {
|
|
1197
|
+
view.problem(` --- ${name} chaffs not checked: its tests have to pass first`);
|
|
1198
|
+
continue;
|
|
972
1199
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1200
|
+
bad += 1;
|
|
1201
|
+
view.problem(
|
|
1202
|
+
` BAD your own suite has no tests for ${name}, so its chaffs are unproven`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (bad > 0) {
|
|
1206
|
+
view.problem(`pll: ${bad} problem(s) with this bundle; not written.`);
|
|
1207
|
+
return EXIT.testsFailed;
|
|
1208
|
+
}
|
|
1209
|
+
view.problem("verified.");
|
|
1210
|
+
return EXIT.ok;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/cli/runtime.ts
|
|
1214
|
+
var path5 = __toESM(require("node:path"));
|
|
1215
|
+
|
|
1216
|
+
// src/desktop/pyodideRuntime.ts
|
|
1217
|
+
var fs4 = __toESM(require("node:fs"));
|
|
1218
|
+
var path4 = __toESM(require("node:path"));
|
|
1219
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
1220
|
+
|
|
1221
|
+
// src/common/deliverResult.ts
|
|
1222
|
+
function deliverRunResult(result, onEvent, fileName) {
|
|
1223
|
+
if (result.displays) {
|
|
1224
|
+
for (const display of result.displays) {
|
|
1225
|
+
deliverDisplay(display, onEvent, fileName);
|
|
977
1226
|
}
|
|
978
|
-
return {
|
|
979
|
-
headline: `${named} is annotated as ${wanted}, but ${got} was assigned here.`,
|
|
980
|
-
howToFix
|
|
981
|
-
};
|
|
982
1227
|
}
|
|
1228
|
+
if (result.result_repr !== null && result.result_repr !== void 0) {
|
|
1229
|
+
onEvent({ kind: "result", repr: result.result_repr });
|
|
1230
|
+
}
|
|
1231
|
+
if (!result.ok && result.error_type) {
|
|
1232
|
+
onEvent({
|
|
1233
|
+
kind: "error",
|
|
1234
|
+
errorType: result.error_type,
|
|
1235
|
+
message: result.error_message ?? "",
|
|
1236
|
+
traceback: result.traceback ?? "",
|
|
1237
|
+
lineNumber: result.line_number,
|
|
1238
|
+
column: result.column,
|
|
1239
|
+
fileName
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
onEvent({ kind: "done" });
|
|
1243
|
+
}
|
|
1244
|
+
function deliverDisplay(display, onEvent, fileName) {
|
|
1245
|
+
switch (display.type) {
|
|
1246
|
+
case "stdout":
|
|
1247
|
+
onEvent({ kind: "stdout", text: display.text });
|
|
1248
|
+
break;
|
|
1249
|
+
case "stderr":
|
|
1250
|
+
onEvent({ kind: "stderr", text: display.text });
|
|
1251
|
+
break;
|
|
1252
|
+
case "image":
|
|
1253
|
+
onEvent({
|
|
1254
|
+
kind: "image",
|
|
1255
|
+
svg: display.data,
|
|
1256
|
+
width: display.width,
|
|
1257
|
+
height: display.height,
|
|
1258
|
+
source: fileName
|
|
1259
|
+
});
|
|
1260
|
+
break;
|
|
1261
|
+
case "reactor":
|
|
1262
|
+
onEvent({
|
|
1263
|
+
kind: "reactor",
|
|
1264
|
+
id: display.id,
|
|
1265
|
+
title: display.title,
|
|
1266
|
+
tickRate: display.tick_rate,
|
|
1267
|
+
ticking: display.ticking,
|
|
1268
|
+
wantsKeys: display.wants_keys,
|
|
1269
|
+
wantsMouse: display.wants_mouse,
|
|
1270
|
+
register: display.register,
|
|
1271
|
+
frame: display.frame,
|
|
1272
|
+
index: display.index,
|
|
1273
|
+
length: display.length,
|
|
1274
|
+
atEnd: display.at_end,
|
|
1275
|
+
stopped: display.stopped,
|
|
1276
|
+
valueRepr: display.value_repr
|
|
1277
|
+
});
|
|
1278
|
+
break;
|
|
1279
|
+
case "table":
|
|
1280
|
+
onEvent({
|
|
1281
|
+
kind: "table",
|
|
1282
|
+
columns: display.columns,
|
|
1283
|
+
rows: display.rows,
|
|
1284
|
+
rowCount: display.row_count,
|
|
1285
|
+
shownCount: display.shown_count,
|
|
1286
|
+
truncated: display.truncated,
|
|
1287
|
+
source: fileName
|
|
1288
|
+
});
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function asPlain(value) {
|
|
1293
|
+
if (value instanceof Map) {
|
|
1294
|
+
const obj = {};
|
|
1295
|
+
for (const [k, v] of value.entries()) {
|
|
1296
|
+
obj[String(k)] = asPlain(v);
|
|
1297
|
+
}
|
|
1298
|
+
return obj;
|
|
1299
|
+
}
|
|
1300
|
+
if (Array.isArray(value)) {
|
|
1301
|
+
return value.map(asPlain);
|
|
1302
|
+
}
|
|
1303
|
+
return value;
|
|
1304
|
+
}
|
|
1305
|
+
function adaptTestCase(raw) {
|
|
1306
|
+
const row = asPlain(raw) ?? {};
|
|
1307
|
+
const line = row.line_number;
|
|
983
1308
|
return {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1309
|
+
name: String(row.name ?? ""),
|
|
1310
|
+
outcome: String(row.outcome ?? "failed"),
|
|
1311
|
+
lineNumber: typeof line === "number" ? line : null,
|
|
1312
|
+
message: row.message == null ? null : String(row.message),
|
|
1313
|
+
stdout: row.stdout == null ? null : String(row.stdout)
|
|
989
1314
|
};
|
|
990
1315
|
}
|
|
1316
|
+
function deliverTestResult(result, onEvent, fileName) {
|
|
1317
|
+
if (result.internal_error && result.error_type) {
|
|
1318
|
+
onEvent({
|
|
1319
|
+
kind: "error",
|
|
1320
|
+
errorType: result.error_type,
|
|
1321
|
+
message: result.error_message ?? "",
|
|
1322
|
+
traceback: result.traceback ?? "",
|
|
1323
|
+
lineNumber: result.line_number,
|
|
1324
|
+
column: result.column,
|
|
1325
|
+
fileName
|
|
1326
|
+
});
|
|
1327
|
+
onEvent({ kind: "done" });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const tests = Array.isArray(result.tests) ? result.tests.map(adaptTestCase) : [];
|
|
1331
|
+
onEvent({
|
|
1332
|
+
kind: "testReport",
|
|
1333
|
+
fileName,
|
|
1334
|
+
passed: result.passed ?? 0,
|
|
1335
|
+
failed: result.failed ?? 0,
|
|
1336
|
+
skipped: result.skipped ?? 0,
|
|
1337
|
+
errors: result.errors ?? 0,
|
|
1338
|
+
tests
|
|
1339
|
+
});
|
|
1340
|
+
onEvent({ kind: "done" });
|
|
1341
|
+
}
|
|
991
1342
|
|
|
992
|
-
// src/common/
|
|
993
|
-
var
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1343
|
+
// src/common/interruptBuffer.ts
|
|
1344
|
+
var INTERRUPT_SAB_BYTES = 1;
|
|
1345
|
+
var INTERRUPT_SIGINT = 2;
|
|
1346
|
+
function tryCreateInterruptBuffer() {
|
|
1347
|
+
try {
|
|
1348
|
+
return new SharedArrayBuffer(INTERRUPT_SAB_BYTES);
|
|
1349
|
+
} catch {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function signalInterrupt(sab) {
|
|
1354
|
+
Atomics.store(new Uint8Array(sab), 0, INTERRUPT_SIGINT);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/common/stdinBuffer.ts
|
|
1358
|
+
var STDIN_SAB_BYTES = 64 * 1024;
|
|
1359
|
+
var STDIN_STATE_INDEX = 0;
|
|
1360
|
+
var STDIN_LENGTH_INDEX = 1;
|
|
1361
|
+
var STDIN_PAYLOAD_OFFSET = 8;
|
|
1362
|
+
var STDIN_STATE_LINE = 1;
|
|
1363
|
+
var STDIN_STATE_EOF = 2;
|
|
1364
|
+
function tryCreateStdinBuffer() {
|
|
1365
|
+
try {
|
|
1366
|
+
return new SharedArrayBuffer(STDIN_SAB_BYTES);
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function writeStdinLine(sab, line) {
|
|
1372
|
+
const state = new Int32Array(sab);
|
|
1373
|
+
if (line === null) {
|
|
1374
|
+
Atomics.store(state, STDIN_STATE_INDEX, STDIN_STATE_EOF);
|
|
1375
|
+
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const encoded = new TextEncoder().encode(line);
|
|
1379
|
+
const max = sab.byteLength - STDIN_PAYLOAD_OFFSET;
|
|
1380
|
+
const n = Math.min(encoded.length, max);
|
|
1381
|
+
const bytes = new Uint8Array(sab, STDIN_PAYLOAD_OFFSET);
|
|
1382
|
+
bytes.set(encoded.subarray(0, n));
|
|
1383
|
+
Atomics.store(state, STDIN_LENGTH_INDEX, n);
|
|
1384
|
+
Atomics.store(state, STDIN_STATE_INDEX, STDIN_STATE_LINE);
|
|
1385
|
+
Atomics.notify(state, STDIN_STATE_INDEX);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/common/workerRuntime.ts
|
|
1389
|
+
var WorkerPythonRuntime = class {
|
|
1390
|
+
worker = null;
|
|
1391
|
+
nextId = 1;
|
|
1392
|
+
pending = /* @__PURE__ */ new Map();
|
|
1393
|
+
initPromise = null;
|
|
1394
|
+
stdinBuffer = null;
|
|
1395
|
+
interruptBuffer = null;
|
|
1396
|
+
stdinHandler = null;
|
|
1397
|
+
/** Event sink for the in-flight runFile, so live displays can stream. */
|
|
1398
|
+
live = null;
|
|
1399
|
+
async initialize() {
|
|
1400
|
+
if (!this.initPromise) {
|
|
1401
|
+
this.initPromise = this.doInitialize();
|
|
1005
1402
|
}
|
|
1006
|
-
|
|
1403
|
+
await this.initPromise;
|
|
1007
1404
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
innermost ? innermost.functionName : null,
|
|
1025
|
-
level
|
|
1405
|
+
async doInitialize() {
|
|
1406
|
+
const indexUrl = this.resolveIndexUrl();
|
|
1407
|
+
this.worker = this.spawn({
|
|
1408
|
+
onMessage: (msg) => this.handleMessage(msg),
|
|
1409
|
+
onError: (err) => this.rejectAllPending(err)
|
|
1410
|
+
});
|
|
1411
|
+
this.stdinBuffer = tryCreateStdinBuffer();
|
|
1412
|
+
this.interruptBuffer = tryCreateInterruptBuffer();
|
|
1413
|
+
await this.request(
|
|
1414
|
+
{
|
|
1415
|
+
type: "init",
|
|
1416
|
+
indexUrl,
|
|
1417
|
+
...this.stdinBuffer ? { stdinBuffer: this.stdinBuffer } : {},
|
|
1418
|
+
...this.interruptBuffer ? { interruptBuffer: this.interruptBuffer } : {}
|
|
1419
|
+
},
|
|
1420
|
+
"ready"
|
|
1026
1421
|
);
|
|
1422
|
+
}
|
|
1423
|
+
async runFile(request, onEvent) {
|
|
1424
|
+
await this.initialize();
|
|
1425
|
+
this.live = { onEvent, fileName: request.fileName };
|
|
1426
|
+
try {
|
|
1427
|
+
const { result } = await this.request({ type: "runFile", ...request }, "result");
|
|
1428
|
+
deliverRunResult(result, onEvent, request.fileName);
|
|
1429
|
+
} finally {
|
|
1430
|
+
this.live = null;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async replEval(request, onEvent) {
|
|
1434
|
+
await this.initialize();
|
|
1435
|
+
const { result } = await this.request({ type: "replEval", ...request }, "result");
|
|
1436
|
+
deliverRunResult(result, onEvent, "<repl>");
|
|
1437
|
+
}
|
|
1438
|
+
async checkReplComplete(code) {
|
|
1439
|
+
await this.initialize();
|
|
1440
|
+
const { result } = await this.request({ type: "checkSyntax", code }, "syntax");
|
|
1027
1441
|
return {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
headline: explanation.headline,
|
|
1034
|
-
howToFix: explanation.howToFix,
|
|
1035
|
-
fileName: blamed ? blamed.fileName : fileName,
|
|
1036
|
-
lineNumber: blamed ? blamed.line : parsedError.lineNumber,
|
|
1037
|
-
column: null,
|
|
1038
|
-
nameToken: parsed.name,
|
|
1039
|
-
severity: "error",
|
|
1040
|
-
raw: parsedError.traceback,
|
|
1041
|
-
origin: "runtime",
|
|
1042
|
-
level
|
|
1442
|
+
status: result.status,
|
|
1443
|
+
errorType: result.error_type,
|
|
1444
|
+
message: result.message,
|
|
1445
|
+
lineNumber: result.lineno,
|
|
1446
|
+
offset: result.offset
|
|
1043
1447
|
};
|
|
1044
1448
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
function findRuntimeFinding(source, fileName, level, parsedError) {
|
|
1050
|
-
for (const analyzer of analyzers) {
|
|
1051
|
-
if (!analyzer.handles.includes(parsedError.errorType)) {
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
const finding = analyzer.analyze({ source, fileName, level, parsedError });
|
|
1055
|
-
if (finding) {
|
|
1056
|
-
return finding;
|
|
1057
|
-
}
|
|
1449
|
+
async hasTests(code) {
|
|
1450
|
+
await this.initialize();
|
|
1451
|
+
const { result } = await this.request({ type: "hasTests", code }, "hasTests");
|
|
1452
|
+
return result;
|
|
1058
1453
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
// src/common/analyzers/runtimeFinding.ts
|
|
1063
|
-
function findingForErrorEvent(event, source, fileName, level) {
|
|
1064
|
-
const traceback = event.traceback || `${event.errorType}: ${event.message}`;
|
|
1065
|
-
const parsed = parsePythonError(traceback);
|
|
1066
|
-
if (parsed.lineNumber === null && event.lineNumber !== null) {
|
|
1067
|
-
parsed.lineNumber = event.lineNumber;
|
|
1454
|
+
async ensurePackages(code) {
|
|
1455
|
+
await this.initialize();
|
|
1456
|
+
await this.request({ type: "loadPackages", code }, "packagesReady");
|
|
1068
1457
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1458
|
+
async ensurePytest() {
|
|
1459
|
+
await this.initialize();
|
|
1460
|
+
await this.request({ type: "loadPytest" }, "pytestReady");
|
|
1071
1461
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1462
|
+
async runTests(request, onEvent) {
|
|
1463
|
+
await this.initialize();
|
|
1464
|
+
const { result } = await this.request(
|
|
1465
|
+
{
|
|
1466
|
+
type: "runTests",
|
|
1467
|
+
code: request.code,
|
|
1468
|
+
fileName: request.fileName,
|
|
1469
|
+
level: request.level
|
|
1470
|
+
},
|
|
1471
|
+
"testResult"
|
|
1472
|
+
);
|
|
1473
|
+
deliverTestResult(result, onEvent, request.fileName);
|
|
1074
1474
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
var ANY_IMPORT_RE = /(^|\n)[ \t]*(import|from)[ \t]+\S/;
|
|
1080
|
-
|
|
1081
|
-
// src/cli/files.ts
|
|
1082
|
-
var fs2 = __toESM(require("node:fs/promises"));
|
|
1083
|
-
var path3 = __toESM(require("node:path"));
|
|
1084
|
-
|
|
1085
|
-
// src/common/workspaceFilePolicy.ts
|
|
1086
|
-
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1087
|
-
var MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
1088
|
-
var MAX_FILES = 50;
|
|
1089
|
-
var MOUNT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1090
|
-
".csv",
|
|
1091
|
-
".txt",
|
|
1092
|
-
".tsv",
|
|
1093
|
-
".json",
|
|
1094
|
-
".md",
|
|
1095
|
-
".dat",
|
|
1096
|
-
".xml",
|
|
1097
|
-
".py"
|
|
1098
|
-
]);
|
|
1099
|
-
var WRITEBACK_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1100
|
-
".csv",
|
|
1101
|
-
".txt",
|
|
1102
|
-
".tsv",
|
|
1103
|
-
".json",
|
|
1104
|
-
".md",
|
|
1105
|
-
".dat",
|
|
1106
|
-
".xml"
|
|
1107
|
-
]);
|
|
1108
|
-
function extensionOf(name) {
|
|
1109
|
-
const dot = name.lastIndexOf(".");
|
|
1110
|
-
if (dot <= 0) {
|
|
1111
|
-
return "";
|
|
1475
|
+
async staticAnalyze(request) {
|
|
1476
|
+
await this.initialize();
|
|
1477
|
+
const { result } = await this.request({ type: "staticAnalyze", ...request }, "static");
|
|
1478
|
+
return result ?? [];
|
|
1112
1479
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (!name || name === "." || name === "..") {
|
|
1117
|
-
return false;
|
|
1480
|
+
async mountWorkspaceFiles(files) {
|
|
1481
|
+
await this.initialize();
|
|
1482
|
+
await this.request({ type: "mountWorkspace", files }, "workspaceReady");
|
|
1118
1483
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1484
|
+
async collectWorkspaceFiles() {
|
|
1485
|
+
await this.initialize();
|
|
1486
|
+
const { files } = await this.request({ type: "collectWorkspace" }, "workspaceFiles");
|
|
1487
|
+
return files;
|
|
1121
1488
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1489
|
+
async examplarBuild(sources) {
|
|
1490
|
+
await this.initialize();
|
|
1491
|
+
const { result } = await this.request({ type: "examplarBuild", sources }, "examplarBuilt");
|
|
1492
|
+
return result;
|
|
1124
1493
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
function utf8ByteLength(text) {
|
|
1134
|
-
return new TextEncoder().encode(text).byteLength;
|
|
1135
|
-
}
|
|
1136
|
-
function decodeMountContents(data) {
|
|
1137
|
-
if (data.byteLength > MAX_FILE_BYTES) {
|
|
1138
|
-
return void 0;
|
|
1494
|
+
async examplarRun(testSource, bundle) {
|
|
1495
|
+
await this.initialize();
|
|
1496
|
+
const { result } = await this.request(
|
|
1497
|
+
{ type: "examplarRun", testSource, bundle },
|
|
1498
|
+
"examplarRan"
|
|
1499
|
+
);
|
|
1500
|
+
return result;
|
|
1139
1501
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1502
|
+
async reactorStep(reactorId, event) {
|
|
1503
|
+
await this.initialize();
|
|
1504
|
+
const { result } = await this.request(
|
|
1505
|
+
{ type: "reactorStep", reactorId, event },
|
|
1506
|
+
"reactorFrame"
|
|
1507
|
+
);
|
|
1508
|
+
return result;
|
|
1144
1509
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (
|
|
1159
|
-
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
if (candidate.contents.includes("\0")) {
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
text = candidate.contents;
|
|
1166
|
-
} else {
|
|
1167
|
-
const decoded = decodeMountContents(candidate.contents);
|
|
1168
|
-
if (decoded === void 0) {
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
text = decoded;
|
|
1510
|
+
async reactorSeek(reactorId, index) {
|
|
1511
|
+
await this.initialize();
|
|
1512
|
+
const { result } = await this.request(
|
|
1513
|
+
{ type: "reactorSeek", reactorId, index },
|
|
1514
|
+
"reactorFrame"
|
|
1515
|
+
);
|
|
1516
|
+
return result;
|
|
1517
|
+
}
|
|
1518
|
+
async reactorDispose(reactorId) {
|
|
1519
|
+
await this.initialize();
|
|
1520
|
+
await this.request({ type: "reactorDispose", reactorId }, "reactorDisposed");
|
|
1521
|
+
}
|
|
1522
|
+
interrupt() {
|
|
1523
|
+
if (!this.interruptBuffer) {
|
|
1524
|
+
return false;
|
|
1172
1525
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1526
|
+
signalInterrupt(this.interruptBuffer);
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
setStdinHandler(handler) {
|
|
1530
|
+
this.stdinHandler = handler;
|
|
1531
|
+
}
|
|
1532
|
+
dispose() {
|
|
1533
|
+
this.worker?.terminate();
|
|
1534
|
+
this.worker = null;
|
|
1535
|
+
this.initPromise = null;
|
|
1536
|
+
this.stdinBuffer = null;
|
|
1537
|
+
this.interruptBuffer = null;
|
|
1538
|
+
this.live = null;
|
|
1539
|
+
this.rejectAllPending(new Error("Runtime disposed"));
|
|
1540
|
+
}
|
|
1541
|
+
/** Send `msg` and resolve with the reply, asserting it is of type `expected`. */
|
|
1542
|
+
async request(msg, expected) {
|
|
1543
|
+
const worker = this.worker;
|
|
1544
|
+
if (!worker) {
|
|
1545
|
+
throw new Error("Worker not initialized");
|
|
1176
1546
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1547
|
+
const id = this.nextId++;
|
|
1548
|
+
const reply = await new Promise((resolve4, reject) => {
|
|
1549
|
+
this.pending.set(id, { resolve: resolve4, reject });
|
|
1550
|
+
worker.post({ id, ...msg });
|
|
1551
|
+
});
|
|
1552
|
+
if (reply.type !== expected) {
|
|
1553
|
+
throw new Error(`Pyodide worker replied "${reply.type}", expected "${expected}"`);
|
|
1554
|
+
}
|
|
1555
|
+
return reply;
|
|
1179
1556
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
if (selected.length >= MAX_FILES) {
|
|
1187
|
-
break;
|
|
1557
|
+
handleMessage(msg) {
|
|
1558
|
+
if (msg.type === "display") {
|
|
1559
|
+
if (this.live) {
|
|
1560
|
+
deliverDisplay(msg.payload, this.live.onEvent, this.live.fileName);
|
|
1561
|
+
}
|
|
1562
|
+
return;
|
|
1188
1563
|
}
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1564
|
+
if (msg.type === "stdinRequest") {
|
|
1565
|
+
void this.handleStdinRequest();
|
|
1566
|
+
return;
|
|
1191
1567
|
}
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
1194
|
-
|
|
1568
|
+
const pending = this.pending.get(msg.id);
|
|
1569
|
+
if (!pending) {
|
|
1570
|
+
return;
|
|
1195
1571
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1572
|
+
this.pending.delete(msg.id);
|
|
1573
|
+
if (msg.type === "error") {
|
|
1574
|
+
pending.reject(new Error(msg.message));
|
|
1575
|
+
} else {
|
|
1576
|
+
pending.resolve(msg);
|
|
1198
1577
|
}
|
|
1199
|
-
selected.push(file);
|
|
1200
|
-
total += size;
|
|
1201
|
-
}
|
|
1202
|
-
return selected;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// src/cli/files.ts
|
|
1206
|
-
async function collectSiblingFiles(scriptPath) {
|
|
1207
|
-
const folder = path3.dirname(path3.resolve(scriptPath));
|
|
1208
|
-
let entries;
|
|
1209
|
-
try {
|
|
1210
|
-
entries = await fs2.readdir(folder, { withFileTypes: true });
|
|
1211
|
-
} catch {
|
|
1212
|
-
return [];
|
|
1213
1578
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
continue;
|
|
1218
|
-
}
|
|
1219
|
-
try {
|
|
1220
|
-
candidates.push({ name: entry.name, contents: await fs2.readFile(path3.join(folder, entry.name)) });
|
|
1221
|
-
} catch {
|
|
1579
|
+
rejectAllPending(err) {
|
|
1580
|
+
for (const pending of this.pending.values()) {
|
|
1581
|
+
pending.reject(err);
|
|
1222
1582
|
}
|
|
1583
|
+
this.pending.clear();
|
|
1223
1584
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
async function writeBackSiblingFiles(scriptPath, files) {
|
|
1227
|
-
const folder = path3.dirname(path3.resolve(scriptPath));
|
|
1228
|
-
const written = [];
|
|
1229
|
-
for (const file of selectWritebackFiles(files)) {
|
|
1585
|
+
async handleStdinRequest() {
|
|
1586
|
+
let line = null;
|
|
1230
1587
|
try {
|
|
1231
|
-
await
|
|
1232
|
-
written.push(file.name);
|
|
1588
|
+
line = this.stdinHandler ? await this.stdinHandler() : null;
|
|
1233
1589
|
} catch {
|
|
1590
|
+
line = null;
|
|
1234
1591
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// src/cli/run.ts
|
|
1240
|
-
var EXIT = {
|
|
1241
|
-
ok: 0,
|
|
1242
|
-
/** The program raised. */
|
|
1243
|
-
programError: 1,
|
|
1244
|
-
/** Level checks found problems, so the file was not run. */
|
|
1245
|
-
blocked: 2,
|
|
1246
|
-
/** The program ran, but its in-file tests did not all pass. */
|
|
1247
|
-
testsFailed: 3,
|
|
1248
|
-
/** Bad usage, or PLL itself could not run. */
|
|
1249
|
-
usage: 64
|
|
1250
|
-
};
|
|
1251
|
-
async function runFile(runtime, view, opts) {
|
|
1252
|
-
const fileName = path4.basename(opts.file);
|
|
1253
|
-
let source;
|
|
1254
|
-
try {
|
|
1255
|
-
source = await fs3.readFile(opts.file, "utf8");
|
|
1256
|
-
} catch (err) {
|
|
1257
|
-
view.problem(`Cannot read ${opts.file}: ${errText(err)}`);
|
|
1258
|
-
return EXIT.usage;
|
|
1259
|
-
}
|
|
1260
|
-
const level = parseLevel(source);
|
|
1261
|
-
view.note(`${fileName} [${level}]`);
|
|
1262
|
-
if (levelHasStaticChecks(level)) {
|
|
1263
|
-
const blocked = await staticChecks(runtime, view, source, fileName, level);
|
|
1264
|
-
if (blocked) {
|
|
1265
|
-
return EXIT.blocked;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
await ensurePackages(runtime, view, source);
|
|
1269
|
-
try {
|
|
1270
|
-
await runtime.mountWorkspaceFiles(await collectSiblingFiles(opts.file));
|
|
1271
|
-
} catch (err) {
|
|
1272
|
-
view.note(`Could not load files next to this script (${errText(err)}).`);
|
|
1273
|
-
}
|
|
1274
|
-
const onEvent = (event) => {
|
|
1275
|
-
if (event.kind === "error") {
|
|
1276
|
-
const finding = findingForErrorEvent(event, source, fileName, level);
|
|
1277
|
-
if (finding) {
|
|
1278
|
-
view.runtimeFinding(finding);
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1592
|
+
if (this.stdinBuffer) {
|
|
1593
|
+
writeStdinLine(this.stdinBuffer, line);
|
|
1281
1594
|
}
|
|
1282
|
-
view.handle(event);
|
|
1283
|
-
};
|
|
1284
|
-
if (opts.runTests) {
|
|
1285
|
-
await maybeRunTests(runtime, view, source, fileName, level, onEvent);
|
|
1286
|
-
}
|
|
1287
|
-
await runtime.runFile({ code: source, fileName, sessionKey: opts.file, level }, onEvent);
|
|
1288
|
-
await writeBack(runtime, view, opts.file);
|
|
1289
|
-
if (view.sawError) return EXIT.programError;
|
|
1290
|
-
if (view.testFailures > 0) return EXIT.testsFailed;
|
|
1291
|
-
return EXIT.ok;
|
|
1292
|
-
}
|
|
1293
|
-
async function staticChecks(runtime, view, source, fileName, level) {
|
|
1294
|
-
let raw;
|
|
1295
|
-
try {
|
|
1296
|
-
raw = await runtime.staticAnalyze({ code: source, fileName, level });
|
|
1297
|
-
} catch (err) {
|
|
1298
|
-
view.note(`Static analysis failed (${errText(err)}); running anyway.`);
|
|
1299
|
-
return false;
|
|
1300
|
-
}
|
|
1301
|
-
const findings = enrichStaticFindings(raw, level, fileName);
|
|
1302
|
-
if (findings.length === 0) {
|
|
1303
|
-
return false;
|
|
1304
|
-
}
|
|
1305
|
-
view.findings(findings);
|
|
1306
|
-
view.problem(`Static analysis found ${findings.length} problem(s). File not run.`);
|
|
1307
|
-
return true;
|
|
1308
|
-
}
|
|
1309
|
-
async function ensurePackages(runtime, view, source) {
|
|
1310
|
-
if (!ANY_IMPORT_RE.test(source)) {
|
|
1311
|
-
return;
|
|
1312
1595
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
// src/desktop/pyodideRuntime.ts
|
|
1599
|
+
var DesktopPyodideRuntime = class extends WorkerPythonRuntime {
|
|
1600
|
+
constructor(paths) {
|
|
1601
|
+
super();
|
|
1602
|
+
this.paths = paths;
|
|
1320
1603
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1604
|
+
paths;
|
|
1605
|
+
resolveIndexUrl() {
|
|
1606
|
+
const found = this.paths.indexUrlCandidates.find(
|
|
1607
|
+
(dir) => fs4.existsSync(path4.join(dir, "pyodide.asm.wasm"))
|
|
1608
|
+
);
|
|
1609
|
+
if (!found) {
|
|
1610
|
+
throw new Error(this.paths.missingAssetsHint);
|
|
1326
1611
|
}
|
|
1327
|
-
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
view.note(`Could not check for tests (${errText(err)}); skipping them.`);
|
|
1330
|
-
return;
|
|
1612
|
+
return found;
|
|
1331
1613
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
view.note(`Could not save files next to this script (${errText(err)}).`);
|
|
1614
|
+
spawn(handlers) {
|
|
1615
|
+
const worker = new import_node_worker_threads.Worker(this.paths.workerPath);
|
|
1616
|
+
worker.on("message", (msg) => handlers.onMessage(msg));
|
|
1617
|
+
worker.on(
|
|
1618
|
+
"error",
|
|
1619
|
+
(err) => handlers.onError(err instanceof Error ? err : new Error(String(err)))
|
|
1620
|
+
);
|
|
1621
|
+
return {
|
|
1622
|
+
post: (msg) => worker.postMessage(msg),
|
|
1623
|
+
terminate: () => void worker.terminate()
|
|
1624
|
+
};
|
|
1346
1625
|
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// src/cli/runtime.ts
|
|
1629
|
+
function createCliRuntime() {
|
|
1630
|
+
return new DesktopPyodideRuntime({
|
|
1631
|
+
// `pyodide` is external in the bundle, so this resolves at run time
|
|
1632
|
+
// from the installed package rather than being inlined.
|
|
1633
|
+
indexUrlCandidates: [path5.dirname(require.resolve("pyodide"))],
|
|
1634
|
+
workerPath: path5.join(__dirname, "worker.cjs"),
|
|
1635
|
+
missingAssetsHint: "Could not find Pyodide's assets. Reinstall pll-python so its `pyodide` dependency is present."
|
|
1636
|
+
});
|
|
1350
1637
|
}
|
|
1351
1638
|
|
|
1352
1639
|
// src/cli/stdin.ts
|
|
@@ -1381,7 +1668,7 @@ function createLineReader() {
|
|
|
1381
1668
|
return Promise.resolve(null);
|
|
1382
1669
|
}
|
|
1383
1670
|
ensure();
|
|
1384
|
-
return new Promise((
|
|
1671
|
+
return new Promise((resolve4) => pending.push(resolve4));
|
|
1385
1672
|
},
|
|
1386
1673
|
close() {
|
|
1387
1674
|
closed = true;
|
|
@@ -1392,8 +1679,8 @@ function createLineReader() {
|
|
|
1392
1679
|
}
|
|
1393
1680
|
|
|
1394
1681
|
// src/cli/view.ts
|
|
1395
|
-
var
|
|
1396
|
-
var
|
|
1682
|
+
var fs5 = __toESM(require("node:fs"));
|
|
1683
|
+
var path6 = __toESM(require("node:path"));
|
|
1397
1684
|
|
|
1398
1685
|
// src/common/analyzers/findingLocation.ts
|
|
1399
1686
|
function findingLocation(finding) {
|
|
@@ -1532,10 +1819,10 @@ var CliView = class {
|
|
|
1532
1819
|
);
|
|
1533
1820
|
return;
|
|
1534
1821
|
}
|
|
1535
|
-
const target =
|
|
1822
|
+
const target = path6.join(dir, `image-${this.imageCount}.svg`);
|
|
1536
1823
|
try {
|
|
1537
|
-
|
|
1538
|
-
|
|
1824
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1825
|
+
fs5.writeFileSync(target, svg, "utf8");
|
|
1539
1826
|
this.note(this.dim(`[image ${width}x${height} -> ${target}]`));
|
|
1540
1827
|
} catch (err) {
|
|
1541
1828
|
this.problem(`Could not write ${target}: ${errText2(err)}`);
|
|
@@ -1591,6 +1878,9 @@ The level comes from the file's own \`#level\` line, exactly as in the
|
|
|
1591
1878
|
editor; there is deliberately no flag to override it, so a file behaves the
|
|
1592
1879
|
same everywhere.
|
|
1593
1880
|
|
|
1881
|
+
Subcommands
|
|
1882
|
+
examplar build author an Examplar bundle; see \`pll examplar --help\`
|
|
1883
|
+
|
|
1594
1884
|
Options
|
|
1595
1885
|
--no-tests do not run the file's \`test_*\` functions first
|
|
1596
1886
|
--save-images <dir> write pictures there as .svg (they cannot be drawn
|
|
@@ -1607,12 +1897,14 @@ Exit codes
|
|
|
1607
1897
|
0 ran, tests passed 2 level checks blocked it
|
|
1608
1898
|
1 the program raised 3 a test failed
|
|
1609
1899
|
`;
|
|
1900
|
+
function colorDefault() {
|
|
1901
|
+
return process.stderr.isTTY === true && !process.env.NO_COLOR;
|
|
1902
|
+
}
|
|
1610
1903
|
function parseArgs(argv) {
|
|
1611
1904
|
const args = {
|
|
1612
1905
|
runTests: true,
|
|
1613
1906
|
quiet: false,
|
|
1614
|
-
|
|
1615
|
-
color: process.stderr.isTTY === true && !process.env.NO_COLOR,
|
|
1907
|
+
color: colorDefault(),
|
|
1616
1908
|
help: false,
|
|
1617
1909
|
version: false
|
|
1618
1910
|
};
|
|
@@ -1649,13 +1941,31 @@ function parseArgs(argv) {
|
|
|
1649
1941
|
return args;
|
|
1650
1942
|
}
|
|
1651
1943
|
async function main(argv) {
|
|
1944
|
+
const firstPositional = argv.find((arg) => !arg.startsWith("-"));
|
|
1945
|
+
if (firstPositional === "examplar") {
|
|
1946
|
+
const at = argv.indexOf("examplar");
|
|
1947
|
+
const leading = argv.slice(0, at);
|
|
1948
|
+
const view2 = new CliView({
|
|
1949
|
+
quiet: leading.includes("-q") || leading.includes("--quiet"),
|
|
1950
|
+
color: colorDefault() && !leading.includes("--no-color")
|
|
1951
|
+
});
|
|
1952
|
+
const runtime2 = createCliRuntime();
|
|
1953
|
+
try {
|
|
1954
|
+
return await runExamplar(runtime2, view2, argv.slice(at + 1));
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
view2.problem(`pll: ${err instanceof Error ? err.message : String(err)}`);
|
|
1957
|
+
return EXIT.usage;
|
|
1958
|
+
} finally {
|
|
1959
|
+
runtime2.dispose();
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1652
1962
|
const args = parseArgs(argv);
|
|
1653
1963
|
if (args.help) {
|
|
1654
1964
|
process.stdout.write(USAGE);
|
|
1655
1965
|
return EXIT.ok;
|
|
1656
1966
|
}
|
|
1657
1967
|
if (args.version) {
|
|
1658
|
-
process.stdout.write("0.1.
|
|
1968
|
+
process.stdout.write("0.1.2\n");
|
|
1659
1969
|
return EXIT.ok;
|
|
1660
1970
|
}
|
|
1661
1971
|
if (args.error !== void 0) {
|
|
@@ -1686,7 +1996,7 @@ ${USAGE}`);
|
|
|
1686
1996
|
process.on("SIGINT", onSigint);
|
|
1687
1997
|
try {
|
|
1688
1998
|
return await runFile(runtime, view, {
|
|
1689
|
-
file:
|
|
1999
|
+
file: path7.resolve(args.file),
|
|
1690
2000
|
runTests: args.runTests
|
|
1691
2001
|
});
|
|
1692
2002
|
} catch (err) {
|