/ concept-collection / jupyterlite-numbl-kernel
Sign in
concept-collection / jupyterlite-numbl-kernel
Sync .m workspace files from the notebook directory into the session
Before each execute_request, .m files in the notebook's directory (read via the JupyterLite contents manager) are written into the numbl session's VFS. numbl rescans its working directory on every execution, so named functions defined in these files are callable from cells immediately, and edits made in the Jupyter editor take effect on the next cell run. This works around the numbl REPL's restriction against defining named functions directly in a cell. Demo: demo/content/statsutils.m + a cell in 01-intro.ipynb that calls it.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 6b31a9ad1166 parent 557a670 Browse files
5 changed files+208−117
README.mdmodified+18−10View file
@@ -8,8 +8,10 @@ The language engine is [numbl](https://github.com/flatironinstitute/numbl), an
88 open-source MATLAB-syntax implementation in TypeScript. Each kernel runs a
99 numbl session in a Web Worker in the page: variables persist across cells,
1010 console output streams into the running cell, MATLAB plotting commands render
11-as figures in cell outputs (including interactive 3-D), and the `mip` package
12-manager can install MATLAB-syntax packages from GitHub — all client-side.
11+as figures in cell outputs (including interactive 3-D), the `mip` package
12+manager can install MATLAB-syntax packages from GitHub, and `.m` files next
13+to the notebook are part of the workspace (named functions, called from
14+cells) — all client-side.
1315
1416 **Demo site:**
1517 <https://concept-collection.github.io/jupyterlite-numbl-kernel/> (deployed
@@ -27,11 +29,14 @@ link, and executable by anyone with a browser.
2729 Three small pieces, all in this repo:
2830
2931 - **Kernel** (`src/kernel.ts`) — implements JupyterLite's `BaseKernel` from
30- `@jupyterlite/services`. `execute_request` forwards the cell source to a
31- numbl session (`createNumblSession` / `session.execute` from
32- `numbl/browser`, a Web Worker that numbl manages). Output streams back as
33- `stream` messages; the run's plot instructions are published as
34- `display_data` with the mime type `application/vnd.numbl.figure+json`.
32+ `@jupyterlite/services`. Before each `execute_request`, `.m` files in the
33+ notebook's directory (read via the JupyterLite contents manager) are
34+ synced into the numbl session; the cell source then runs against the
35+ session's persistent workspace (`createNumblSession` /
36+ `session.execute` from `numbl/browser`, a Web Worker that numbl manages).
37+ Output streams back as `stream` messages; the run's plot instructions are
38+ published as `display_data` with the mime type
39+ `application/vnd.numbl.figure+json`.
3540 - **Figure renderer** (`src/mime.tsx`) — a JupyterLab mime renderer for that
3641 mime type: it replays the instructions through numbl's figures reducer and
3742 mounts numbl's React `FigureView` (from `numbl/graphics`). Outputs are
@@ -63,11 +68,14 @@ deploys it to GitHub Pages.
6368 figures its own commands produce; `hold on` does not span cells.
6469 - **Named function definitions are not supported inside cells** (a numbl
6570 REPL limitation) — anonymous functions work; named functions belong in
66- `.m` files.
71+ `.m` files next to the notebook (see `demo/content/statsutils.m`), which
72+ this kernel syncs into the session automatically.
73+- The `.m`-file sync is **one-way**: deleting a `.m` file from the file
74+ browser leaves its function defined until the kernel restarts, and files
75+ written by cell code (e.g. via `fopen`) don't appear back in the file
76+ browser.
6777 - **uihtml** components render display-only; the MATLAB↔HTML event bridge
6878 is not wired into outputs yet.
69-- Notebook files from the JupyterLite contents (e.g. sibling `.m` files)
70- are not yet synced into the numbl session's virtual filesystem.
7179 - numbl itself is not MATLAB: it covers a large, tested subset of the
7280 language and toolbox surface. See the
7381 [numbl repo](https://github.com/flatironinstitute/numbl) for scope.
demo/content/01-intro.ipynbmodified+119−105View file
@@ -1,107 +1,121 @@
11 {
2- "cells": [
3- {
4- "cell_type": "markdown",
5- "id": "intro-title",
6- "metadata": {},
7- "source": [
8- "# MATLAB syntax, entirely in your browser\n",
9- "\n",
10- "This notebook runs on the **numbl kernel for JupyterLite**. Every cell executes\n",
11- "in a Web Worker in *your* browser tab — there is no server and no kernel\n",
12- "process behind this page, and nothing to install.\n",
13- "\n",
14- "The engine is [numbl](https://github.com/flatironinstitute/numbl), an\n",
15- "open-source MATLAB-syntax language implementation in TypeScript.\n",
16- "\n",
17- "Run the cells below with **Shift+Enter**."
18- ]
19- },
20- {
21- "cell_type": "code",
22- "execution_count": null,
23- "id": "intro-matrix",
24- "metadata": {},
25- "outputs": [],
26- "source": [
27- "A = [4 2 1; 2 5 3; 1 3 6]"
28- ]
29- },
30- {
31- "cell_type": "code",
32- "execution_count": null,
33- "id": "intro-solve",
34- "metadata": {},
35- "outputs": [],
36- "source": [
37- "% Variables persist across cells; solve a linear system with backslash\n",
38- "b = [1; 2; 3];\n",
39- "x = A \\ b\n",
40- "residual = norm(A*x - b)"
41- ]
42- },
43- {
44- "cell_type": "code",
45- "execution_count": null,
46- "id": "intro-indexing",
47- "metadata": {},
48- "outputs": [],
49- "source": [
50- "% MATLAB indexing: rows, and logical masks\n",
51- "A(2, :)\n",
52- "A(A > 3)'"
53- ]
54- },
55- {
56- "cell_type": "code",
57- "execution_count": null,
58- "id": "intro-loop",
59- "metadata": {},
60- "outputs": [],
61- "source": [
62- "total = 0;\n",
63- "for k = 1:10\n",
64- " total = total + k^2;\n",
65- "end\n",
66- "fprintf('sum of squares 1..10 = %d\\n', total);"
67- ]
68- },
69- {
70- "cell_type": "code",
71- "execution_count": null,
72- "id": "intro-anon",
73- "metadata": {},
74- "outputs": [],
75- "source": [
76- "% Anonymous functions work in cells (named functions belong in .m files)\n",
77- "f = @(t) exp(-t) .* cos(2*pi*t);\n",
78- "vals = arrayfun(f, 0:0.5:2)"
79- ]
80- },
81- {
82- "cell_type": "markdown",
83- "id": "intro-next",
84- "metadata": {},
85- "source": [
86- "Next: [plotting](./02-plotting.ipynb) and\n",
87- "[installing packages](./03-packages.ipynb) — both also fully client-side."
88- ]
89- }
90- ],
91- "metadata": {
92- "kernelspec": {
93- "display_name": "MATLAB (numbl)",
94- "language": "matlab",
95- "name": "numbl"
96- },
97- "language_info": {
98- "codemirror_mode": "octave",
99- "file_extension": ".m",
100- "mimetype": "text/x-octave",
101- "name": "matlab",
102- "pygments_lexer": "matlab"
103- }
2+ "cells": [
3+ {
4+ "cell_type": "markdown",
5+ "id": "intro-title",
6+ "metadata": {},
7+ "source": [
8+ "# MATLAB syntax, entirely in your browser\n",
9+ "\n",
10+ "This notebook runs on the **numbl kernel for JupyterLite**. Every cell executes\n",
11+ "in a Web Worker in *your* browser tab — there is no server and no kernel\n",
12+ "process behind this page, and nothing to install.\n",
13+ "\n",
14+ "The engine is [numbl](https://github.com/flatironinstitute/numbl), an\n",
15+ "open-source MATLAB-syntax language implementation in TypeScript.\n",
16+ "\n",
17+ "Run the cells below with **Shift+Enter**."
18+ ]
10419 },
105- "nbformat": 4,
106- "nbformat_minor": 5
107-}
20+ {
21+ "cell_type": "code",
22+ "execution_count": null,
23+ "id": "intro-matrix",
24+ "metadata": {},
25+ "outputs": [],
26+ "source": [
27+ "A = [4 2 1; 2 5 3; 1 3 6]"
28+ ]
29+ },
30+ {
31+ "cell_type": "code",
32+ "execution_count": null,
33+ "id": "intro-solve",
34+ "metadata": {},
35+ "outputs": [],
36+ "source": [
37+ "% Variables persist across cells; solve a linear system with backslash\n",
38+ "b = [1; 2; 3];\n",
39+ "x = A \\ b\n",
40+ "residual = norm(A*x - b)"
41+ ]
42+ },
43+ {
44+ "cell_type": "code",
45+ "execution_count": null,
46+ "id": "intro-indexing",
47+ "metadata": {},
48+ "outputs": [],
49+ "source": [
50+ "% MATLAB indexing: rows, and logical masks\n",
51+ "A(2, :)\n",
52+ "A(A > 3)'"
53+ ]
54+ },
55+ {
56+ "cell_type": "code",
57+ "execution_count": null,
58+ "id": "intro-loop",
59+ "metadata": {},
60+ "outputs": [],
61+ "source": [
62+ "total = 0;\n",
63+ "for k = 1:10\n",
64+ " total = total + k^2;\n",
65+ "end\n",
66+ "fprintf('sum of squares 1..10 = %d\\n', total);"
67+ ]
68+ },
69+ {
70+ "cell_type": "code",
71+ "execution_count": null,
72+ "id": "intro-anon",
73+ "metadata": {},
74+ "outputs": [],
75+ "source": [
76+ "% Anonymous functions work in cells (named functions belong in .m files)\n",
77+ "f = @(t) exp(-t) .* cos(2*pi*t);\n",
78+ "vals = arrayfun(f, 0:0.5:2)"
79+ ]
80+ },
81+ {
82+ "cell_type": "markdown",
83+ "id": "1e814d6c",
84+ "source": "## Named functions live in `.m` files\n\nnumbl's cells work like the MATLAB console: you can't define a *named*\nfunction directly in a cell. Instead, put it in a `.m` file next to the\nnotebook — this kernel syncs `.m` files from the file browser into the\nsession before every cell runs, so functions defined there are callable\nimmediately, and edits take effect the next time you run a cell.\n\nThis notebook ships with [statsutils.m](./statsutils.m) — open it, and try\nediting it (e.g. add a `median` field) while the cell below is still there.",
85+ "metadata": {}
86+ },
87+ {
88+ "cell_type": "code",
89+ "id": "55d6bb19",
90+ "source": "data = [2 4 4 4 5 5 7 9];\ns = statsutils(data);\nfprintf('mean=%.4f std=%.4f range=%.4f\\n', s.mean, s.std, s.range);",
91+ "metadata": {},
92+ "execution_count": null,
93+ "outputs": []
94+ },
95+ {
96+ "cell_type": "markdown",
97+ "id": "intro-next",
98+ "metadata": {},
99+ "source": [
100+ "Next: [plotting](./02-plotting.ipynb) and\n",
101+ "[installing packages](./03-packages.ipynb) — both also fully client-side."
102+ ]
103+ }
104+ ],
105+ "metadata": {
106+ "kernelspec": {
107+ "display_name": "MATLAB (numbl)",
108+ "language": "matlab",
109+ "name": "numbl"
110+ },
111+ "language_info": {
112+ "codemirror_mode": "octave",
113+ "file_extension": ".m",
114+ "mimetype": "text/x-octave",
115+ "name": "matlab",
116+ "pygments_lexer": "matlab"
117+ }
118+ },
119+ "nbformat": 4,
120+ "nbformat_minor": 5
121+}
\ No newline at end of file
demo/content/statsutils.madded+8−0View file
@@ -0,0 +1,8 @@
1+function s = statsutils(x)
2+% Workspace helper: named functions like this live in a plain .m file next
3+% to the notebook (numbl's REPL cells can't define named functions
4+% directly). Edit this file and rerun a cell — numbl picks up the change.
5+s.mean = mean(x);
6+s.std = std(x);
7+s.range = max(x) - min(x);
8+end
src/index.tsmodified+1−1View file
@@ -32,7 +32,7 @@ const kernel: JupyterFrontEndPlugin<void> = {
3232 }
3333 },
3434 create: async (options: IKernel.IOptions): Promise<IKernel> => {
35- return new NumblKernel(options);
35+ return new NumblKernel(options, app.serviceManager.contents);
3636 }
3737 });
3838 }
src/kernel.tsmodified+62−1View file
@@ -1,6 +1,7 @@
1-import type { KernelMessage } from '@jupyterlab/services';
1+import type { Contents, KernelMessage } from '@jupyterlab/services';
22
33 import { BaseKernel } from '@jupyterlite/services';
4+import type { IKernel } from '@jupyterlite/services';
45
56 import { createNumblSession } from 'numbl/browser';
67 import type { NumblSession } from 'numbl/browser';
@@ -17,8 +18,24 @@ export const FIGURE_MIME = 'application/vnd.numbl.figure+json';
1718 * cell, and figures are published as display_data with the numbl figure
1819 * mime type. Restarting the kernel disposes the session, so the next
1920 * execution boots a fresh workspace.
21+ *
22+ * Before every execution, `.m` files sitting next to the notebook (in the
23+ * JupyterLite contents) are synced into the session, so named functions can
24+ * be defined in the file browser, edited in the Jupyter editor, and called
25+ * from cells — numbl rescans the working directory on each execution, so
26+ * edits apply on the next run. The sync is one-way and additive: deleting a
27+ * `.m` file leaves its function defined until the kernel restarts.
2028 */
2129 export class NumblKernel extends BaseKernel {
30+ /**
31+ * @param options Standard kernel options.
32+ * @param contents The (browser-side) contents manager used to read `.m`
33+ * workspace files from the notebook's directory.
34+ */
35+ constructor(options: IKernel.IOptions, contents?: Contents.IManager) {
36+ super(options);
37+ this._contents = contents ?? null;
38+ }
2239 /**
2340 * Handle a kernel_info_request message.
2441 */
@@ -70,6 +87,7 @@ export class NumblKernel extends BaseKernel {
7087 );
7188 }
7289
90+ await this._syncWorkspaceFiles(session);
7391 const result = await session.execute(content.code);
7492
7593 if (!result.ok) {
@@ -172,6 +190,47 @@ export class NumblKernel extends BaseKernel {
172190 super.dispose();
173191 }
174192
193+ /**
194+ * Sync `.m` files from the notebook's directory into the session VFS.
195+ * numbl rescans its working directory on every execution, so a file
196+ * written here becomes callable on this same execute() call, and an
197+ * edit made in the Jupyter editor takes effect the next time a cell runs.
198+ * Best-effort: a contents-manager error (e.g. no browser drive mounted)
199+ * just skips the sync rather than failing the cell.
200+ */
201+ private async _syncWorkspaceFiles(session: NumblSession): Promise<void> {
202+ if (!this._contents) {
203+ return;
204+ }
205+ const dir = this.location;
206+ let listing: Contents.IModel;
207+ try {
208+ listing = await this._contents.get(dir, { content: true });
209+ } catch {
210+ return;
211+ }
212+ const files = Array.isArray(listing.content) ? listing.content : [];
213+ for (const entry of files as Contents.IModel[]) {
214+ if (entry.type !== 'file' || !entry.name.endsWith('.m')) {
215+ continue;
216+ }
217+ if (this._syncedMTimes.get(entry.path) === entry.last_modified) {
218+ continue;
219+ }
220+ try {
221+ const file = await this._contents.get(entry.path, {
222+ content: true,
223+ type: 'file',
224+ format: 'text'
225+ });
226+ session.writeFile(entry.name, String(file.content));
227+ this._syncedMTimes.set(entry.path, entry.last_modified);
228+ } catch {
229+ // Skip this file; other workspace files still sync.
230+ }
231+ }
232+ }
233+
175234 /**
176235 * Boot the numbl session lazily on first use, so creating the kernel is
177236 * instant and boot progress (mip download, cached-package restore) streams
@@ -203,4 +262,6 @@ export class NumblKernel extends BaseKernel {
203262 }
204263
205264 private _session: Promise<NumblSession> | null = null;
265+ private readonly _contents: Contents.IManager | null;
266+ private readonly _syncedMTimes = new Map<string, string>();
206267 }
moveopenescclose