@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 +47 -1
- package/dist/lfortran-wasm.global.js +6 -0
- package/package.json +9 -2
- package/scripts/build-iife.mjs +64 -0
- package/src/index.js +11 -1
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.
|
|
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
|
|
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');
|