@live-codes/lfortran-wasm 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.
- package/README.md +146 -0
- package/THIRD-PARTY-NOTICES.md +68 -0
- package/assets/wasm_run.data +0 -0
- package/assets/wasm_run.js +14 -0
- package/assets/wasm_run.wasm.gz +0 -0
- package/bin/copy-assets.mjs +70 -0
- package/package.json +36 -0
- package/src/index.js +170 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @live-codes/lfortran-wasm
|
|
2
|
+
|
|
3
|
+
Run modern Fortran in the browser or in Node, on **LFortran**'s LLVM backend compiled to
|
|
4
|
+
WebAssembly. No server, no transpiler step, and no cross-origin isolation: nothing in the pipeline is
|
|
5
|
+
threaded, so no `SharedArrayBuffer` is involved.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createCompiler } from '@live-codes/lfortran-wasm';
|
|
9
|
+
|
|
10
|
+
const compiler = await createCompiler(); // once; each run is milliseconds after this
|
|
11
|
+
const { stdout, errors, exitCode } = await compiler.run(`
|
|
12
|
+
program hello
|
|
13
|
+
print *, 'hello from LFortran'
|
|
14
|
+
end program
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
console.log(exitCode, stdout); // 0 'hello from LFortran\n'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
### `createCompiler(options?) → Promise<compiler>`
|
|
23
|
+
|
|
24
|
+
| option | meaning |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `baseUrl` | where to fetch `wasm_run.js`, `wasm_run.wasm.gz` and `wasm_run.data`. Defaults to the assets shipped in this package, which is what a bundler resolves; pass a CDN URL to load them from elsewhere |
|
|
27
|
+
| `glueUrl`, `assetBaseUrl`, `wasmUrl` | finer-grained overrides if the three assets do not sit together |
|
|
28
|
+
| `wasmBinary` | an already-decompressed `ArrayBuffer`, which skips the download entirely |
|
|
29
|
+
| `print`, `printErr` | receive output as it is written, instead of collecting it into the result |
|
|
30
|
+
|
|
31
|
+
### `compiler.run(code, stdin = '') → Promise<{ stdout, errors, exitCode, runMs }>`
|
|
32
|
+
|
|
33
|
+
`exitCode` is `0` when the program ran, and `null` when it did not — a compile error, or the program
|
|
34
|
+
calling `exit()`. Compile failures put LFortran's own rendered diagnostic in `errors`; they do not
|
|
35
|
+
throw.
|
|
36
|
+
|
|
37
|
+
One compiler is meant to be reused: it holds the loaded module, and `run` compiles a fresh program
|
|
38
|
+
each time. Each run links its program as a separate wasm side module, which the module's executor
|
|
39
|
+
loads; those live for the lifetime of the loaded compiler, which is why a long-lived page should
|
|
40
|
+
create one and reuse it rather than one per keystroke.
|
|
41
|
+
|
|
42
|
+
## What it costs
|
|
43
|
+
|
|
44
|
+
| asset | raw | gzip | brotli |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| `wasm_run.wasm` | 70.75 MiB | 19.04 MiB | 12.90 MiB |
|
|
47
|
+
| `wasm_run.js` | 0.54 MiB | 0.12 MiB | 0.10 MiB |
|
|
48
|
+
| `wasm_run.data` | 0.17 MiB | 0.03 MiB | 0.02 MiB |
|
|
49
|
+
| **total** | **71.46 MiB** | **19.19 MiB** | **13.02 MiB** |
|
|
50
|
+
|
|
51
|
+
The package ships the **gzip** (19.04 MiB) and decompresses it in the client with
|
|
52
|
+
`DecompressionStream`. The raw file is never published: at 70.75 MiB it is past what a CDN will serve
|
|
53
|
+
for a package file. Brotli is smaller but browsers cannot decompress it from script —
|
|
54
|
+
`DecompressionStream` supports only gzip and deflate — so it is left on the table.
|
|
55
|
+
|
|
56
|
+
For comparison, the toolchain LiveCodes already loads for C and C++ (`@live-codes/clang-wasm`
|
|
57
|
+
0.2.0) is 28.5 MiB across 30 files, its largest being 15.0 MiB. This package is smaller in total but
|
|
58
|
+
ships as one much larger file.
|
|
59
|
+
|
|
60
|
+
## Why it is built the way it is
|
|
61
|
+
|
|
62
|
+
The artifact is produced by the Docker build in this repository at `docker/lfortran-wasm/`, which
|
|
63
|
+
documents each decision at the point it is made. The short version:
|
|
64
|
+
|
|
65
|
+
- **LFortran v0.65.0**, Emscripten 4.0.9, LLVM **22.1.8** for `emscripten-wasm32`. The LLVM version is
|
|
66
|
+
pinned deliberately: `pixi.toml` says `llvm = "*"`, so solving today gives LLVM 23, and LFortran
|
|
67
|
+
v0.65.0 does not build against it.
|
|
68
|
+
- **Own entry point.** LFortran's wasm build only *emits* things — AST, ASR, WAT, C, C++, wasm bytes.
|
|
69
|
+
In a browser there is no linker subprocess to hand a binary to, so `wasm-run-main.cpp` compiles and
|
|
70
|
+
runs in-process through `FortranEvaluator`, which is what its own wasm-compatible tests use.
|
|
71
|
+
- **`-s MAIN_MODULE=1`**, because the executor loads the program it just compiled with `dlopen`. No
|
|
72
|
+
`-pthread` and no `USE_PTHREADS` anywhere.
|
|
73
|
+
- **One patch to LFortran**, in two `start_new_block` helpers: `getTerminator()` became
|
|
74
|
+
`getTerminatorOrNull()`. Modern LLVM changed `BasicBlock::getTerminator()` to *assume* a
|
|
75
|
+
well-formed block; under `NDEBUG` it returns the trailing instruction for a block that is not
|
|
76
|
+
terminated, so LFortran concluded blocks were already terminated and emitted no branches, producing
|
|
77
|
+
invalid IR (`does not have terminator`) for every program.
|
|
78
|
+
- **The reported version is pinned to the clean tag**, because that patch makes the build tree dirty:
|
|
79
|
+
`build0.sh` runs `ci/version.sh`, which is `git describe --tags --dirty`, so the compiler called
|
|
80
|
+
itself `0.65.0-dirty` while the preloaded runtime `.mod` files said `0.65.0` — and LFortran refuses
|
|
81
|
+
to load a `.mod` from a different version, which breaks `open`, `use iso_fortran_env` and more.
|
|
82
|
+
There is a test for it.
|
|
83
|
+
|
|
84
|
+
**`MAIN_MODULE=2` was measured and rejected.** Exporting only a curated list would drop the 43,098
|
|
85
|
+
exported symbols that `MAIN_MODULE=1` carries — about 4.74 MiB of export section, roughly 7% of the
|
|
86
|
+
raw wasm and less once compressed. The list has to be exactly what the link defines, and deriving it
|
|
87
|
+
is awkward in both obvious ways (from the runtime's sources it includes symbols the link never pulls
|
|
88
|
+
in; from the side modules' imports it includes GOT entries they resolve locally), so it needs a
|
|
89
|
+
CMake custom command running `nm` over `liblfortran_runtime_static.a` before the link. Worse, the
|
|
90
|
+
importable surface would then be frozen at build time, so a user program needing a runtime function
|
|
91
|
+
the link did not happen to pull in would fail at `dlopen` — where `MAIN_MODULE=1` simply works. Not
|
|
92
|
+
worth 4.7 MiB. The reasoning lives in `docker/lfortran-wasm/build-in-container.sh`.
|
|
93
|
+
|
|
94
|
+
## Loader details worth knowing
|
|
95
|
+
|
|
96
|
+
Each of these was found by running it, and each is load-bearing:
|
|
97
|
+
|
|
98
|
+
- **`locateFile` must return a filesystem path under Node** and a URL in a browser. Emscripten's Node
|
|
99
|
+
path reads assets with `fs`, so a `file:` URL gets concatenated onto the script directory.
|
|
100
|
+
- **stdin is wired by pointing fd 0 at a MEMFS file**, not by setting `Module.stdin`.
|
|
101
|
+
`FS.createStandardStreams` only creates a device for `/dev/stdin` `if (input)`, and that sits behind
|
|
102
|
+
Emscripten's compile-time `expectToReceiveOnModule('stdin')` check; otherwise `/dev/stdin` symlinks
|
|
103
|
+
to `/dev/tty`, whose fallback reads the host's stdin.
|
|
104
|
+
- **`print` and `printErr` are called once per line with the newline consumed**, so output has to be
|
|
105
|
+
rejoined with `\n` or consecutive writes run together.
|
|
106
|
+
- **A Fortran `exit()` arrives as a thrown `ExitStatus` rather than a return value**, and under Node
|
|
107
|
+
it also sets the host process's exit code — which makes a test runner report a whole file as failed
|
|
108
|
+
even when every assertion passed. Both are handled here.
|
|
109
|
+
- **The glue is an ES module** (`EXPORT_ES6=1`). That is why one loader covers a page, a worker and
|
|
110
|
+
Node. The alternative — a script tag in one and a `vm` context in the other — creates a realm
|
|
111
|
+
mismatch in which a `TypeError` thrown by the host's `WebAssembly` is not an instance of the vm's
|
|
112
|
+
`TypeError`, turning a handled case into a raw `WebAssembly.Table.set` failure.
|
|
113
|
+
|
|
114
|
+
## Building the artifact
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
docker build -t lfortran-wasm-build docker/lfortran-wasm
|
|
118
|
+
docker run -d --name lfortran-wasm-run lfortran-wasm-build # ~25 min first time, ~7 after
|
|
119
|
+
docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.js docker/lfortran-wasm/out/
|
|
120
|
+
docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.wasm docker/lfortran-wasm/out/
|
|
121
|
+
docker cp lfortran-wasm-run:/src/build-wasm/src/bin/wasm_run.data docker/lfortran-wasm/out/
|
|
122
|
+
npm run copy-assets # vendors + gzips into assets/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`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.
|
|
127
|
+
|
|
128
|
+
## Tests
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npm test # 6 tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
They exercise the packaged artifact through the public API: free-form source, modules with contained
|
|
135
|
+
procedures and derived types, array sections, stdin, a compile error, and a program that exits.
|
|
136
|
+
`assets/` is produced by the build above, so a checkout without it skips rather than fails.
|
|
137
|
+
|
|
138
|
+
The same loader and the same corpus are also driven against a real browser by
|
|
139
|
+
`docker/lfortran-wasm/browser-test.html` (14/14) and in Node by `docker/lfortran-wasm/test-run.mjs`,
|
|
140
|
+
which is how the browser path is verified rather than assumed.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT for this package. The wasm it ships is LFortran (BSD 3-Clause), LLVM and LLD (Apache-2.0 with
|
|
145
|
+
LLVM Exceptions) and Emscripten (MIT/University of Illinois) — see `THIRD-PARTY-NOTICES.md`, which
|
|
146
|
+
must travel with any redistribution.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
`assets/wasm_run.wasm` is not written by this package. It is LFortran, its runtime, LLVM and LLD,
|
|
4
|
+
compiled to WebAssembly with Emscripten. Redistributing it means redistributing those projects'
|
|
5
|
+
binaries, so their notices are reproduced here — LFortran's BSD 3-Clause and LLVM's Apache-2.0 both
|
|
6
|
+
require the notice to travel with binary distributions.
|
|
7
|
+
|
|
8
|
+
## LFortran
|
|
9
|
+
|
|
10
|
+
<https://github.com/lfortran/lfortran> — BSD 3-Clause
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
BSD 3-Clause License
|
|
14
|
+
|
|
15
|
+
Copyright (c) 2019-2020, Triad National Security, LLC. All rights reserved.
|
|
16
|
+
|
|
17
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
18
|
+
are permitted provided that the following conditions are met:
|
|
19
|
+
|
|
20
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
21
|
+
list of conditions and the following disclaimer.
|
|
22
|
+
|
|
23
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
24
|
+
this list of conditions and the following disclaimer in the documentation and/or
|
|
25
|
+
other materials provided with the distribution.
|
|
26
|
+
|
|
27
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
28
|
+
may be used to endorse or promote products derived from this software without
|
|
29
|
+
specific prior written permission.
|
|
30
|
+
|
|
31
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
32
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
33
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
34
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
35
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
36
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
37
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
38
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
39
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
40
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This build also carries the patches listed in the repository's
|
|
44
|
+
`docker/lfortran-wasm/build-in-container.sh`; they are described in that file.
|
|
45
|
+
|
|
46
|
+
## LLVM and LLD
|
|
47
|
+
|
|
48
|
+
<https://llvm.org> — Apache License v2.0 with LLVM Exceptions
|
|
49
|
+
|
|
50
|
+
The compiler's backend, the `libLLVM*.a` archives, and LLD (which links the side module the compiler
|
|
51
|
+
emits at run time) are compiled into the wasm. The LLVM exception means the resulting binary does not
|
|
52
|
+
impose Apache-2.0 terms on this package, but the license and the exception notice must be included
|
|
53
|
+
with redistributions. The Apache-2.0 text is at <https://llvm.org/LICENSE.txt>, and the exception
|
|
54
|
+
notice is the first lines of `LICENSE.TXT` in any LLVM checkout.
|
|
55
|
+
|
|
56
|
+
## Emscripten
|
|
57
|
+
|
|
58
|
+
<https://emscripten.org> — MIT / University of Illinois
|
|
59
|
+
|
|
60
|
+
The JavaScript glue (`assets/wasm_run.js`), the preloaded filesystem format, and the C/C++ runtime
|
|
61
|
+
the wasm links against come from Emscripten. License: <https://github.com/emscripten-core/emscripten>
|
|
62
|
+
(`LICENSE`).
|
|
63
|
+
|
|
64
|
+
## LFortran runtime modules
|
|
65
|
+
|
|
66
|
+
`assets/wasm_run.data` contains the runtime `.mod` files built from LFortran's own runtime sources
|
|
67
|
+
(`src/runtime/`), covered by LFortran's license above. They are preloaded into the wasm filesystem at
|
|
68
|
+
`/lib`, which is where the evaluator resolves `use iso_c_binding` and friends.
|
|
Binary file
|