@live-codes/lfortran-wasm 0.1.0 → 0.1.1

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 CHANGED
@@ -39,6 +39,28 @@ each time. Each run links its program as a separate wasm side module, which the
39
39
  loads; those live for the lifetime of the loaded compiler, which is why a long-lived page should
40
40
  create one and reuse it rather than one per keystroke.
41
41
 
42
+ ### Classic workers
43
+
44
+ A page or a module worker imports the package directly. A **classic** worker can use it too, because
45
+ dynamic `import()` is available in classic workers as well — `await import(packageUrl)` works, verified
46
+ with the corpus at 14/14.
47
+
48
+ If a host can only use `importScripts`, there is a classic IIFE build for that:
49
+
50
+ ```js
51
+ // in a classic (non-module) worker
52
+ importScripts('https://cdn.jsdelivr.net/npm/@live-codes/lfortran-wasm/dist/lfortran-wasm.global.js');
53
+ const compiler = await self.lfortranWasm.createCompiler(); // baseUrl optional; defaults to the CDN
54
+ ```
55
+
56
+ It is 3.4 KB minified, because the loader has no dependencies. Pin the version in production.
57
+
58
+ `importScripts` cannot load the emscripten glue, and this build exists because of it: the glue is an ES
59
+ module (`EXPORT_ES6=1`), so `importScripts` reports a NetworkError for it. Measured against the same
60
+ CDN in Chromium: another package's classic build loads, ours does not. The loader therefore reaches the
61
+ glue with dynamic `import()`, which works from a classic worker, and only the loader needed a classic
62
+ form.
63
+
42
64
  ## What it costs
43
65
 
44
66
  | asset | raw | gzip | brotli |
@@ -120,10 +142,12 @@ docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.js docker/lfortra
120
142
  docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.wasm docker/lfortran-wasm/out/
121
143
  docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.data docker/lfortran-wasm/out/
122
144
  npm run copy-assets # vendors + gzips into assets/
145
+ npm run build:iife # dist/lfortran-wasm.global.js
123
146
  ```
124
147
 
125
148
  `npm run copy-assets` prints a size and a SHA-256 receipt per asset, so a published artifact can be
126
- matched against a build.
149
+ matched against a build. `build:iife` needs `esbuild`; in this repository it falls back to the copy the
150
+ sibling package has installed.
127
151
 
128
152
  ## Tests
129
153
 
@@ -139,6 +163,28 @@ The same loader and the same corpus are also driven against a real browser by
139
163
  `docker/lfortran-wasm/browser-test.html` (14/14) and in Node by `docker/lfortran-wasm/test-run.mjs`,
140
164
  which is how the browser path is verified rather than assumed.
141
165
 
166
+ ## Behaviour worth knowing
167
+
168
+ Verified, not assumed — each of these was run:
169
+
170
+ - **Bounds and intrinsic-domain errors are compile-time diagnostics.** `a(5) = 1` on `integer :: a(3)`
171
+ reports `Array index 5 is out of bounds (1 to 3)` and `sqrt(-1.0)` reports `Argument of 'sqrt' has a
172
+ negative argument`. They are not runtime traps.
173
+ - **A trap does not poison the compiler.** Stack exhaustion from runaway recursion surfaces as
174
+ `Maximum call stack size exceeded`, and the next `run()` works normally. The same is true after a
175
+ program that exits, and after a failed read from an empty stdin.
176
+ - **Integer division by zero prints `0`** rather than trapping or diagnosing. Fortran leaves it
177
+ undefined, so this is a defensible choice rather than a bug, but it is silent.
178
+ - **Node cannot import the glue from a CDN.** Node's ESM loader accepts only `file:` and `data:` URLs,
179
+ so `baseUrl` pointing at a CDN works in a browser and a worker, and in Node only the wasm and
180
+ `.data` come from there — the loader says so explicitly rather than failing obscurely.
181
+ - **The first load is network-bound.** 19.04 MiB compressed; on a slow link that is tens of seconds.
182
+ It is fetched once per page or worker and cached immutably by the CDN.
183
+ - **Coverage is thin.** 14 corpus programs plus a 22-program spread — modules with contained
184
+ procedures, derived types, allocatables, array sections, whole-array arithmetic, formatted and file
185
+ I/O, stdin, libm. LFortran is a young compiler; coarrays, submodules, quad precision and parts of
186
+ I/O are not verified here, and a live playground will find things this does not.
187
+
142
188
  ## License
143
189
 
144
190
  MIT for this package. The wasm it ships is LFortran (BSD 3-Clause), LLVM and LLD (Apache-2.0 with
@@ -0,0 +1,6 @@
1
+ /*! @live-codes/lfortran-wasm - MIT. IIFE build, sets self.lfortranWasm.
2
+ * importScripts('lfortran-wasm.global.js') then self.lfortranWasm.createCompiler({ baseUrl }).
3
+ * Downloads the LFortran + LLVM wasm from assets/ (19 MiB compressed). LFortran is BSD 3-Clause and
4
+ * LLVM is Apache-2.0 WITH LLVM-exception - see THIRD-PARTY-NOTICES.md. */
5
+ var lfortranWasm=(()=>{var k=Object.create;var p=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var _=Object.getPrototypeOf,N=Object.prototype.hasOwnProperty;var y=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var v=(t,e)=>{for(var r in e)p(t,r,{get:e[r],enumerable:!0})},F=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of P(e))!N.call(t,o)&&o!==r&&p(t,o,{get:()=>e[o],enumerable:!(n=A(e,o))||n.enumerable});return t};var g=(t,e,r)=>(r=t!=null?k(_(t)):{},F(e||!t||!t.__esModule?p(r,"default",{value:t,enumerable:!0}):r,t)),D=t=>F(p({},"__esModule",{value:!0}),t);var q={};v(q,{createCompiler:()=>C,default:()=>W});var U=typeof process<"u"&&process.versions?.node!=null,a=t=>t.join(`
6
+ `);async function G(t){if(String(t).startsWith("file:")){let{readFile:n}=await import("node:fs/promises"),{gunzipSync:o}=await import("node:zlib");return new Uint8Array(o(await n(t)))}let e=await fetch(t);if(!e.ok)throw new Error(`fetching ${t}: ${e.status} ${e.statusText}`);let r=e.body.pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(r).arrayBuffer())}function H(t){let e=new TextEncoder;return r=>{let n=t.FS;if(!n)throw new Error("FS was not exported, so stdin cannot be wired");n.writeFile("/.stdin",e.encode(r));let o=n.open("/.stdin","r");n.streams[0]&&n.close(n.streams[0]),n.streams[0]=o}}async function C(t={}){let{baseUrl:e,glueUrl:r,assetBaseUrl:n,wasmUrl:o,wasmBinary:B,print:x,printErr:L}=t,u=new URL(n??(e?new URL(e,"https://cdn.jsdelivr.net/npm/@live-codes/lfortran-wasm@0.1.1/src/index.js"):new URL("../assets/","https://cdn.jsdelivr.net/npm/@live-codes/lfortran-wasm@0.1.1/src/index.js")),"https://cdn.jsdelivr.net/npm/@live-codes/lfortran-wasm@0.1.1/src/index.js"),l=U&&u.protocol==="file:",b=l?(await import("node:url")).fileURLToPath:null,$=s=>{let d=new URL(s,u);return l?b(d):d.href},i=[],c=[],z=B??await G(new URL(o??"wasm_run.wasm.gz",u)),w=new URL(r??"wasm_run.js",u).href;if(l&&!/^(file|data):/.test(w))throw new Error(`node cannot import the glue from ${w}: its ESM loader supports only file: and data: URLs. Point glueUrl at a local copy, or run this in a browser or a worker.`);let S=await import(w),E=S.default??S.createLFortran;if(typeof E!="function")throw new Error("the glue module did not export a factory");let f=await E({locateFile:$,wasmBinary:z,print:s=>x?x(s):i.push(s),printErr:s=>L?L(s):c.push(s)}),M=f.cwrap("run_fortran","string",["string"]),T=H(f);return{module:f,async run(s,d=""){i.length=0,c.length=0,T(d);let m=performance.now(),j=U?process.exitCode:void 0;try{let h;try{h=M(s)}catch(R){return{stdout:a(i),errors:a(c)||`the program terminated: ${R?.message??R}`,exitCode:null,runMs:performance.now()-m}}return h==="0"?{stdout:a(i),errors:a(c),exitCode:0,runMs:performance.now()-m}:{stdout:a(i),errors:h.replace(/^1,/,"")||a(c),exitCode:null,runMs:performance.now()-m}}finally{U&&(process.exitCode=j)}}}}var W=C;return D(q);})();
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@live-codes/lfortran-wasm",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Run modern Fortran in the browser or in Node, on LFortran's LLVM backend compiled to WebAssembly",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
8
- ".": "./src/index.js"
8
+ ".": "./src/index.js",
9
+ "./iife": "./dist/lfortran-wasm.global.js"
9
10
  },
10
11
  "bin": {
11
12
  "lfortran-wasm-copy-assets": "./bin/copy-assets.mjs"
@@ -13,6 +14,8 @@
13
14
  "files": [
14
15
  "src",
15
16
  "bin",
17
+ "scripts",
18
+ "dist",
16
19
  "assets",
17
20
  "THIRD-PARTY-NOTICES.md"
18
21
  ],
@@ -21,8 +24,12 @@
21
24
  },
22
25
  "scripts": {
23
26
  "copy-assets": "node bin/copy-assets.mjs",
27
+ "build:iife": "node scripts/build-iife.mjs",
24
28
  "test": "node --test \"test/*.test.js\""
25
29
  },
30
+ "devDependencies": {
31
+ "esbuild": "^0.25.0"
32
+ },
26
33
  "keywords": [
27
34
  "fortran",
28
35
  "lfortran",
@@ -0,0 +1,64 @@
1
+ // Builds the IIFE bundle: one classic script for workers and pages that cannot use ES modules.
2
+ //
3
+ // npm run build:iife
4
+ //
5
+ // Why this exists: `importScripts()` evaluates a *classic* script, and an ES module is not one. Measured
6
+ // in Chromium, against the same CDN — importing the clang package's IIFE works, while importing our
7
+ // `assets/wasm_run.js` fails with a NetworkError precisely because it is a module. The glue can stay a
8
+ // module (this bundle reaches it with dynamic import(), which a classic worker supports), but the
9
+ // loader itself has to have a classic form for `importScripts` to be able to load anything.
10
+ //
11
+ // The output is committed, because a classic worker can only point at a URL and a consumer should not
12
+ // need a bundler to get one.
13
+ //
14
+ // `import.meta.url` is defined rather than left to the bundler: esbuild has no meaning for it in an
15
+ // IIFE, and the loader uses it for its default asset base. Pointing it at this package's `src/index.js`
16
+ // on jsDelivr makes the default resolve to `.../assets/` there, which is what a consumer of the global
17
+ // build wants; `baseUrl` still overrides it, which is how the local build is tested.
18
+ import { readFileSync, statSync } from 'node:fs';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const packageJson = JSON.parse(
22
+ readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'),
23
+ );
24
+ const sourceUrl = `https://cdn.jsdelivr.net/npm/${packageJson.name}@${packageJson.version}/src/index.js`;
25
+ const outfile = fileURLToPath(new URL('../dist/lfortran-wasm.global.js', import.meta.url));
26
+
27
+ // esbuild is a devDependency of this package. In this repository the sibling package already has it
28
+ // installed, so fall back to that rather than requiring a network install to build.
29
+ async function loadEsbuild() {
30
+ try {
31
+ return await import('esbuild');
32
+ } catch {
33
+ // A file: URL, not a path — on Windows a bare `D:\...` is read as a URL scheme.
34
+ const sibling = new URL('../../fortran-wasm/node_modules/esbuild/lib/main.js', import.meta.url);
35
+ return import(sibling.href);
36
+ }
37
+ }
38
+
39
+ const { build } = await loadEsbuild();
40
+
41
+ const banner = `/*! ${packageJson.name} - MIT. IIFE build, sets self.lfortranWasm.
42
+ * importScripts('lfortran-wasm.global.js') then self.lfortranWasm.createCompiler({ baseUrl }).
43
+ * Downloads the LFortran + LLVM wasm from assets/ (19 MiB compressed). LFortran is BSD 3-Clause and
44
+ * LLVM is Apache-2.0 WITH LLVM-exception - see THIRD-PARTY-NOTICES.md. */`;
45
+
46
+ await build({
47
+ entryPoints: [fileURLToPath(new URL('../src/index.js', import.meta.url))],
48
+ outfile,
49
+ bundle: true,
50
+ format: 'iife',
51
+ globalName: 'lfortranWasm',
52
+ minify: true,
53
+ platform: 'browser',
54
+ target: 'es2022',
55
+ // Node-only paths in the loader are behind a runtime check and never taken in a browser, so they are
56
+ // left as imports rather than bundled.
57
+ external: ['node:*'],
58
+ define: { 'import.meta.url': JSON.stringify(sourceUrl) },
59
+ legalComments: 'external',
60
+ banner: { js: banner },
61
+ });
62
+
63
+ console.log(`dist/lfortran-wasm.global.js ${(statSync(outfile).size / 1024).toFixed(1)} KB (minified)`);
64
+ console.log(`default asset base: ${new URL('../assets/', sourceUrl).href}`);
package/src/index.js CHANGED
@@ -93,7 +93,17 @@ export async function createCompiler(options = {}) {
93
93
  const stdout = [];
94
94
  const stderr = [];
95
95
  const binary = wasmBinary ?? (await readGzippedBytes(new URL(wasmUrl ?? 'wasm_run.wasm.gz', assetBase)));
96
- const glue = await import(new URL(glueUrl ?? 'wasm_run.js', assetBase).href);
96
+ const glueHref = new URL(glueUrl ?? 'wasm_run.js', assetBase).href;
97
+ if (inNode && !/^(file|data):/.test(glueHref)) {
98
+ // Node's ESM loader accepts only file: and data: URLs, so a CDN-hosted glue cannot be imported
99
+ // there even though the wasm and .data fetch fine over https. Browsers and workers import it
100
+ // from a CDN without ceremony, which is where this normally runs.
101
+ throw new Error(
102
+ `node cannot import the glue from ${glueHref}: its ESM loader supports only file: and data: URLs. ` +
103
+ 'Point glueUrl at a local copy, or run this in a browser or a worker.',
104
+ );
105
+ }
106
+ const glue = await import(glueHref);
97
107
  const factory = glue.default ?? glue.createLFortran;
98
108
  if (typeof factory !== 'function') {
99
109
  throw new Error('the glue module did not export a factory');