@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 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