pll-python 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +114 -0
  2. package/cli.cjs +1711 -0
  3. package/package.json +36 -0
  4. package/worker.cjs +1478 -0
package/cli.cjs ADDED
@@ -0,0 +1,1711 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/main.ts
27
+ var path6 = __toESM(require("node:path"));
28
+
29
+ // src/cli/runtime.ts
30
+ var path2 = __toESM(require("node:path"));
31
+
32
+ // src/desktop/pyodideRuntime.ts
33
+ var fs = __toESM(require("node:fs"));
34
+ var path = __toESM(require("node:path"));
35
+ var import_node_worker_threads = require("node:worker_threads");
36
+
37
+ // src/common/deliverResult.ts
38
+ function deliverRunResult(result, onEvent, fileName) {
39
+ if (result.displays) {
40
+ for (const display of result.displays) {
41
+ deliverDisplay(display, onEvent, fileName);
42
+ }
43
+ }
44
+ if (result.result_repr !== null && result.result_repr !== void 0) {
45
+ onEvent({ kind: "result", repr: result.result_repr });
46
+ }
47
+ if (!result.ok && result.error_type) {
48
+ onEvent({
49
+ kind: "error",
50
+ errorType: result.error_type,
51
+ message: result.error_message ?? "",
52
+ traceback: result.traceback ?? "",
53
+ lineNumber: result.line_number,
54
+ column: result.column,
55
+ fileName
56
+ });
57
+ }
58
+ onEvent({ kind: "done" });
59
+ }
60
+ function deliverDisplay(display, onEvent, fileName) {
61
+ switch (display.type) {
62
+ case "stdout":
63
+ onEvent({ kind: "stdout", text: display.text });
64
+ break;
65
+ case "stderr":
66
+ onEvent({ kind: "stderr", text: display.text });
67
+ break;
68
+ case "image":
69
+ onEvent({
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;
95
+ case "table":
96
+ onEvent({
97
+ kind: "table",
98
+ columns: display.columns,
99
+ rows: display.rows,
100
+ rowCount: display.row_count,
101
+ shownCount: display.shown_count,
102
+ truncated: display.truncated,
103
+ source: fileName
104
+ });
105
+ break;
106
+ }
107
+ }
108
+ function asPlain(value) {
109
+ if (value instanceof Map) {
110
+ const obj = {};
111
+ for (const [k, v] of value.entries()) {
112
+ obj[String(k)] = asPlain(v);
113
+ }
114
+ return obj;
115
+ }
116
+ if (Array.isArray(value)) {
117
+ return value.map(asPlain);
118
+ }
119
+ return value;
120
+ }
121
+ function adaptTestCase(raw) {
122
+ const row = asPlain(raw) ?? {};
123
+ const line = row.line_number;
124
+ return {
125
+ name: String(row.name ?? ""),
126
+ outcome: String(row.outcome ?? "failed"),
127
+ lineNumber: typeof line === "number" ? line : null,
128
+ message: row.message == null ? null : String(row.message),
129
+ stdout: row.stdout == null ? null : String(row.stdout)
130
+ };
131
+ }
132
+ function deliverTestResult(result, onEvent, fileName) {
133
+ if (result.internal_error && result.error_type) {
134
+ onEvent({
135
+ kind: "error",
136
+ errorType: result.error_type,
137
+ message: result.error_message ?? "",
138
+ traceback: result.traceback ?? "",
139
+ lineNumber: result.line_number,
140
+ column: result.column,
141
+ fileName
142
+ });
143
+ onEvent({ kind: "done" });
144
+ return;
145
+ }
146
+ const tests = Array.isArray(result.tests) ? result.tests.map(adaptTestCase) : [];
147
+ onEvent({
148
+ kind: "testReport",
149
+ fileName,
150
+ passed: result.passed ?? 0,
151
+ failed: result.failed ?? 0,
152
+ skipped: result.skipped ?? 0,
153
+ errors: result.errors ?? 0,
154
+ tests
155
+ });
156
+ onEvent({ kind: "done" });
157
+ }
158
+
159
+ // src/common/interruptBuffer.ts
160
+ var INTERRUPT_SAB_BYTES = 1;
161
+ var INTERRUPT_SIGINT = 2;
162
+ function tryCreateInterruptBuffer() {
163
+ try {
164
+ return new SharedArrayBuffer(INTERRUPT_SAB_BYTES);
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+ function signalInterrupt(sab) {
170
+ Atomics.store(new Uint8Array(sab), 0, INTERRUPT_SIGINT);
171
+ }
172
+
173
+ // src/common/stdinBuffer.ts
174
+ var STDIN_SAB_BYTES = 64 * 1024;
175
+ var STDIN_STATE_INDEX = 0;
176
+ var STDIN_LENGTH_INDEX = 1;
177
+ var STDIN_PAYLOAD_OFFSET = 8;
178
+ var STDIN_STATE_LINE = 1;
179
+ var STDIN_STATE_EOF = 2;
180
+ function tryCreateStdinBuffer() {
181
+ try {
182
+ return new SharedArrayBuffer(STDIN_SAB_BYTES);
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ function writeStdinLine(sab, line) {
188
+ const state = new Int32Array(sab);
189
+ if (line === null) {
190
+ Atomics.store(state, STDIN_STATE_INDEX, STDIN_STATE_EOF);
191
+ Atomics.notify(state, STDIN_STATE_INDEX);
192
+ return;
193
+ }
194
+ const encoded = new TextEncoder().encode(line);
195
+ const max = sab.byteLength - STDIN_PAYLOAD_OFFSET;
196
+ const n = Math.min(encoded.length, max);
197
+ const bytes = new Uint8Array(sab, STDIN_PAYLOAD_OFFSET);
198
+ bytes.set(encoded.subarray(0, n));
199
+ Atomics.store(state, STDIN_LENGTH_INDEX, n);
200
+ Atomics.store(state, STDIN_STATE_INDEX, STDIN_STATE_LINE);
201
+ Atomics.notify(state, STDIN_STATE_INDEX);
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();
218
+ }
219
+ await this.initPromise;
220
+ }
221
+ async doInitialize() {
222
+ const indexUrl = this.resolveIndexUrl();
223
+ this.worker = this.spawn({
224
+ onMessage: (msg) => this.handleMessage(msg),
225
+ onError: (err) => this.rejectAllPending(err)
226
+ });
227
+ this.stdinBuffer = tryCreateStdinBuffer();
228
+ this.interruptBuffer = tryCreateInterruptBuffer();
229
+ await this.request(
230
+ {
231
+ type: "init",
232
+ indexUrl,
233
+ ...this.stdinBuffer ? { stdinBuffer: this.stdinBuffer } : {},
234
+ ...this.interruptBuffer ? { interruptBuffer: this.interruptBuffer } : {}
235
+ },
236
+ "ready"
237
+ );
238
+ }
239
+ async runFile(request, onEvent) {
240
+ await this.initialize();
241
+ this.live = { onEvent, fileName: request.fileName };
242
+ try {
243
+ const { result } = await this.request({ type: "runFile", ...request }, "result");
244
+ deliverRunResult(result, onEvent, request.fileName);
245
+ } finally {
246
+ this.live = null;
247
+ }
248
+ }
249
+ async replEval(request, onEvent) {
250
+ await this.initialize();
251
+ const { result } = await this.request({ type: "replEval", ...request }, "result");
252
+ deliverRunResult(result, onEvent, "<repl>");
253
+ }
254
+ async checkReplComplete(code) {
255
+ await this.initialize();
256
+ const { result } = await this.request({ type: "checkSyntax", code }, "syntax");
257
+ return {
258
+ status: result.status,
259
+ errorType: result.error_type,
260
+ message: result.message,
261
+ lineNumber: result.lineno,
262
+ offset: result.offset
263
+ };
264
+ }
265
+ async hasTests(code) {
266
+ await this.initialize();
267
+ const { result } = await this.request({ type: "hasTests", code }, "hasTests");
268
+ return result;
269
+ }
270
+ async ensurePackages(code) {
271
+ await this.initialize();
272
+ await this.request({ type: "loadPackages", code }, "packagesReady");
273
+ }
274
+ async ensurePytest() {
275
+ await this.initialize();
276
+ await this.request({ type: "loadPytest" }, "pytestReady");
277
+ }
278
+ async runTests(request, onEvent) {
279
+ await this.initialize();
280
+ const { result } = await this.request(
281
+ {
282
+ type: "runTests",
283
+ code: request.code,
284
+ fileName: request.fileName,
285
+ level: request.level
286
+ },
287
+ "testResult"
288
+ );
289
+ deliverTestResult(result, onEvent, request.fileName);
290
+ }
291
+ async staticAnalyze(request) {
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;
304
+ }
305
+ async reactorStep(reactorId, event) {
306
+ await this.initialize();
307
+ const { result } = await this.request(
308
+ { type: "reactorStep", reactorId, event },
309
+ "reactorFrame"
310
+ );
311
+ return result;
312
+ }
313
+ async reactorSeek(reactorId, index) {
314
+ await this.initialize();
315
+ const { result } = await this.request(
316
+ { type: "reactorSeek", reactorId, index },
317
+ "reactorFrame"
318
+ );
319
+ return result;
320
+ }
321
+ async reactorDispose(reactorId) {
322
+ await this.initialize();
323
+ await this.request({ type: "reactorDispose", reactorId }, "reactorDisposed");
324
+ }
325
+ interrupt() {
326
+ if (!this.interruptBuffer) {
327
+ return false;
328
+ }
329
+ signalInterrupt(this.interruptBuffer);
330
+ return true;
331
+ }
332
+ setStdinHandler(handler) {
333
+ this.stdinHandler = handler;
334
+ }
335
+ dispose() {
336
+ this.worker?.terminate();
337
+ this.worker = null;
338
+ this.initPromise = null;
339
+ this.stdinBuffer = null;
340
+ this.interruptBuffer = null;
341
+ this.live = null;
342
+ this.rejectAllPending(new Error("Runtime disposed"));
343
+ }
344
+ /** Send `msg` and resolve with the reply, asserting it is of type `expected`. */
345
+ async request(msg, expected) {
346
+ const worker = this.worker;
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;
359
+ }
360
+ handleMessage(msg) {
361
+ if (msg.type === "display") {
362
+ if (this.live) {
363
+ deliverDisplay(msg.payload, this.live.onEvent, this.live.fileName);
364
+ }
365
+ return;
366
+ }
367
+ if (msg.type === "stdinRequest") {
368
+ void this.handleStdinRequest();
369
+ return;
370
+ }
371
+ const pending = this.pending.get(msg.id);
372
+ if (!pending) {
373
+ return;
374
+ }
375
+ this.pending.delete(msg.id);
376
+ if (msg.type === "error") {
377
+ pending.reject(new Error(msg.message));
378
+ } else {
379
+ pending.resolve(msg);
380
+ }
381
+ }
382
+ rejectAllPending(err) {
383
+ for (const pending of this.pending.values()) {
384
+ pending.reject(err);
385
+ }
386
+ this.pending.clear();
387
+ }
388
+ async handleStdinRequest() {
389
+ let line = null;
390
+ try {
391
+ line = this.stdinHandler ? await this.stdinHandler() : null;
392
+ } catch {
393
+ line = null;
394
+ }
395
+ if (this.stdinBuffer) {
396
+ writeStdinLine(this.stdinBuffer, line);
397
+ }
398
+ }
399
+ };
400
+
401
+ // src/desktop/pyodideRuntime.ts
402
+ var DesktopPyodideRuntime = class extends WorkerPythonRuntime {
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);
414
+ }
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
+ return {
425
+ post: (msg) => worker.postMessage(msg),
426
+ terminate: () => void worker.terminate()
427
+ };
428
+ }
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
+ return {
491
+ id: "shadowing-builtin",
492
+ errorType: "Shadowing",
493
+ message: raw.message,
494
+ headline,
495
+ howToFix: [
496
+ `Pick a different name. Common patterns: \`my_${name}\`, \`${name}_value\`, \`${name}s\` (plural), or a more specific noun describing what this value represents.`
497
+ ],
498
+ fileName,
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
506
+ };
507
+ }
508
+
509
+ // src/common/analyzers/static/reassignmentExplainer.ts
510
+ function explainReassignment(raw, level, fileName) {
511
+ const name = raw.name_token ?? "this name";
512
+ const firstLine = raw.first_line_number ?? null;
513
+ const firstLineHint = firstLine !== null && firstLine > 0 ? ` (first assigned on line ${firstLine})` : "";
514
+ return {
515
+ id: "reassignment",
516
+ errorType: "Reassignment",
517
+ message: raw.message,
518
+ headline: `\`${name}\` is already assigned${firstLineHint}.`,
519
+ howToFix: level === "intermediate" ? [
520
+ `If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
521
+ `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}(...)\`.`
522
+ ] : [
523
+ `If you want a different value, give it a different name (e.g. \`${name}_doubled\`, \`new_${name}\`, \`${name}2\`).`,
524
+ `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.`
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
+ };
535
+ }
536
+
537
+ // src/common/analyzers/static/disallowedKeywordExplainer.ts
538
+ function explainDisallowedKeyword(raw, level, fileName) {
539
+ const keyword = raw.keyword === "nonlocal" ? "nonlocal" : "global";
540
+ const names = Array.isArray(raw.names) ? raw.names : [];
541
+ const formattedNames = names.map((n) => `\`${n}\``).join(", ");
542
+ const namesPhrase = formattedNames.length > 0 ? ` ${formattedNames}` : "";
543
+ const headline = `\`${keyword}\` is not allowed at the ${level} level.`;
544
+ const howToFix = [];
545
+ if (keyword === "global") {
546
+ howToFix.push(
547
+ `Pass${namesPhrase || " the value"} into this function as an argument instead of declaring it \`global\`.`,
548
+ `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)\`).`
549
+ );
550
+ } else {
551
+ howToFix.push(
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.`
554
+ );
555
+ }
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
+ };
580
+ function fallbackExplainer(raw, level, fileName) {
581
+ return {
582
+ id: raw.id,
583
+ errorType: raw.error_type || "StaticError",
584
+ message: raw.message,
585
+ headline: raw.message,
586
+ howToFix: [],
587
+ fileName,
588
+ lineNumber: raw.line_number,
589
+ column: raw.column,
590
+ nameToken: raw.name_token,
591
+ severity: "error",
592
+ raw: JSON.stringify(raw),
593
+ origin: "static",
594
+ level
595
+ };
596
+ }
597
+ function enrichStaticFindings(rawFindings, level, fileName) {
598
+ return rawFindings.map((raw) => {
599
+ const explainer = explainers[raw.id] ?? fallbackExplainer;
600
+ return explainer(raw, level, fileName);
601
+ });
602
+ }
603
+
604
+ // src/common/errors/pythonErrorParser.ts
605
+ var NON_USER_FRAME = [
606
+ "<exec>",
607
+ "/pll_vendor",
608
+ "site-packages",
609
+ "/lib/python",
610
+ "_pytest",
611
+ "pluggy"
612
+ ];
613
+ function userTracebackFrames(traceback) {
614
+ const frames = [];
615
+ for (const raw of traceback.split(/\r?\n/)) {
616
+ const m = raw.match(/^\s*File "([^"]+)", line (\d+)(?:, in (\S+))?/);
617
+ if (!m) continue;
618
+ const fileName = m[1];
619
+ if (NON_USER_FRAME.some((token) => fileName.includes(token))) continue;
620
+ frames.push({
621
+ fileName,
622
+ line: parseInt(m[2], 10),
623
+ functionName: m[3] && m[3] !== "<module>" ? m[3] : null
624
+ });
625
+ }
626
+ return frames;
627
+ }
628
+ function parsePythonError(rawTraceback) {
629
+ const traceback = rawTraceback.trimEnd();
630
+ const lines = traceback.split(/\r?\n/);
631
+ let errorType = "Error";
632
+ let message = traceback;
633
+ for (let i = lines.length - 1; i >= 0; i--) {
634
+ const line = lines[i];
635
+ const m = line.match(/^([A-Za-z_][A-Za-z0-9_.]*):\s?(.*)$/);
636
+ if (m && !line.startsWith(" ")) {
637
+ errorType = m[1];
638
+ message = m[2];
639
+ break;
640
+ }
641
+ if (line.match(/^[A-Za-z_][A-Za-z0-9_.]*$/) && !line.startsWith(" ")) {
642
+ errorType = line;
643
+ message = "";
644
+ break;
645
+ }
646
+ }
647
+ let fileName = null;
648
+ let lineNumber = null;
649
+ for (let i = lines.length - 1; i >= 0; i--) {
650
+ const line = lines[i];
651
+ const m = line.match(/^\s*File\s+"([^"]+)",\s+line\s+(\d+)/);
652
+ if (m) {
653
+ fileName = m[1];
654
+ lineNumber = parseInt(m[2], 10);
655
+ break;
656
+ }
657
+ }
658
+ let column = null;
659
+ if (lineNumber !== null) {
660
+ for (let i = 0; i < lines.length - 1; i++) {
661
+ if (/^\s*\^+\s*$/.test(lines[i + 1] ?? "")) {
662
+ const caretLine = lines[i + 1];
663
+ const caretIdx = caretLine.indexOf("^");
664
+ if (caretIdx >= 0) {
665
+ column = caretIdx;
666
+ }
667
+ }
668
+ }
669
+ }
670
+ let nameToken = null;
671
+ if (errorType === "NameError") {
672
+ const nm = message.match(/name '([^']+)' is not defined/);
673
+ if (nm) {
674
+ nameToken = nm[1];
675
+ }
676
+ }
677
+ return {
678
+ errorType,
679
+ message,
680
+ traceback,
681
+ fileName,
682
+ lineNumber,
683
+ column,
684
+ nameToken
685
+ };
686
+ }
687
+
688
+ // src/common/errors/nameErrorExplainer.ts
689
+ function explainNameError(parsed) {
690
+ const name = parsed.nameToken ?? "this name";
691
+ const didYouMean = extractDidYouMean(parsed.message);
692
+ const headline = `Python doesn't know what \`${name}\` means.`;
693
+ const howToFix = [
694
+ `Check the spelling of \`${name}\` (Python is case-sensitive).`,
695
+ `Make sure \`${name}\` is defined before this line runs.`,
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
+ );
702
+ }
703
+ return { headline, howToFix };
704
+ }
705
+ function extractDidYouMean(message) {
706
+ const m = message.match(/Did you mean:\s*([^?]+)\??/i);
707
+ if (!m) {
708
+ return [];
709
+ }
710
+ return m[1].split(/,|\bor\b/).map((s) => s.trim().replace(/^['"`]|['"`]$/g, "")).filter((s) => s.length > 0);
711
+ }
712
+
713
+ // src/common/analyzers/nameErrorAnalyzer.ts
714
+ var nameErrorAnalyzer = {
715
+ handles: ["NameError"],
716
+ analyze(input) {
717
+ const { parsedError, fileName, level } = input;
718
+ if (parsedError.errorType !== "NameError") {
719
+ return null;
720
+ }
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
+ }
738
+ };
739
+
740
+ // src/common/level.ts
741
+ var DEFAULT_LEVEL = "raw";
742
+ var HEADER_RE = /^#\s*level\s+([a-z]+)\s*$/;
743
+ var LEVEL_NAMES = [
744
+ "raw",
745
+ "beginner",
746
+ "intermediate",
747
+ "advanced"
748
+ ];
749
+ function parseLevel(source) {
750
+ const lines = source.split(/\r?\n/);
751
+ for (const raw of lines) {
752
+ const line = raw.trim();
753
+ if (line.length === 0) {
754
+ continue;
755
+ }
756
+ const match = HEADER_RE.exec(line);
757
+ if (!match) {
758
+ return DEFAULT_LEVEL;
759
+ }
760
+ const name = match[1];
761
+ for (const level of LEVEL_NAMES) {
762
+ if (name === level) return level;
763
+ }
764
+ return DEFAULT_LEVEL;
765
+ }
766
+ return DEFAULT_LEVEL;
767
+ }
768
+ function levelRejectsBoolAsNumber(level) {
769
+ return level === "beginner" || level === "intermediate";
770
+ }
771
+ function levelHasStaticChecks(level) {
772
+ return level === "beginner" || level === "intermediate";
773
+ }
774
+
775
+ // src/common/errors/typeCheckExplainer.ts
776
+ var FRIENDLY_TYPES = {
777
+ int: "a whole number",
778
+ float: "a number",
779
+ complex: "a complex number",
780
+ str: "a string",
781
+ bool: "`True` or `False`",
782
+ bytes: "bytes",
783
+ list: "a list",
784
+ dict: "a dictionary",
785
+ set: "a set",
786
+ tuple: "a tuple",
787
+ None: "`None`",
788
+ NoneType: "`None`"
789
+ };
790
+ function cleanTypeName(type) {
791
+ return type.startsWith("__main__.") ? type.slice("__main__.".length) : type;
792
+ }
793
+ function describeType(type) {
794
+ const name = cleanTypeName(type);
795
+ if (name === "None" || name === "NoneType") return "`None`";
796
+ const friendly = FRIENDLY_TYPES[name];
797
+ return friendly ? `${friendly} (\`${name}\`)` : `\`${name}\``;
798
+ }
799
+ function describeTypes(types) {
800
+ const described = types.map(describeType);
801
+ if (described.length === 0) return "a different type";
802
+ if (described.length === 1) return described[0];
803
+ return `${described.slice(0, -1).join(", ")} or ${described[described.length - 1]}`;
804
+ }
805
+ function boolAsNumberNote(expected, actual, level) {
806
+ if (actual !== "bool" || expected.length === 0) return null;
807
+ if (!expected.some((type) => type === "int" || type === "float")) return null;
808
+ if (level !== void 0 && !levelRejectsBoolAsNumber(level)) return null;
809
+ return "At `#level beginner` and `#level intermediate`, `True` and `False` are not accepted as numbers, even though Python counts them as `1` and `0`.";
810
+ }
811
+ function conversionFor(expected, actual) {
812
+ if (expected.length !== 1 || actual === null) return null;
813
+ const to = expected[0];
814
+ const numeric = actual === "int" || actual === "float" || actual === "bool";
815
+ if (to === "int" && (actual === "str" || actual === "float")) return "int";
816
+ if (to === "float" && actual === "str") return "float";
817
+ if (to === "str" && numeric) return "str";
818
+ return null;
819
+ }
820
+ var ELEMENT_RE = /^(item \d+|key .+|value of key .+|\[.+\]) of (.+)$/;
821
+ function membersPhrase(element) {
822
+ if (element.startsWith("the value for key")) return "every value in";
823
+ if (element.startsWith("key ")) return "every key in";
824
+ return "every item in";
825
+ }
826
+ function describeElement(element) {
827
+ if (element.startsWith("value of key ")) {
828
+ return `the value for key ${element.slice("value of key ".length)}`;
829
+ }
830
+ if (element.startsWith("[")) return "one of the values";
831
+ return element;
832
+ }
833
+ function parseTypeCheckMessage(message) {
834
+ const unknown = {
835
+ kind: "unknown",
836
+ name: null,
837
+ element: null,
838
+ actual: null,
839
+ expected: []
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());
851
+ }
852
+ } else {
853
+ const predicates = [
854
+ [/^(.*) is not an instance of (.+)$/, (m) => [m[2]]],
855
+ [/^(.*) is neither float or int$/, () => ["float"]],
856
+ [/^(.*) is not None$/, () => ["None"]],
857
+ [/^(.*) is not a ([A-Za-z_][A-Za-z0-9_]*)$/, (m) => [m[2]]]
858
+ ];
859
+ for (const [re, pick] of predicates) {
860
+ const m = first.match(re);
861
+ if (m) {
862
+ subject = m[1];
863
+ expected = pick(m);
864
+ break;
865
+ }
866
+ }
867
+ }
868
+ if (subject === null) return unknown;
869
+ let actual = null;
870
+ const withActual = subject.match(/^(.*) \(([^()]*)\)$/);
871
+ if (withActual) {
872
+ subject = withActual[1];
873
+ actual = withActual[2];
874
+ }
875
+ let element = null;
876
+ const elementMatch = subject.match(ELEMENT_RE);
877
+ if (elementMatch) {
878
+ element = describeElement(elementMatch[1]);
879
+ subject = elementMatch[2];
880
+ }
881
+ if (subject === "the return value") {
882
+ return { kind: "return", name: null, element, actual, expected };
883
+ }
884
+ const argument = subject.match(/^argument "(.+)"$/);
885
+ if (argument) {
886
+ return { kind: "argument", name: argument[1], element, actual, expected };
887
+ }
888
+ const assigned = subject.match(/^value assigned to (.+)$/);
889
+ if (assigned) {
890
+ return { kind: "variable", name: assigned[1], element, actual, expected };
891
+ }
892
+ return { ...unknown, element, actual, expected };
893
+ }
894
+ function explainTypeCheckError(message, functionName, level) {
895
+ const parsed = parseTypeCheckMessage(message);
896
+ const wanted = describeTypes(parsed.expected);
897
+ const owner = functionName ? `\`${functionName}\`` : "this function";
898
+ const named = parsed.name ? `\`${parsed.name}\`` : "a value";
899
+ const convert = conversionFor(parsed.expected, parsed.actual);
900
+ const boolNote = boolAsNumberNote(parsed.expected, parsed.actual, level);
901
+ const annotation = parsed.expected.length === 1 ? cleanTypeName(parsed.expected[0]) : null;
902
+ if (parsed.kind === "argument") {
903
+ if (parsed.element) {
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
+ );
922
+ }
923
+ return {
924
+ headline: `${owner} expects ${named} to be ${wanted}${got}.`,
925
+ howToFix
926
+ };
927
+ }
928
+ if (parsed.kind === "return") {
929
+ if (parsed.actual === "None" || parsed.actual === "NoneType") {
930
+ return {
931
+ headline: `${owner} should return ${wanted}, but it finished without returning a value.`,
932
+ howToFix: [
933
+ "Make sure every path through the function reaches a `return`.",
934
+ "An `if` with no `else` falls off the end, and Python then returns `None`.",
935
+ `Or annotate the return type as \`None\` if ${owner} is not meant to return anything.`
936
+ ]
937
+ };
938
+ }
939
+ const got = parsed.actual ? describeType(parsed.actual) : "something else";
940
+ const howToFix = [];
941
+ const wantsNone = parsed.expected.length === 1 && /^(None|NoneType)$/.test(parsed.expected[0]);
942
+ if (wantsNone) {
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.`);
948
+ }
949
+ if (boolNote) howToFix.push(boolNote);
950
+ if (annotation && parsed.actual) {
951
+ howToFix.push(
952
+ `Or change ${owner}'s return annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
953
+ );
954
+ }
955
+ return {
956
+ headline: `${owner} says it returns ${wanted}, but this line returns ${got}.`,
957
+ howToFix
958
+ };
959
+ }
960
+ if (parsed.kind === "variable") {
961
+ if (parsed.element) {
962
+ return {
963
+ headline: `${named} is annotated so ${membersPhrase(parsed.element)} it is ${wanted}, but ${parsed.element} is not.`,
964
+ howToFix: [`Look at ${parsed.element} of the value assigned to ${named}.`]
965
+ };
966
+ }
967
+ const got = parsed.actual ? describeType(parsed.actual) : "a different type";
968
+ const howToFix = [`Assign ${wanted} to ${named}.`];
969
+ if (boolNote) howToFix.push(boolNote);
970
+ if (convert) {
971
+ howToFix.push(`You can convert the value with \`${convert}(...)\`.`);
972
+ }
973
+ if (annotation && parsed.actual) {
974
+ howToFix.push(
975
+ `Or change ${named}'s annotation from \`${annotation}\` to \`${cleanTypeName(parsed.actual)}\`.`
976
+ );
977
+ }
978
+ return {
979
+ headline: `${named} is annotated as ${wanted}, but ${got} was assigned here.`,
980
+ howToFix
981
+ };
982
+ }
983
+ return {
984
+ headline: "A value does not match the type annotation written for it.",
985
+ howToFix: [
986
+ message.split(/\r?\n/)[0].trim(),
987
+ "Either pass a value of the annotated type, or change the annotation."
988
+ ]
989
+ };
990
+ }
991
+
992
+ // src/common/analyzers/typeCheckAnalyzer.ts
993
+ var HANDLED = ["TypeCheckError", "typeguard.TypeCheckError"];
994
+ function messageWithDetail(traceback, fallback) {
995
+ const lines = traceback.split(/\r?\n/);
996
+ for (let i = lines.length - 1; i >= 0; i--) {
997
+ const line = lines[i];
998
+ const marker = line.indexOf("TypeCheckError:");
999
+ if (line.startsWith(" ") || marker < 0) continue;
1000
+ const detail = [line.slice(marker + "TypeCheckError:".length).trim()];
1001
+ for (let j = i + 1; j < lines.length; j++) {
1002
+ if (lines[j].trim() === "") continue;
1003
+ if (!lines[j].startsWith(" ")) break;
1004
+ detail.push(lines[j]);
1005
+ }
1006
+ return detail.join("\n");
1007
+ }
1008
+ return fallback;
1009
+ }
1010
+ var typeCheckAnalyzer = {
1011
+ handles: HANDLED,
1012
+ analyze(input) {
1013
+ const { parsedError, fileName, level } = input;
1014
+ if (!HANDLED.includes(parsedError.errorType)) {
1015
+ return null;
1016
+ }
1017
+ const message = messageWithDetail(parsedError.traceback, parsedError.message);
1018
+ const parsed = parseTypeCheckMessage(message);
1019
+ const frames = userTracebackFrames(parsedError.traceback);
1020
+ const innermost = frames.length > 0 ? frames[frames.length - 1] : null;
1021
+ const blamed = parsed.kind === "argument" && frames.length >= 2 ? frames[frames.length - 2] : innermost;
1022
+ const explanation = explainTypeCheckError(
1023
+ message,
1024
+ innermost ? innermost.functionName : null,
1025
+ level
1026
+ );
1027
+ return {
1028
+ id: "type-check",
1029
+ // Not a real Python exception name; "TypeCheckError" means nothing to
1030
+ // a student, and "TypeError" would name a different Python error.
1031
+ errorType: "TypeMismatch",
1032
+ message,
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
1043
+ };
1044
+ }
1045
+ };
1046
+
1047
+ // src/common/analyzers/registry.ts
1048
+ var analyzers = [nameErrorAnalyzer, typeCheckAnalyzer];
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
+ }
1058
+ }
1059
+ return null;
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;
1068
+ }
1069
+ if (parsed.column === null && event.column !== null) {
1070
+ parsed.column = event.column;
1071
+ }
1072
+ if (parsed.fileName === null && event.fileName) {
1073
+ parsed.fileName = event.fileName;
1074
+ }
1075
+ return findRuntimeFinding(source, fileName, level, parsed);
1076
+ }
1077
+
1078
+ // src/common/pyodideRunner.ts
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 "";
1112
+ }
1113
+ return name.slice(dot).toLowerCase();
1114
+ }
1115
+ function isSafeBasename(name) {
1116
+ if (!name || name === "." || name === "..") {
1117
+ return false;
1118
+ }
1119
+ if (name.startsWith(".")) {
1120
+ return false;
1121
+ }
1122
+ if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
1123
+ return false;
1124
+ }
1125
+ return true;
1126
+ }
1127
+ function isMountableName(name) {
1128
+ return isSafeBasename(name) && MOUNT_EXTENSIONS.has(extensionOf(name));
1129
+ }
1130
+ function isWritebackName(name) {
1131
+ return isSafeBasename(name) && WRITEBACK_EXTENSIONS.has(extensionOf(name));
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;
1139
+ }
1140
+ for (let i = 0; i < data.byteLength; i++) {
1141
+ if (data[i] === 0) {
1142
+ return void 0;
1143
+ }
1144
+ }
1145
+ return new TextDecoder("utf-8").decode(data);
1146
+ }
1147
+ function selectMountableFiles(candidates) {
1148
+ const selected = [];
1149
+ let total = 0;
1150
+ for (const candidate of candidates) {
1151
+ if (selected.length >= MAX_FILES) {
1152
+ break;
1153
+ }
1154
+ if (!isMountableName(candidate.name)) {
1155
+ continue;
1156
+ }
1157
+ let text;
1158
+ if (typeof candidate.contents === "string") {
1159
+ if (utf8ByteLength(candidate.contents) > MAX_FILE_BYTES) {
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;
1172
+ }
1173
+ const size = utf8ByteLength(text);
1174
+ if (total + size > MAX_TOTAL_BYTES) {
1175
+ continue;
1176
+ }
1177
+ selected.push({ name: candidate.name, contents: text });
1178
+ total += size;
1179
+ }
1180
+ return selected;
1181
+ }
1182
+ function selectWritebackFiles(files) {
1183
+ const selected = [];
1184
+ let total = 0;
1185
+ for (const file of files) {
1186
+ if (selected.length >= MAX_FILES) {
1187
+ break;
1188
+ }
1189
+ if (!isWritebackName(file.name)) {
1190
+ continue;
1191
+ }
1192
+ const size = utf8ByteLength(file.contents);
1193
+ if (size > MAX_FILE_BYTES || total + size > MAX_TOTAL_BYTES) {
1194
+ continue;
1195
+ }
1196
+ if (file.contents.includes("\0")) {
1197
+ continue;
1198
+ }
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
+ }
1214
+ const candidates = [];
1215
+ for (const entry of entries) {
1216
+ if (!entry.isFile() || !isMountableName(entry.name)) {
1217
+ continue;
1218
+ }
1219
+ try {
1220
+ candidates.push({ name: entry.name, contents: await fs2.readFile(path3.join(folder, entry.name)) });
1221
+ } catch {
1222
+ }
1223
+ }
1224
+ return selectMountableFiles(candidates);
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)) {
1230
+ try {
1231
+ await fs2.writeFile(path3.join(folder, file.name), file.contents, "utf8");
1232
+ written.push(file.name);
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ return written;
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
+ }
1281
+ }
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
+ }
1313
+ try {
1314
+ await runtime.ensurePackages(source);
1315
+ } catch (err) {
1316
+ const message = errText(err);
1317
+ if (!/syntaxerror|invalid syntax/i.test(message)) {
1318
+ view.note(`Could not load libraries (${message}). Continuing.`);
1319
+ }
1320
+ }
1321
+ }
1322
+ async function maybeRunTests(runtime, view, source, fileName, level, onEvent) {
1323
+ try {
1324
+ if (!await runtime.hasTests(source)) {
1325
+ return;
1326
+ }
1327
+ await runtime.ensurePytest();
1328
+ } catch (err) {
1329
+ view.note(`Could not check for tests (${errText(err)}); skipping them.`);
1330
+ return;
1331
+ }
1332
+ await runtime.runTests({ code: source, fileName, sessionKey: fileName, level }, onEvent);
1333
+ }
1334
+ async function writeBack(runtime, view, file) {
1335
+ try {
1336
+ const changed = await runtime.collectWorkspaceFiles();
1337
+ if (changed.length === 0) {
1338
+ return;
1339
+ }
1340
+ const written = await writeBackSiblingFiles(file, changed);
1341
+ if (written.length > 0) {
1342
+ view.note(`Saved ${written.join(", ")} next to ${path4.basename(file)}.`);
1343
+ }
1344
+ } catch (err) {
1345
+ view.note(`Could not save files next to this script (${errText(err)}).`);
1346
+ }
1347
+ }
1348
+ function errText(err) {
1349
+ return err instanceof Error ? err.message : String(err);
1350
+ }
1351
+
1352
+ // src/cli/stdin.ts
1353
+ var readline = __toESM(require("node:readline"));
1354
+ function createLineReader() {
1355
+ let rl = null;
1356
+ let closed = false;
1357
+ const pending = [];
1358
+ const buffered = [];
1359
+ function ensure() {
1360
+ if (rl) return rl;
1361
+ rl = readline.createInterface({ input: process.stdin, terminal: false });
1362
+ rl.on("line", (line) => {
1363
+ const next = pending.shift();
1364
+ if (next) next(line);
1365
+ else buffered.push(line);
1366
+ });
1367
+ const finish = () => {
1368
+ closed = true;
1369
+ while (pending.length > 0) pending.shift()?.(null);
1370
+ };
1371
+ rl.on("close", finish);
1372
+ process.stdin.on("end", finish);
1373
+ return rl;
1374
+ }
1375
+ return {
1376
+ read() {
1377
+ if (buffered.length > 0) {
1378
+ return Promise.resolve(buffered.shift() ?? null);
1379
+ }
1380
+ if (closed) {
1381
+ return Promise.resolve(null);
1382
+ }
1383
+ ensure();
1384
+ return new Promise((resolve3) => pending.push(resolve3));
1385
+ },
1386
+ close() {
1387
+ closed = true;
1388
+ rl?.close();
1389
+ rl = null;
1390
+ }
1391
+ };
1392
+ }
1393
+
1394
+ // src/cli/view.ts
1395
+ var fs4 = __toESM(require("node:fs"));
1396
+ var path5 = __toESM(require("node:path"));
1397
+
1398
+ // src/common/analyzers/findingLocation.ts
1399
+ function findingLocation(finding) {
1400
+ if (finding.lineNumber === null) {
1401
+ return null;
1402
+ }
1403
+ if (finding.fileName === "<repl>" || finding.fileName === "<input>") {
1404
+ return null;
1405
+ }
1406
+ const label = finding.column !== null ? `${finding.fileName}:${finding.lineNumber}:${finding.column + 1}` : `${finding.fileName}:${finding.lineNumber}`;
1407
+ return {
1408
+ fileName: finding.fileName,
1409
+ line: finding.lineNumber,
1410
+ column: finding.column,
1411
+ label
1412
+ };
1413
+ }
1414
+
1415
+ // src/common/errorFormatter.ts
1416
+ function formatFriendlyError(finding) {
1417
+ const lines = [];
1418
+ lines.push(`${finding.errorType}: ${finding.headline}`);
1419
+ const location = findingLocation(finding);
1420
+ if (location) {
1421
+ lines.push(` at ${location.label}`);
1422
+ }
1423
+ if (finding.howToFix.length > 0) {
1424
+ lines.push("");
1425
+ lines.push("How to fix:");
1426
+ for (const item of finding.howToFix) {
1427
+ lines.push(` - ${item}`);
1428
+ }
1429
+ }
1430
+ return lines;
1431
+ }
1432
+
1433
+ // src/cli/view.ts
1434
+ var ESC = String.fromCharCode(27);
1435
+ var CliView = class {
1436
+ constructor(opts) {
1437
+ this.opts = opts;
1438
+ }
1439
+ opts;
1440
+ /** True once the program raised, so the exit code can reflect it. */
1441
+ sawError = false;
1442
+ /** Failing + erroring tests across all reports. */
1443
+ testFailures = 0;
1444
+ imageCount = 0;
1445
+ paint(code, text) {
1446
+ return this.opts.color ? `${ESC}[${code}m${text}${ESC}[0m` : text;
1447
+ }
1448
+ dim(text) {
1449
+ return this.paint("2", text);
1450
+ }
1451
+ red(text) {
1452
+ return this.paint("31", text);
1453
+ }
1454
+ green(text) {
1455
+ return this.paint("32", text);
1456
+ }
1457
+ /** PLL's own commentary. Always stderr, silenced by --quiet. */
1458
+ note(text) {
1459
+ if (!this.opts.quiet) {
1460
+ process.stderr.write(text + "\n");
1461
+ }
1462
+ }
1463
+ /** Something the user must see even under --quiet. */
1464
+ problem(text) {
1465
+ process.stderr.write(text + "\n");
1466
+ }
1467
+ /** Static-analysis findings, in the same words the editor uses. */
1468
+ findings(findings) {
1469
+ for (const finding of findings) {
1470
+ const lines = formatFriendlyError(finding);
1471
+ this.problem(this.red(lines[0]));
1472
+ for (const line of lines.slice(1)) {
1473
+ this.problem(line);
1474
+ }
1475
+ this.problem("");
1476
+ }
1477
+ }
1478
+ /** A friendly finding for a runtime error, in place of the traceback. */
1479
+ runtimeFinding(finding) {
1480
+ this.sawError = true;
1481
+ const lines = formatFriendlyError(finding);
1482
+ this.problem(this.red(lines[0]));
1483
+ for (const line of lines.slice(1)) {
1484
+ this.problem(line);
1485
+ }
1486
+ }
1487
+ handle(event) {
1488
+ switch (event.kind) {
1489
+ case "stdout":
1490
+ process.stdout.write(event.text);
1491
+ break;
1492
+ case "stderr":
1493
+ process.stderr.write(event.text);
1494
+ break;
1495
+ case "result":
1496
+ if (event.repr !== null && event.repr !== void 0) {
1497
+ process.stdout.write(event.repr + "\n");
1498
+ }
1499
+ break;
1500
+ case "image":
1501
+ this.image(event.svg, event.width, event.height);
1502
+ break;
1503
+ case "table":
1504
+ process.stdout.write(renderTable(event) + "\n");
1505
+ break;
1506
+ case "reactor": {
1507
+ const which = event.title === "reactor" ? "reactor" : `reactor "${event.title}"`;
1508
+ this.note(
1509
+ this.dim(`[${which} needs the editor's interactions panel; not run here]`)
1510
+ );
1511
+ break;
1512
+ }
1513
+ case "testReport":
1514
+ this.testReport(event);
1515
+ break;
1516
+ case "error": {
1517
+ this.sawError = true;
1518
+ const body = event.traceback || `${event.errorType}: ${event.message}`;
1519
+ process.stderr.write(this.red(body.trimEnd()) + "\n");
1520
+ break;
1521
+ }
1522
+ case "done":
1523
+ break;
1524
+ }
1525
+ }
1526
+ image(svg, width, height) {
1527
+ this.imageCount += 1;
1528
+ const dir = this.opts.saveImagesDir;
1529
+ if (!dir) {
1530
+ this.note(
1531
+ this.dim(`[image ${width}x${height}; pass --save-images to write it out]`)
1532
+ );
1533
+ return;
1534
+ }
1535
+ const target = path5.join(dir, `image-${this.imageCount}.svg`);
1536
+ try {
1537
+ fs4.mkdirSync(dir, { recursive: true });
1538
+ fs4.writeFileSync(target, svg, "utf8");
1539
+ this.note(this.dim(`[image ${width}x${height} -> ${target}]`));
1540
+ } catch (err) {
1541
+ this.problem(`Could not write ${target}: ${errText2(err)}`);
1542
+ }
1543
+ }
1544
+ testReport(event) {
1545
+ const bad = event.failed + event.errors;
1546
+ this.testFailures += bad;
1547
+ const summary = `${event.passed} passed` + (event.failed ? `, ${event.failed} failed` : "") + (event.errors ? `, ${event.errors} errored` : "") + (event.skipped ? `, ${event.skipped} skipped` : "");
1548
+ this.problem((bad === 0 ? this.green("tests: ") : this.red("tests: ")) + summary);
1549
+ for (const test of event.tests) {
1550
+ if (test.outcome === "passed") {
1551
+ this.note(this.dim(` ok ${test.name}`));
1552
+ continue;
1553
+ }
1554
+ const where = test.lineNumber === null ? "" : ` (line ${test.lineNumber})`;
1555
+ this.problem(this.red(` ${test.outcome.toUpperCase()} ${test.name}${where}`));
1556
+ for (const line of (test.message ?? "").split("\n")) {
1557
+ if (line.trim()) {
1558
+ this.problem(" " + line);
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ };
1564
+ function errText2(err) {
1565
+ return err instanceof Error ? err.message : String(err);
1566
+ }
1567
+ function renderTable(table) {
1568
+ const widths = table.columns.map(
1569
+ (name, i) => Math.max(name.length, ...table.rows.map((row) => (row[i] ?? "").length), 0)
1570
+ );
1571
+ const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
1572
+ const out = [
1573
+ line(table.columns),
1574
+ widths.map((w) => "-".repeat(w)).join(" ").trimEnd()
1575
+ ];
1576
+ for (const row of table.rows) {
1577
+ out.push(line(table.columns.map((_, i) => row[i] ?? "")));
1578
+ }
1579
+ out.push(
1580
+ table.truncated ? `(${table.shownCount} of ${table.rowCount} rows)` : `(${table.rowCount} row${table.rowCount === 1 ? "" : "s"})`
1581
+ );
1582
+ return out.join("\n");
1583
+ }
1584
+
1585
+ // src/cli/main.ts
1586
+ var USAGE = `pll - run Python with PLL's language levels, outside the editor
1587
+
1588
+ pll <file.py> [options]
1589
+
1590
+ The level comes from the file's own \`#level\` line, exactly as in the
1591
+ editor; there is deliberately no flag to override it, so a file behaves the
1592
+ same everywhere.
1593
+
1594
+ Options
1595
+ --no-tests do not run the file's \`test_*\` functions first
1596
+ --save-images <dir> write pictures there as .svg (they cannot be drawn
1597
+ in a terminal); without this, each one prints a note
1598
+ -q, --quiet only the program's own output
1599
+ --no-color never use ANSI colour
1600
+ -h, --help this message
1601
+ -v, --version print the version
1602
+
1603
+ The program's own stdout is the only thing on stdout, so it can be piped;
1604
+ everything PLL says about the run goes to stderr.
1605
+
1606
+ Exit codes
1607
+ 0 ran, tests passed 2 level checks blocked it
1608
+ 1 the program raised 3 a test failed
1609
+ `;
1610
+ function parseArgs(argv) {
1611
+ const args = {
1612
+ runTests: true,
1613
+ quiet: false,
1614
+ // Colour only when stderr is a terminal, and never when NO_COLOR is set.
1615
+ color: process.stderr.isTTY === true && !process.env.NO_COLOR,
1616
+ help: false,
1617
+ version: false
1618
+ };
1619
+ for (let i = 0; i < argv.length; i++) {
1620
+ const arg = argv[i];
1621
+ if (arg === "--no-tests") args.runTests = false;
1622
+ else if (arg === "-q" || arg === "--quiet") args.quiet = true;
1623
+ else if (arg === "--no-color") args.color = false;
1624
+ else if (arg === "-h" || arg === "--help") args.help = true;
1625
+ else if (arg === "-v" || arg === "--version") args.version = true;
1626
+ else if (arg === "--save-images") {
1627
+ const dir = argv[++i];
1628
+ if (dir === void 0) {
1629
+ args.error = "--save-images needs a directory";
1630
+ return args;
1631
+ }
1632
+ args.saveImagesDir = dir;
1633
+ } else if (arg.startsWith("-")) {
1634
+ args.error = `unknown option ${arg}`;
1635
+ return args;
1636
+ } else if (args.file === void 0) {
1637
+ args.file = arg;
1638
+ } else {
1639
+ args.error = `unexpected extra argument ${arg}`;
1640
+ return args;
1641
+ }
1642
+ }
1643
+ if (!args.help && !args.version && args.file === void 0) {
1644
+ args.error = "no file given";
1645
+ }
1646
+ if (args.file !== void 0 && !args.file.endsWith(".py")) {
1647
+ args.error = `${args.file} is not a .py file`;
1648
+ }
1649
+ return args;
1650
+ }
1651
+ async function main(argv) {
1652
+ const args = parseArgs(argv);
1653
+ if (args.help) {
1654
+ process.stdout.write(USAGE);
1655
+ return EXIT.ok;
1656
+ }
1657
+ if (args.version) {
1658
+ process.stdout.write("0.1.0\n");
1659
+ return EXIT.ok;
1660
+ }
1661
+ if (args.error !== void 0) {
1662
+ process.stderr.write(`pll: ${args.error}
1663
+
1664
+ ${USAGE}`);
1665
+ return EXIT.usage;
1666
+ }
1667
+ const view = new CliView({
1668
+ saveImagesDir: args.saveImagesDir,
1669
+ quiet: args.quiet,
1670
+ color: args.color
1671
+ });
1672
+ const runtime = createCliRuntime();
1673
+ const reader = createLineReader();
1674
+ runtime.setStdinHandler(() => reader.read());
1675
+ let interrupted = false;
1676
+ const onSigint = () => {
1677
+ if (interrupted) {
1678
+ process.stderr.write("\npll: giving up.\n");
1679
+ process.exit(130);
1680
+ }
1681
+ interrupted = true;
1682
+ if (!runtime.interrupt()) {
1683
+ process.stderr.write("\npll: cannot interrupt; press Ctrl+C again to quit.\n");
1684
+ }
1685
+ };
1686
+ process.on("SIGINT", onSigint);
1687
+ try {
1688
+ return await runFile(runtime, view, {
1689
+ file: path6.resolve(args.file),
1690
+ runTests: args.runTests
1691
+ });
1692
+ } catch (err) {
1693
+ view.problem(`pll: ${err instanceof Error ? err.message : String(err)}`);
1694
+ return EXIT.usage;
1695
+ } finally {
1696
+ process.off("SIGINT", onSigint);
1697
+ reader.close();
1698
+ runtime.dispose();
1699
+ }
1700
+ }
1701
+
1702
+ // src/cli/bin.ts
1703
+ main(process.argv.slice(2)).then(
1704
+ (code) => process.exit(code),
1705
+ (err) => {
1706
+ process.stderr.write(`pll: ${err instanceof Error ? err.stack : String(err)}
1707
+ `);
1708
+ process.exit(EXIT.usage);
1709
+ }
1710
+ );
1711
+ //# sourceMappingURL=cli.cjs.map