Skip to content

Commit 71fdabc

Browse files
committed
Add import format and change build format to iife
1 parent 820a231 commit 71fdabc

9 files changed

Lines changed: 161 additions & 26 deletions

File tree

rollup.config.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ const config = [
5454
{
5555
input: "src/pyodide/evaluators/full.ts",
5656
output: {
57-
file: "dist/pyodide-evaluator-full.cjs",
58-
format: "cjs",
57+
file: "dist/pyodide-evaluator-full.js",
58+
format: "iife",
5959
name: "PyodideEvaluatorFull",
6060
sourcemap: true,
6161
inlineDynamicImports: true,
@@ -65,8 +65,8 @@ const config = [
6565
{
6666
input: "src/pyodide/evaluators/chapter1.ts",
6767
output: {
68-
file: "dist/pyodide-evaluator-1.cjs",
69-
format: "cjs",
68+
file: "dist/pyodide-evaluator-1.js",
69+
format: "iife",
7070
name: "PyodideEvaluator1",
7171
sourcemap: true,
7272
inlineDynamicImports: true,
@@ -76,8 +76,8 @@ const config = [
7676
{
7777
input: "src/pyodide/evaluators/chapter2.ts",
7878
output: {
79-
file: "dist/pyodide-evaluator-2.cjs",
80-
format: "cjs",
79+
file: "dist/pyodide-evaluator-2.js",
80+
format: "iife",
8181
name: "PyodideEvaluator2",
8282
sourcemap: true,
8383
inlineDynamicImports: true,
@@ -87,8 +87,8 @@ const config = [
8787
{
8888
input: "src/pyodide/evaluators/chapter3.ts",
8989
output: {
90-
file: "dist/pyodide-evaluator-3.cjs",
91-
format: "cjs",
90+
file: "dist/pyodide-evaluator-3.js",
91+
format: "iife",
9292
name: "PyodideEvaluator3",
9393
sourcemap: true,
9494
inlineDynamicImports: true,
@@ -98,8 +98,8 @@ const config = [
9898
{
9999
input: "src/pyodide/evaluators/chapter4.ts",
100100
output: {
101-
file: "dist/pyodide-evaluator-4.cjs",
102-
format: "cjs",
101+
file: "dist/pyodide-evaluator-4.js",
102+
format: "iife",
103103
name: "PyodideEvaluator4",
104104
sourcemap: true,
105105
inlineDynamicImports: true,

src/pyodide/PyodideEvaluator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ConductorError } from "@sourceacademy/conductor/common";
12
import { BasicEvaluator, IRunnerPlugin } from "@sourceacademy/conductor/runner";
23
import type { PyodideInterface } from "pyodide";
34
import { parse } from "../parser/parser-adapter";
@@ -59,8 +60,13 @@ if missing:
5960
}
6061

6162
// --- Execute the (possibly rewritten) code ---
62-
const output = await pyodide.runPythonAsync(code);
63-
this.conductor.sendOutput(output);
63+
try {
64+
const output = await pyodide.runPythonAsync(code);
65+
this.conductor.sendResult(output);
66+
} catch (err: unknown) {
67+
const message = err instanceof Error ? err.message : String(err);
68+
this.conductor.sendError(new ConductorError(message));
69+
}
6470
}
6571
}
6672

src/pyodide/evaluators/chapter1.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { IRunnerPlugin } from "@sourceacademy/conductor/runner";
1+
import { initialise, IRunnerPlugin } from "@sourceacademy/conductor/runner";
22
import { ChapterPyodideEvaluator } from "../PyodideEvaluator";
33

4-
export default class PyodideEvaluator1 extends ChapterPyodideEvaluator {
4+
class PyodideEvaluator1 extends ChapterPyodideEvaluator {
55
constructor(conductor: IRunnerPlugin) {
66
super(conductor, 1);
77
}
88
}
9+
10+
initialise(PyodideEvaluator1);

src/pyodide/evaluators/chapter2.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { IRunnerPlugin } from "@sourceacademy/conductor/runner";
1+
import { initialise, IRunnerPlugin } from "@sourceacademy/conductor/runner";
22
import { ChapterPyodideEvaluator } from "../PyodideEvaluator";
33

4-
export default class PyodideEvaluator2 extends ChapterPyodideEvaluator {
4+
class PyodideEvaluator2 extends ChapterPyodideEvaluator {
55
constructor(conductor: IRunnerPlugin) {
66
super(conductor, 2);
77
}
88
}
9+
10+
initialise(PyodideEvaluator2);

src/pyodide/evaluators/chapter3.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { IRunnerPlugin } from "@sourceacademy/conductor/runner";
1+
import { initialise, IRunnerPlugin } from "@sourceacademy/conductor/runner";
22
import { ChapterPyodideEvaluator } from "../PyodideEvaluator";
33

4-
export default class PyodideEvaluator3 extends ChapterPyodideEvaluator {
4+
class PyodideEvaluator3 extends ChapterPyodideEvaluator {
55
constructor(conductor: IRunnerPlugin) {
66
super(conductor, 3);
77
}
88
}
9+
10+
initialise(PyodideEvaluator3);

src/pyodide/evaluators/chapter4.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { IRunnerPlugin } from "@sourceacademy/conductor/runner";
1+
import { initialise, IRunnerPlugin } from "@sourceacademy/conductor/runner";
22
import { ChapterPyodideEvaluator } from "../PyodideEvaluator";
33

4-
export default class PyodideEvaluator4 extends ChapterPyodideEvaluator {
4+
class PyodideEvaluator4 extends ChapterPyodideEvaluator {
55
constructor(conductor: IRunnerPlugin) {
66
super(conductor, 4);
77
}
88
}
9+
10+
initialise(PyodideEvaluator4);

src/pyodide/evaluators/full.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { IRunnerPlugin } from "@sourceacademy/conductor/runner";
1+
import { initialise, IRunnerPlugin } from "@sourceacademy/conductor/runner";
22
import PyodideEvaluator from "../PyodideEvaluator";
33

4-
export default class PyodideEvaluatorFull extends PyodideEvaluator {
4+
class PyodideEvaluatorFull extends PyodideEvaluator {
55
constructor(conductor: IRunnerPlugin) {
66
super(conductor);
77
}
88

99
protected validateChunk(_chunk: string): void {}
1010
}
11+
12+
initialise(PyodideEvaluatorFull);

src/pyodide/importAnalyzer.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import type { PyodideInterface } from "pyodide";
1111

1212
export interface TorchImportInfo {
13+
/** "import" for bare `import torch`, "from" for `from torch import ...` */
14+
type: "import" | "from";
1315
/** Full module path, e.g. "torch" or "torch.nn" */
1416
module: string;
1517
/** Imported names with optional aliases */
@@ -26,7 +28,7 @@ const ANALYZE_IMPORTS_PY = `
2628
import ast as _ast, json as _json
2729
2830
def _sa_analyze_imports(source):
29-
"""Parse source and return JSON array of from-import info."""
31+
"""Parse source and return JSON array of import info (both 'import' and 'from ... import')."""
3032
try:
3133
tree = _ast.parse(source)
3234
except SyntaxError:
@@ -35,13 +37,22 @@ def _sa_analyze_imports(source):
3537
for node in _ast.walk(tree):
3638
if isinstance(node, _ast.ImportFrom) and node.module:
3739
result.append({
40+
"type": "from",
3841
"module": node.module,
3942
"names": [
4043
{"name": a.name, "alias": a.asname}
4144
for a in node.names
4245
],
4346
"line": node.lineno,
4447
})
48+
elif isinstance(node, _ast.Import):
49+
for a in node.names:
50+
result.append({
51+
"type": "import",
52+
"module": a.name,
53+
"names": [{"name": a.name, "alias": a.asname}],
54+
"line": node.lineno,
55+
})
4556
return _json.dumps(result)
4657
`;
4758

@@ -111,13 +122,33 @@ export async function getNonTorchImportRoots(
111122
/**
112123
* Generates Python assignment code that replaces a torch import statement.
113124
*
114-
* Example:
125+
* Examples:
126+
* import torch → torch = __sa_import_torch
127+
* import torch as t → t = __sa_import_torch
128+
* import torch.nn → torch = __sa_import_torch
115129
* from torch.nn import Linear as L, Conv2d
116-
* → L = __sa_import_torch.nn.Linear
117-
* Conv2d = __sa_import_torch.nn.Conv2d
130+
* → L = __sa_import_torch.nn.Linear
131+
* Conv2d = __sa_import_torch.nn.Conv2d
118132
*/
119133
function generateReplacement(imp: TorchImportInfo): string {
120134
const injected = "__sa_import_torch";
135+
136+
if (imp.type === "import") {
137+
// `import torch` or `import torch as t` or `import torch.nn`
138+
const alias = imp.names[0].alias;
139+
if (alias) {
140+
// import torch as t → t = __sa_import_torch
141+
// import torch.nn as nn → nn = __sa_import_torch.nn
142+
const subparts = imp.module.split(".").slice(1);
143+
const rhs = subparts.length > 0 ? `${injected}.${subparts.join(".")}` : injected;
144+
return `${alias} = ${rhs}`;
145+
}
146+
// import torch → torch = __sa_import_torch
147+
// import torch.nn → torch = __sa_import_torch (Python binds the top-level name)
148+
return `torch = ${injected}`;
149+
}
150+
151+
// from torch.nn import Linear as L, Conv2d
121152
const subparts = imp.module.split(".").slice(1);
122153
const base = subparts.length > 0 ? `${injected}.${subparts.join(".")}` : injected;
123154

src/tests/import-analyzer.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,37 @@ describe("detectTorchImports", () => {
5353
expect(result[1].module).toBe("torch.nn");
5454
});
5555

56+
test("detects bare `import torch`", async () => {
57+
const result = await detectTorchImports(pyodide, "import torch\nx = 1\n");
58+
expect(result).toHaveLength(1);
59+
expect(result[0].type).toBe("import");
60+
expect(result[0].module).toBe("torch");
61+
expect(result[0].names).toEqual([{ name: "torch", alias: null }]);
62+
});
63+
64+
test("detects `import torch as t`", async () => {
65+
const result = await detectTorchImports(pyodide, "import torch as t\nx = 1\n");
66+
expect(result).toHaveLength(1);
67+
expect(result[0].type).toBe("import");
68+
expect(result[0].module).toBe("torch");
69+
expect(result[0].names).toEqual([{ name: "torch", alias: "t" }]);
70+
});
71+
72+
test("detects `import torch.nn`", async () => {
73+
const result = await detectTorchImports(pyodide, "import torch.nn\nx = 1\n");
74+
expect(result).toHaveLength(1);
75+
expect(result[0].type).toBe("import");
76+
expect(result[0].module).toBe("torch.nn");
77+
});
78+
79+
test("detects mix of bare import and from-import", async () => {
80+
const src = "import torch\nfrom torch.nn import Linear\nx = 1\n";
81+
const result = await detectTorchImports(pyodide, src);
82+
expect(result).toHaveLength(2);
83+
const types = result.map(r => r.type).sort();
84+
expect(types).toEqual(["from", "import"]);
85+
});
86+
5687
test("ignores non-torch imports", async () => {
5788
const result = await detectTorchImports(pyodide, "from math import sqrt\nx = 1\n");
5889
expect(result).toHaveLength(0);
@@ -97,6 +128,12 @@ describe("getNonTorchImportRoots", () => {
97128
const roots = await getNonTorchImportRoots(pyodide, "from os.path import join\nx = 1\n");
98129
expect(roots).toEqual(new Set(["os"]));
99130
});
131+
132+
test("returns non-torch roots for bare import statements", async () => {
133+
const src = "import numpy\nimport torch\nx = 1\n";
134+
const roots = await getNonTorchImportRoots(pyodide, src);
135+
expect(roots).toEqual(new Set(["numpy"]));
136+
});
100137
});
101138

102139
// ---------------------------------------------------------------------------
@@ -157,6 +194,57 @@ describe("rewriteTorchImports", () => {
157194
expect(code).toContain("relu = __sa_import_torch.nn.functional.relu");
158195
});
159196

197+
test("rewrites bare `import torch`", async () => {
198+
const { code, hasTorch } = await rewriteTorchImports(
199+
pyodide,
200+
"import torch\nx = torch.tensor([1, 2, 3])\n",
201+
);
202+
expect(hasTorch).toBe(true);
203+
expect(code).toContain("torch = __sa_import_torch");
204+
expect(code).not.toContain("import torch");
205+
expect(code).toContain("x = torch.tensor([1, 2, 3])");
206+
});
207+
208+
test("rewrites `import torch as t`", async () => {
209+
const { code, hasTorch } = await rewriteTorchImports(
210+
pyodide,
211+
"import torch as t\nx = t.tensor([1])\n",
212+
);
213+
expect(hasTorch).toBe(true);
214+
expect(code).toContain("t = __sa_import_torch");
215+
expect(code).toContain("x = t.tensor([1])");
216+
});
217+
218+
test("rewrites `import torch.nn as nn`", async () => {
219+
const { code, hasTorch } = await rewriteTorchImports(
220+
pyodide,
221+
"import torch.nn as nn\nx = nn.Linear(3, 2)\n",
222+
);
223+
expect(hasTorch).toBe(true);
224+
expect(code).toContain("nn = __sa_import_torch.nn");
225+
expect(code).toContain("x = nn.Linear(3, 2)");
226+
});
227+
228+
test("rewrites `import torch.nn` (no alias)", async () => {
229+
const { code, hasTorch } = await rewriteTorchImports(
230+
pyodide,
231+
"import torch.nn\nx = torch.nn.Linear(3, 2)\n",
232+
);
233+
expect(hasTorch).toBe(true);
234+
expect(code).toContain("torch = __sa_import_torch");
235+
expect(code).toContain("x = torch.nn.Linear(3, 2)");
236+
});
237+
238+
test("rewrites mix of bare import and from-import", async () => {
239+
const src = "import torch\nfrom torch.nn import Linear\nx = torch.tensor(1)\ny = Linear(3, 2)\n";
240+
const { code, hasTorch } = await rewriteTorchImports(pyodide, src);
241+
expect(hasTorch).toBe(true);
242+
expect(code).toContain("torch = __sa_import_torch");
243+
expect(code).toContain("Linear = __sa_import_torch.nn.Linear");
244+
expect(code).not.toMatch(/^import torch$/m);
245+
expect(code).not.toContain("from torch");
246+
});
247+
160248
test("handles full Python body that py-slang cannot parse", async () => {
161249
const src = "from torch import tensor\nx = tensor([1, 2, 3]).tolist()\nprint(x)\n";
162250
const { code, hasTorch } = await rewriteTorchImports(pyodide, src);

0 commit comments

Comments
 (0)