@swiftbrowser/wasm-compiler 0.0.0-stage → 0.8.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 +299 -2
- package/bin/swiftbrowser-wasmc.js +18 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +147 -0
- package/dist/compile.d.ts +69 -0
- package/dist/compile.js +89 -0
- package/dist/driver.d.ts +116 -0
- package/dist/driver.js +165 -0
- package/dist/host.d.ts +27 -0
- package/dist/host.js +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/memory-host.d.ts +50 -0
- package/dist/memory-host.js +195 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +4 -0
- package/dist/node/native-host.d.ts +8 -0
- package/dist/node/native-host.js +31 -0
- package/dist/node/plugin-executable.d.ts +15 -0
- package/dist/node/plugin-executable.js +97 -0
- package/dist/node/tools.d.ts +24 -0
- package/dist/node/tools.js +66 -0
- package/dist/node/wasi-host.d.ts +53 -0
- package/dist/node/wasi-host.js +189 -0
- package/dist/object.d.ts +5 -0
- package/dist/object.js +67 -0
- package/dist/plugins.d.ts +15 -0
- package/dist/plugins.js +147 -0
- package/dist/wasm.d.ts +24 -0
- package/dist/wasm.js +90 -0
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -1,3 +1,300 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @swiftbrowser/wasm-compiler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Swift compiler and linker built as WebAssembly, and what runs them:
|
|
4
|
+
Milestone 1 of [the in-browser compiler plan](../../docs/in-browser-compiler.md).
|
|
5
|
+
Not published yet.
|
|
6
|
+
|
|
7
|
+
- `src/driver.ts` plans the jobs `swiftc` would run for
|
|
8
|
+
`wasm32-unknown-wasip1`: one `swift-frontend` job per source, then
|
|
9
|
+
`wasm-ld`. Pure TypeScript, so the browser can use it.
|
|
10
|
+
- `src/wasm.ts` reads the autolink entries out of the objects, in place of
|
|
11
|
+
`swift-autolink-extract`.
|
|
12
|
+
- `src/compile.ts` runs those jobs on a host. `src/node` has two hosts: the
|
|
13
|
+
native toolchain, and the WebAssembly tools under Node's WASI.
|
|
14
|
+
- `src/cli.ts` is `swiftbrowser-wasmc`.
|
|
15
|
+
- `toolchain/` downloads the inputs and builds the WebAssembly tools.
|
|
16
|
+
|
|
17
|
+
## The CLI
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm run build --workspace packages/wasm-compiler
|
|
21
|
+
node packages/wasm-compiler/bin/swiftbrowser-wasmc.js \
|
|
22
|
+
--sdk <swift-sdk>/wasm32-unknown-wasip1 --tools $WASMC_WORK/build/tools \
|
|
23
|
+
--module-cache ~/.cache/swiftbrowser-wasmc \
|
|
24
|
+
--module-name Hello -o hello.wasm --run main.swift
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`toolchain/install-tools.sh` puts `swift-frontend.wasm` and `wasm-ld.wasm`
|
|
28
|
+
in `$WASMC_WORK/build/tools`. The first compile builds Clang's modules for
|
|
29
|
+
SwiftShims and wasi-libc into the module cache; later ones reuse them.
|
|
30
|
+
|
|
31
|
+
`--native-toolchain <dir>` runs the same jobs with a native Swift toolchain
|
|
32
|
+
instead, which is how the driver is checked on a machine with Swift.
|
|
33
|
+
|
|
34
|
+
## Where it stands
|
|
35
|
+
|
|
36
|
+
Milestone 1 works: the CLI compiles `Sources/HelloWasm/main.swift` with
|
|
37
|
+
`swift-frontend.wasm`, links it with `wasm-ld.wasm`, both under Node's WASI,
|
|
38
|
+
and the result prints its line under Node and wasmtime. Multi-file modules
|
|
39
|
+
and diagnostics (shown with host paths) work too.
|
|
40
|
+
|
|
41
|
+
| | Time |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `swift-frontend.wasm -c`, HelloWasm, empty module cache | 1.55 s |
|
|
44
|
+
| the same, module cache warm | 1.35 s |
|
|
45
|
+
| `wasm-ld.wasm` | 0.61 s |
|
|
46
|
+
| the whole CLI run | 2.0–2.3 s |
|
|
47
|
+
|
|
48
|
+
The same host, native tools: 0.07 s and 0.19 s (below). Most of the
|
|
49
|
+
WebAssembly time is V8 compiling the modules lazily as functions are first
|
|
50
|
+
called; wasmtime compiles the whole frontend up front, about 45 s per run.
|
|
51
|
+
|
|
52
|
+
What came out of the check against native (steps 1.3 and 1.4): the frontend's
|
|
53
|
+
object, linked by the native `wasm-ld`, runs; its bytes match the native
|
|
54
|
+
frontend's but for 16 bytes the native one randomizes too. `wasm-ld.wasm`
|
|
55
|
+
and the native `wasm-ld` write byte-identical executables.
|
|
56
|
+
|
|
57
|
+
The tools' sizes (brotli at quality 11 with a 16 MB window):
|
|
58
|
+
|
|
59
|
+
| | Raw | gzip | brotli |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `swift-frontend.wasm`, assertions, not stripped | 168.3 MB | 40.7 MB | 25.0 MB |
|
|
62
|
+
| `swift-frontend.wasm`, no assertions, stripped | 118.8 MB | 35.0 MB | 21.5 MB |
|
|
63
|
+
| `wasm-ld.wasm`, not stripped | 54.4 MB | 13.8 MB | 8.7 MB |
|
|
64
|
+
| `wasm-ld.wasm`, stripped | 39.9 MB | 12.4 MB | 7.8 MB |
|
|
65
|
+
|
|
66
|
+
Stripping (`STRIP=ON toolchain/install-tools.sh`) does most of it: the name
|
|
67
|
+
section is 43 MB of the frontend. Turning off assertions saves 7 MB raw.
|
|
68
|
+
`wasm-opt -Os` on the stripped linker saves 8 MB raw but almost nothing
|
|
69
|
+
compressed (7.8 → 7.7 MB brotli), so it is not used. The no-assertions
|
|
70
|
+
frontend compiles hello world in 1.15–1.3 s under Node.
|
|
71
|
+
|
|
72
|
+
**The in-memory host** (`memoryHost` in `src/memory-host.ts`, over
|
|
73
|
+
`@bjorn3/browser_wasi_shim`, which is what the browser will use) runs the
|
|
74
|
+
same compile, link and program in about 1.3 s, building Clang's modules
|
|
75
|
+
in memory as it goes. `toolchain/record-sdk-files.mjs` drives it and logs
|
|
76
|
+
which SDK files the tools open: for hello world, 53 of the SDK's 3324 files,
|
|
77
|
+
46.6 MB, listed in `sdk-manifests/hello.json`. Most of that is
|
|
78
|
+
`Swift.swiftmodule` (17 MB), `libswiftCore.a` (12 MB) and `Swift.swiftdoc`
|
|
79
|
+
(4.5 MB, opened alongside the module; whether a compile can do without it
|
|
80
|
+
is for Milestone 3's download work).
|
|
81
|
+
|
|
82
|
+
Things a 32-bit host ran into, all fixed in `toolchain/patches`:
|
|
83
|
+
|
|
84
|
+
- Types that assume pointers have three spare low bits (`PointerIntPair`
|
|
85
|
+
with 3 bits, custom `PointerLikeTypeTraits`) get explicit `alignas` or a
|
|
86
|
+
plain struct on 32-bit.
|
|
87
|
+
- `BridgedSwiftObject`'s refcount field is 32 bits on a 32-bit host, as the
|
|
88
|
+
Swift object header is.
|
|
89
|
+
- Clang's `ZeroFilledMemoryBuffer` uses `calloc` instead of `mmap`, and
|
|
90
|
+
IRGen frees the SIL module on the same thread, since there are none.
|
|
91
|
+
|
|
92
|
+
Node 22's V8 crashes in the garbage collector with fast API calls when a
|
|
93
|
+
module this size runs under WASI; `src/node/wasi-host.ts` turns them off
|
|
94
|
+
(`--no-turbo-fast-api-calls`).
|
|
95
|
+
|
|
96
|
+
## Apps (Milestone 2)
|
|
97
|
+
|
|
98
|
+
`SWIFTBROWSER_COMPILER=wasm` (or `auto`; `packages/cli/src/compiler.ts`)
|
|
99
|
+
makes the CLI (`packages/cli`) build an app with these tools instead of
|
|
100
|
+
`swift build`, for `build` and `dev` alike, and type-check it for `check`
|
|
101
|
+
(`packages/cli/src/wasmCompiler.ts`), with no Swift install: the CLI
|
|
102
|
+
downloads the Swift SDK for WebAssembly itself and uses the released kit,
|
|
103
|
+
whose macros come built as WebAssembly. It reproduces SwiftPM's release build
|
|
104
|
+
of the staged package: one whole-module `-O` frontend job against the kit's
|
|
105
|
+
modules, Foundation's trimmed ICU data as an object written in TypeScript
|
|
106
|
+
(`dataObject` in `src/object.ts`, in place of clang assembling `.incbin`),
|
|
107
|
+
and a `wasm-ld` link with the app's exports and the kit's archives, linked
|
|
108
|
+
whole. Package dependencies, copied for the shim or plain, are compiled
|
|
109
|
+
module by module first (`packages/cli/src/wasmPackages.ts`), only the
|
|
110
|
+
targets the app's products need. Without SwiftPM their manifests run on
|
|
111
|
+
these tools: each is compiled against PackageDescription built for
|
|
112
|
+
WebAssembly (`toolchain/build-manifest-api.sh`, shipped as `manifest-api/`
|
|
113
|
+
in @swiftbrowser/wasm-tools), linked with Foundation, and run under WASI
|
|
114
|
+
the way SwiftPM runs it (`packages/cli/src/manifest.ts`). C targets
|
|
115
|
+
compile with clang built as WebAssembly (`clang.wasm`, from the same LLVM
|
|
116
|
+
build as `wasm-ld`; `compileC` in `src/compile.ts`). Macro targets in
|
|
117
|
+
packages are linked as WebAssembly executables, like the kit's macros, and
|
|
118
|
+
handed to the host as they are built (`WasiHost.addPlugin`).
|
|
119
|
+
|
|
120
|
+
Every example, built this way and run headless, gives the same op stream as
|
|
121
|
+
its native build (`scripts/snapshot-test.sh`'s check). With tools release
|
|
122
|
+
`swift-6.4.0-r3`:
|
|
123
|
+
|
|
124
|
+
| Example | Macros | Frontend (`-O`, whole module) | `wasm-ld` | Snapshot |
|
|
125
|
+
|---|---|---|---|---|
|
|
126
|
+
| Counter, 3 files | | 3.7 s, 130 MB | 2.8 s, 409 MB | 111 ops, same |
|
|
127
|
+
| Filters | | 5.4 s, 137 MB | 2.8 s, 409 MB | 145 ops, same |
|
|
128
|
+
| Gallery | | 9.0 s, 144 MB | 3.0 s, 410 MB | 360 ops, same |
|
|
129
|
+
| PhotoPicker | | 4.8 s, 137 MB | 2.8 s, 413 MB | 61 ops, same |
|
|
130
|
+
| FoodTruck, 57 files | | 36.8 s, 227 MB | 2.7 s, 415 MB | 4064 ops, same |
|
|
131
|
+
| Settings | `@Observable` | 7.2 s, 143 MB | 2.8 s, 410 MB | 154 ops, same |
|
|
132
|
+
| Todos | `@Model`, `@Query` | 7.3 s, 142 MB | 2.7 s, 408 MB | 198 ops, same |
|
|
133
|
+
| Journal | SwiftData | 8.0 s, 145 MB | 2.7 s, 409 MB | 85 ops, same |
|
|
134
|
+
| Bookshelf | SwiftData, `#Predicate` | 9.0 s, 149 MB | 2.8 s, 415 MB | 341 ops, same |
|
|
135
|
+
| CupcakeCorner | `@Observable` | 6.9 s, 144 MB | 2.6 s, 409 MB | 104 ops, same |
|
|
136
|
+
| HackingWithSwift, 16 files | SwiftData, `@Observable` | 15.3 s, 172 MB | 2.7 s, 410 MB | 1182 ops, same |
|
|
137
|
+
| Fidelity | `@Observable`, `@Entry` | 17.9 s, 168 MB | 2.7 s, 412 MB | 635 ops, same |
|
|
138
|
+
| HotProspects + CodeScanner | SwiftData | 7.8 s + 4.2 s, 149 MB | 3.3 s, 424 MB | 167 ops, same |
|
|
139
|
+
|
|
140
|
+
Time is wall time under Node on the 4-core container (module cache warm);
|
|
141
|
+
memory is each tool's linear memory when it exited, which is its peak. The
|
|
142
|
+
native frontend compiles Counter in 1.1 s (below), so the WebAssembly one
|
|
143
|
+
runs at about a third of native speed. Linking the kit's archives whole
|
|
144
|
+
costs about 3 s and 410 MB whatever the app, which is cheap enough that
|
|
145
|
+
static linking stays (Milestone 4's dynamic linking is not needed for
|
|
146
|
+
time). In the dev server, Counter builds in 6.3 s and rebuilds on save.
|
|
147
|
+
|
|
148
|
+
### Macros
|
|
149
|
+
|
|
150
|
+
A native compiler runs a macro plugin as a process. Here the plugins are
|
|
151
|
+
WebAssembly too (`toolchain/build-plugins.sh`: the shim's SwiftDataMacros,
|
|
152
|
+
the toolchain's ObservationMacros and SwiftMacros, the standard
|
|
153
|
+
library's, each given a `@main`, and swift-testing's TestingMacros), built with
|
|
154
|
+
swift-syntax for WASI, and the host runs them in the compiler's own thread
|
|
155
|
+
(`src/plugins.ts`): the compiler, patched to ask the host instead of
|
|
156
|
+
spawning (`PluginRegistry.cpp`), imports `plugin_spawn`, `plugin_write`,
|
|
157
|
+
`plugin_read` and `plugin_close` from a `swiftbrowser` module; the host
|
|
158
|
+
calls the plugin's `swift_wasm_macro_v1_pump` export once per message, with
|
|
159
|
+
the message on its stdin and the reply taken from its stdout. Everything is
|
|
160
|
+
synchronous, so it works the same under Node and in a browser worker; the
|
|
161
|
+
e2e tests expand `@Observable` on both hosts, and CI does on a runner
|
|
162
|
+
without Swift. A compiler is named a plugin with
|
|
163
|
+
`-load-plugin-executable /plugins/ObservationMacros.wasm#ObservationMacros`;
|
|
164
|
+
the CLIs find the plugins in `plugins/` next to the tools.
|
|
165
|
+
|
|
166
|
+
Expanding macros needs the compiler built with swift-syntax and ASTGen
|
|
167
|
+
(`SWIFT_SYNTAX=ON toolchain/build-swift.sh`): swift-syntax as static
|
|
168
|
+
libraries, the regex parser from swift-experimental-string-processing, and
|
|
169
|
+
no plugin server or shared-library macros on WASI. The frontend grows from
|
|
170
|
+
168 to 187 MB (27.3 MB brotli); each plugin is about 41 MB (7.2 MB brotli).
|
|
171
|
+
|
|
172
|
+
### The module cache
|
|
173
|
+
|
|
174
|
+
The first compile that imports Foundation builds Clang modules for
|
|
175
|
+
SwiftShims, the WASI libc and Foundation's C parts: 13 MB of PCMs. On disk
|
|
176
|
+
(the CLIs) they stay in the module cache between builds. In memory (the
|
|
177
|
+
browser) a session starts empty, so `memoryHost({ moduleCache })` starts
|
|
178
|
+
from a prebuilt cache and `moduleCacheFiles()` returns the one a session
|
|
179
|
+
ends with. Every file in memory has the same timestamp, so a cache built
|
|
180
|
+
for the same files and flags validates in any session and nothing is
|
|
181
|
+
rebuilt; the e2e tests check that only the pruning timestamp changes.
|
|
182
|
+
`toolchain/prebuild-module-cache.mjs` builds one by type-checking a source
|
|
183
|
+
that imports Foundation and Observation (the cache directory is a hash of
|
|
184
|
+
the flags, and a type-check and a compile share it): 8 modules, 13.0 MB,
|
|
185
|
+
3.3 MB brotli, for the playground to ship with the SDK, built with the
|
|
186
|
+
flags it compiles with.
|
|
187
|
+
|
|
188
|
+
On the in-memory host, a program importing Foundation runs its frontend in
|
|
189
|
+
2.8 s with an empty cache and 1.0 s with the prebuilt one. Before
|
|
190
|
+
`writeAt` in `src/memory-host.ts`, the empty cache took 12.6 s:
|
|
191
|
+
browser_wasi_shim grows a file to exactly the size of each write, so
|
|
192
|
+
writing a 10 MB PCM in small writes copied it quadratically; writes now go
|
|
193
|
+
into a buffer that doubles.
|
|
194
|
+
|
|
195
|
+
### What the examples found
|
|
196
|
+
|
|
197
|
+
- FoodTruck: Swift sized `debug_value` and `alloc_stack` instructions
|
|
198
|
+
without the padding before their 8-byte-aligned trailing debug-expression
|
|
199
|
+
elements, which SROA then wrote past on a 32-bit host (fixed from tools
|
|
200
|
+
release `swift-6.4.0-r2`).
|
|
201
|
+
- HotProspects, built twice: LLVM maps a file read-only with
|
|
202
|
+
`MAP_NORESERVE` to compare an output with the one it replaces, which
|
|
203
|
+
wasi-libc's emulated `mmap` rejects, so rewriting an unchanged
|
|
204
|
+
`.swiftmodule` failed (fixed in `toolchain/patches/llvm-project`).
|
|
205
|
+
- The in-memory host: `browser_wasi_shim` reports readlink as unsupported
|
|
206
|
+
rather than "not a link", and grants directories no rights, both of which
|
|
207
|
+
the compiler's checks on a plugin's path trip over; `src/memory-host.ts`
|
|
208
|
+
answers as a real WASI host does.
|
|
209
|
+
|
|
210
|
+
## Building the tools
|
|
211
|
+
|
|
212
|
+
The scripts in `toolchain/` keep everything under `$WASMC_WORK` (about 30 GB),
|
|
213
|
+
in this order:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
export WASMC_WORK=~/wasmc
|
|
217
|
+
toolchain/fetch.sh # Swift 6.4.0, its Swift SDK for Wasm, wasmtime; the sources, patched
|
|
218
|
+
toolchain/build-native-tools.sh # llvm-tblgen and clang-tblgen for the build machine
|
|
219
|
+
toolchain/build-sysroot.sh # the Swift SDK's sysroot, with a libc++ that has threads
|
|
220
|
+
toolchain/build-resources.sh # the Swift SDK's resources, with a libc module map C++ interop can use
|
|
221
|
+
toolchain/build-llvm.sh # LLVM, Clang and LLD for a wasm32-wasip1 host
|
|
222
|
+
toolchain/check-llvm.sh # clang.wasm and lld.wasm compile and link a C program that runs
|
|
223
|
+
toolchain/build-cmark.sh # swift-cmark for the same host
|
|
224
|
+
toolchain/build-swift.sh # swift-frontend for the same host, and the LLVM libraries it links
|
|
225
|
+
toolchain/build-plugins.sh # the macro plugins, as WebAssembly
|
|
226
|
+
toolchain/build-manifest-api.sh # PackageDescription and CompilerPluginSupport for WebAssembly, for manifests
|
|
227
|
+
toolchain/install-tools.sh # swift-frontend.wasm, wasm-ld.wasm, the plugins and manifest-api into $WASMC_WORK/build/tools
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`toolchain/publish-tools.sh pack`, then `upload`, compresses the tools with
|
|
231
|
+
brotli and puts them in Cloudflare R2 (bucket `swiftbrowser-wasm-tools`,
|
|
232
|
+
under the release `versions.sh` names), and writes
|
|
233
|
+
`packages/wasm-tools/tools.json`, which pins them. `@swiftbrowser/wasm-tools`
|
|
234
|
+
(`packages/wasm-tools`) is how everything else gets them: its `fetch.mjs`
|
|
235
|
+
downloads them from R2 into the package and checks them, the npm release
|
|
236
|
+
publishes the package with them, and the `WebAssembly compiler` workflow
|
|
237
|
+
(`.github/workflows/wasm-compiler.yml`) uses them to compile HelloWasm on a
|
|
238
|
+
runner without Swift. The CLI takes them from that package when neither
|
|
239
|
+
`--tools` nor `SWIFTBROWSER_WASM_TOOLS` is set. To use the published tools
|
|
240
|
+
instead of building them:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_ACCOUNT_ID=... node packages/wasm-tools/fetch.mjs
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Then, to record the SDK files a compile opens, on the in-memory host:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
npm run build --workspace packages/wasm-compiler
|
|
250
|
+
ulimit -s unlimited
|
|
251
|
+
node --stack-size=65500 packages/wasm-compiler/toolchain/record-sdk-files.mjs \
|
|
252
|
+
--sdk <swift-sdk>/wasm32-unknown-wasip1 --tools $WASMC_WORK/build/tools \
|
|
253
|
+
--source Sources/HelloWasm/main.swift --out hello.json
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The native toolchain's clang compiles everything against the Swift SDK's
|
|
257
|
+
WASI sysroot, so the compiler's C++ and its Swift parts share one libc and
|
|
258
|
+
one libc++. The compiler's Swift parts (SwiftCompilerSources, mandatory
|
|
259
|
+
since Swift 6) are cross-compiled by the native `swiftc` with C++ interop.
|
|
260
|
+
`toolchain/patches` holds the changes to LLVM, Swift and swift-syntax;
|
|
261
|
+
after editing the sources under `$WASMC_WORK/src`, `toolchain/save-patches.sh`
|
|
262
|
+
writes them back, each ending its description with the base it applies to.
|
|
263
|
+
That base is pinned in `toolchain/versions.sh`: `SWIFT_TAG` and, per source
|
|
264
|
+
tree, the commit the tag names, which `fetch.sh` checks every clone
|
|
265
|
+
against. Moving to another Swift release means changing both, then
|
|
266
|
+
rebasing the patches.
|
|
267
|
+
|
|
268
|
+
## Tests
|
|
269
|
+
|
|
270
|
+
`npm run test:unit` checks the driver against the jobs the native driver and
|
|
271
|
+
clang run, recorded in `test/fixtures` by `toolchain/record-invocations.mjs`.
|
|
272
|
+
With `WASMC_WORK` set, `test/e2e.test.ts` also compiles, links and runs hello
|
|
273
|
+
world with the native tools, and with the WebAssembly tools once
|
|
274
|
+
`$WASMC_WORK/build/tools` holds them.
|
|
275
|
+
|
|
276
|
+
## Native baseline
|
|
277
|
+
|
|
278
|
+
Step 1.1 measured the native Swift 6.4.0 frontend and `wasm-ld` for
|
|
279
|
+
`wasm32-unknown-wasip1` (`toolchain/measure.py`: wall time and peak resident
|
|
280
|
+
memory), on a 4-core container that was also building LLVM, so the times
|
|
281
|
+
are on the slow side. The examples are staged by the CLI and compiled
|
|
282
|
+
against its kit, with the toolchain's macro plugins (`@Observable` needs
|
|
283
|
+
ObservationMacros).
|
|
284
|
+
|
|
285
|
+
| Input | Step | Time | Peak memory |
|
|
286
|
+
|---|---|---|---|
|
|
287
|
+
| HelloWasm | `-typecheck` | 0.06 s | 101 MB |
|
|
288
|
+
| HelloWasm | `-c -Onone` | 0.07 s | 114 MB |
|
|
289
|
+
| HelloWasm | `-c -O` | 0.14 s | 129 MB |
|
|
290
|
+
| HelloWasm | `wasm-ld` | 0.19 s | 119 MB |
|
|
291
|
+
| Counter, 3 files | `-typecheck`, whole module | 0.13 s | 138 MB |
|
|
292
|
+
| Counter | `-c -Onone`, one primary file | 0.20 s | 153 MB |
|
|
293
|
+
| Counter | `-c -O -wmo` | 1.11 s | 228 MB |
|
|
294
|
+
| Settings, 3 files | `-typecheck`, whole module | 0.30 s | 166 MB |
|
|
295
|
+
| Settings | `-c -O -wmo` | 1.61 s | 200 MB |
|
|
296
|
+
| HackingWithSwift, 16 files | `-typecheck`, whole module | 0.87 s | 186 MB |
|
|
297
|
+
| HackingWithSwift | `-c -O -wmo` | 5.19 s | 245 MB |
|
|
298
|
+
|
|
299
|
+
A compile stays far below the 4 GB a 32-bit WebAssembly memory allows, so
|
|
300
|
+
the compiler is built for wasm32; Memory64 is not needed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point of the `swiftbrowser-wasmc` command (see ../src/cli.ts).
|
|
3
|
+
import { main } from '../dist/cli.js';
|
|
4
|
+
|
|
5
|
+
// Node marks its WASI as experimental; every run would say so.
|
|
6
|
+
process.removeAllListeners('warning');
|
|
7
|
+
process.on('warning', (warning) => {
|
|
8
|
+
if (warning.name === 'ExperimentalWarning' && warning.message.includes('WASI')) return;
|
|
9
|
+
console.warn(warning);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
main(process.argv.slice(2)).then(
|
|
13
|
+
(code) => process.exit(code),
|
|
14
|
+
(error) => {
|
|
15
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
},
|
|
18
|
+
);
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv: string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// `swiftbrowser-wasmc`: compiles Swift sources into a WASI executable with
|
|
2
|
+
// the WebAssembly-hosted Swift compiler and linker, no native Swift needed.
|
|
3
|
+
// Milestone 1 of docs/in-browser-compiler.md. `--native-toolchain` runs the
|
|
4
|
+
// same jobs with a native toolchain instead, for comparison.
|
|
5
|
+
import { copyFile, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { compile } from './compile.js';
|
|
10
|
+
import { nativeHost } from './node/native-host.js';
|
|
11
|
+
import { findPlugins, packagedToolsDir, toolFile } from './node/tools.js';
|
|
12
|
+
import { runWasiCommand, wasiHost } from './node/wasi-host.js';
|
|
13
|
+
const USAGE = `usage: swiftbrowser-wasmc [options] <file.swift>...
|
|
14
|
+
|
|
15
|
+
Compiles Swift sources for wasm32-unknown-wasip1 with the Swift compiler and
|
|
16
|
+
linker built as WebAssembly, run under Node's WASI.
|
|
17
|
+
|
|
18
|
+
-o, --output FILE the executable to write (default: <module>.wasm)
|
|
19
|
+
--typecheck type-check only
|
|
20
|
+
--module-name NAME the module name (default: main)
|
|
21
|
+
--sdk DIR the Swift SDK's wasm32-unknown-wasip1 directory
|
|
22
|
+
(default: $SWIFTBROWSER_WASM_SDK)
|
|
23
|
+
--tools DIR the directory holding swift-frontend.wasm and
|
|
24
|
+
wasm-ld.wasm, or their .wasm.br (default:
|
|
25
|
+
$SWIFTBROWSER_WASM_TOOLS, then the installed
|
|
26
|
+
@swiftbrowser/wasm-tools); macro plugins are
|
|
27
|
+
taken from its plugins/ directory
|
|
28
|
+
--module-cache DIR keep Clang's module cache here between runs
|
|
29
|
+
(default: a temporary directory per run)
|
|
30
|
+
--native-toolchain DIR
|
|
31
|
+
run a native toolchain's tools instead
|
|
32
|
+
--run run the executable under Node's WASI after linking
|
|
33
|
+
-v, --verbose print each tool invocation and how long it took
|
|
34
|
+
-h, --help`;
|
|
35
|
+
export async function main(argv) {
|
|
36
|
+
const { values, positionals } = parseArgs({
|
|
37
|
+
args: argv,
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: {
|
|
40
|
+
output: { type: 'string', short: 'o' },
|
|
41
|
+
typecheck: { type: 'boolean' },
|
|
42
|
+
'module-name': { type: 'string' },
|
|
43
|
+
sdk: { type: 'string' },
|
|
44
|
+
tools: { type: 'string' },
|
|
45
|
+
'module-cache': { type: 'string' },
|
|
46
|
+
'native-toolchain': { type: 'string' },
|
|
47
|
+
run: { type: 'boolean' },
|
|
48
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
49
|
+
help: { type: 'boolean', short: 'h' },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (values.help) {
|
|
53
|
+
console.log(USAGE);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (positionals.length === 0) {
|
|
57
|
+
console.error(USAGE);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
const sdkRoot = values.sdk ?? process.env['SWIFTBROWSER_WASM_SDK'];
|
|
61
|
+
if (!sdkRoot)
|
|
62
|
+
throw new Error('no Swift SDK: pass --sdk or set SWIFTBROWSER_WASM_SDK');
|
|
63
|
+
const moduleName = values['module-name'] ?? 'main';
|
|
64
|
+
const output = values.typecheck ? undefined : path.resolve(values.output ?? `${moduleName}.wasm`);
|
|
65
|
+
const sources = positionals.map((file) => path.resolve(file));
|
|
66
|
+
const work = await mkdtemp(path.join(tmpdir(), 'wasmc-work-'));
|
|
67
|
+
try {
|
|
68
|
+
let host;
|
|
69
|
+
// Guest directory → host directory, to show diagnostics with real paths.
|
|
70
|
+
let guestToHost = [];
|
|
71
|
+
let guestSources;
|
|
72
|
+
let guestWork;
|
|
73
|
+
let frontendFlags = [];
|
|
74
|
+
if (values['native-toolchain']) {
|
|
75
|
+
host = nativeHost({ toolchain: path.resolve(values['native-toolchain']), sdkRoot: path.resolve(sdkRoot) });
|
|
76
|
+
guestSources = sources;
|
|
77
|
+
guestWork = work;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const toolsDir = values.tools ?? process.env['SWIFTBROWSER_WASM_TOOLS'] ?? packagedToolsDir();
|
|
81
|
+
// Each source directory is mounted once, at /src/0, /src/1, ...
|
|
82
|
+
const sourceDirs = [...new Set(sources.map((file) => path.dirname(file)))];
|
|
83
|
+
const mounts = { '/sdk': path.resolve(sdkRoot), '/work': work };
|
|
84
|
+
sourceDirs.forEach((dir, i) => (mounts[`/src/${i}`] = dir));
|
|
85
|
+
guestSources = sources.map((file) => `/src/${sourceDirs.indexOf(path.dirname(file))}/${path.basename(file)}`);
|
|
86
|
+
guestToHost = Object.entries(mounts).filter(([guest]) => guest.startsWith('/src/'));
|
|
87
|
+
guestWork = '/work';
|
|
88
|
+
// The macro plugins next to the tools (plugins/), mounted at /plugins.
|
|
89
|
+
const plugins = Object.entries(findPlugins(toolsDir));
|
|
90
|
+
if (plugins.length > 0)
|
|
91
|
+
mounts['/plugins'] = path.dirname(plugins[0][1]);
|
|
92
|
+
const guestPlugin = (file) => `/plugins/${path.basename(file)}`;
|
|
93
|
+
frontendFlags = plugins.flatMap(([name, file]) => ['-load-plugin-executable', `${guestPlugin(file)}#${name}`]);
|
|
94
|
+
host = wasiHost({
|
|
95
|
+
tools: { 'swift-frontend': toolFile(toolsDir, 'swift-frontend'), 'wasm-ld': toolFile(toolsDir, 'wasm-ld') },
|
|
96
|
+
mounts,
|
|
97
|
+
sdkRoot: '/sdk',
|
|
98
|
+
plugins: Object.fromEntries(plugins.map(([, file]) => [guestPlugin(file), file])),
|
|
99
|
+
...(values['module-cache'] ? { moduleCacheDir: path.resolve(values['module-cache']) } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (values.verbose)
|
|
103
|
+
console.error(`using ${host.description}`);
|
|
104
|
+
let started = performance.now();
|
|
105
|
+
let current = null;
|
|
106
|
+
const finishStep = () => {
|
|
107
|
+
if (values.verbose && current)
|
|
108
|
+
console.error(` ${current}: ${((performance.now() - started) / 1000).toFixed(2)} s`);
|
|
109
|
+
};
|
|
110
|
+
const result = await compile(host, {
|
|
111
|
+
moduleName,
|
|
112
|
+
sources: guestSources,
|
|
113
|
+
objectDir: guestWork,
|
|
114
|
+
...(output ? { output: `${guestWork}/${moduleName}.wasm` } : {}),
|
|
115
|
+
...(frontendFlags.length > 0 ? { frontendFlags } : {}),
|
|
116
|
+
onStep: (step) => {
|
|
117
|
+
finishStep();
|
|
118
|
+
current = step.tool;
|
|
119
|
+
started = performance.now();
|
|
120
|
+
if (values.verbose)
|
|
121
|
+
console.error(`${step.tool} ${step.args.join(' ')}`);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
finishStep();
|
|
125
|
+
if (result.output) {
|
|
126
|
+
let output = result.output;
|
|
127
|
+
for (const [guest, hostDir] of guestToHost)
|
|
128
|
+
output = output.split(`${guest}/`).join(`${hostDir}/`);
|
|
129
|
+
process.stderr.write(output);
|
|
130
|
+
}
|
|
131
|
+
if (!result.ok)
|
|
132
|
+
return 1;
|
|
133
|
+
if (!output)
|
|
134
|
+
return 0;
|
|
135
|
+
await copyFile(path.join(work, `${moduleName}.wasm`), output);
|
|
136
|
+
if (values.run) {
|
|
137
|
+
const module = await WebAssembly.compile(await readFile(output));
|
|
138
|
+
const run = await runWasiCommand({ module, args: [moduleName], mounts: {} });
|
|
139
|
+
process.stdout.write(run.output);
|
|
140
|
+
return run.exitCode;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await rm(work, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Host } from './host.js';
|
|
2
|
+
export interface CompileRequest {
|
|
3
|
+
moduleName: string;
|
|
4
|
+
/** Source paths as the host's tools see them. */
|
|
5
|
+
sources: readonly string[];
|
|
6
|
+
/** Where objects go, as the tools see it. */
|
|
7
|
+
objectDir: string;
|
|
8
|
+
/** The executable to write, as the tools see it; omit to type-check only. */
|
|
9
|
+
output?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Compile a library module instead of an executable: objects and this
|
|
12
|
+
* `.swiftmodule` (as the tools see it), for a later compile to import
|
|
13
|
+
* (`-I` its directory) and a later link to take the objects. Runs one
|
|
14
|
+
* whole-module job and links nothing.
|
|
15
|
+
*/
|
|
16
|
+
emitModule?: string;
|
|
17
|
+
/**
|
|
18
|
+
* With `emitModule`, whether the module's sources are a library's (the
|
|
19
|
+
* default); false keeps a `main.swift`'s top-level code, for an executable
|
|
20
|
+
* module built for its tests to import.
|
|
21
|
+
*/
|
|
22
|
+
parseAsLibrary?: boolean;
|
|
23
|
+
/** One frontend job for the whole module (`FrontendOptions.wholeModule`). */
|
|
24
|
+
wholeModule?: boolean;
|
|
25
|
+
/** The build's own frontend flags (`FrontendOptions.flags`). */
|
|
26
|
+
frontendFlags?: readonly string[];
|
|
27
|
+
/** Objects to link besides the module's own, as the tools see them. */
|
|
28
|
+
objects?: readonly string[];
|
|
29
|
+
/** The build's own linker flags (`LinkOptions.flags`). */
|
|
30
|
+
linkerFlags?: readonly string[];
|
|
31
|
+
/** Called with each step as it starts, for progress and timing. */
|
|
32
|
+
onStep?: (step: {
|
|
33
|
+
tool: string;
|
|
34
|
+
args: readonly string[];
|
|
35
|
+
}) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface CompileResult {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
/** Every tool's output, in order: the diagnostics. */
|
|
40
|
+
output: string;
|
|
41
|
+
/** Each step that ran: how long it took and, for a WebAssembly tool, its peak memory. */
|
|
42
|
+
steps: CompileStep[];
|
|
43
|
+
/** The objects the frontend wrote, as the tools see them. */
|
|
44
|
+
objects: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface CompileStep {
|
|
47
|
+
tool: string;
|
|
48
|
+
exitCode: number;
|
|
49
|
+
seconds: number;
|
|
50
|
+
memoryBytes?: number;
|
|
51
|
+
}
|
|
52
|
+
export declare function compile(host: Host, request: CompileRequest): Promise<CompileResult>;
|
|
53
|
+
export interface CompileCRequest {
|
|
54
|
+
/** C or C++ sources, as the tools see them. */
|
|
55
|
+
sources: readonly string[];
|
|
56
|
+
/** Where objects go: one per source, named for its path below `root`. */
|
|
57
|
+
objectDir: string;
|
|
58
|
+
/** The directory sources are named relative to, for their objects' names. */
|
|
59
|
+
root: string;
|
|
60
|
+
/** clang flags for every source (`ClangOptions.flags`). */
|
|
61
|
+
flags?: readonly string[];
|
|
62
|
+
/** Called with each step as it starts. */
|
|
63
|
+
onStep?: (step: {
|
|
64
|
+
tool: string;
|
|
65
|
+
args: readonly string[];
|
|
66
|
+
}) => void;
|
|
67
|
+
}
|
|
68
|
+
/** A C target's sources compiled with clang, one job per source, as SwiftPM builds one. */
|
|
69
|
+
export declare function compileC(host: Host, request: CompileCRequest): Promise<CompileResult>;
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// What `swiftc -static-stdlib -o app.wasm *.swift` does, as jobs on a host:
|
|
2
|
+
// the frontend once per source, the autolink entries read from the objects,
|
|
3
|
+
// then wasm-ld.
|
|
4
|
+
import { clangJob, frontendJobs, linkJob } from './driver.js';
|
|
5
|
+
import { extractAutolink } from './wasm.js';
|
|
6
|
+
export async function compile(host, request) {
|
|
7
|
+
const library = request.emitModule !== undefined;
|
|
8
|
+
const jobs = frontendJobs({
|
|
9
|
+
sdk: host.sdk,
|
|
10
|
+
moduleName: request.moduleName,
|
|
11
|
+
sources: request.sources,
|
|
12
|
+
action: request.output === undefined && !library ? 'typecheck' : 'compile',
|
|
13
|
+
objectDir: request.objectDir,
|
|
14
|
+
...(host.plugins ? { plugins: host.plugins } : {}),
|
|
15
|
+
...(host.moduleCachePath ? { moduleCachePath: host.moduleCachePath } : {}),
|
|
16
|
+
...(request.wholeModule || library ? { wholeModule: true } : {}),
|
|
17
|
+
flags: [
|
|
18
|
+
...(request.frontendFlags ?? []),
|
|
19
|
+
...(library ? [...(request.parseAsLibrary === false ? [] : ['-parse-as-library']), '-emit-module-path', request.emitModule] : []),
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
const objects = jobs.flatMap((job) => job.outputs);
|
|
23
|
+
let output = '';
|
|
24
|
+
const steps = [];
|
|
25
|
+
const run = async (job) => {
|
|
26
|
+
request.onStep?.(job);
|
|
27
|
+
const started = performance.now();
|
|
28
|
+
const result = await host.run(job.tool, job.args);
|
|
29
|
+
output += result.output;
|
|
30
|
+
steps.push({
|
|
31
|
+
tool: job.tool,
|
|
32
|
+
exitCode: result.exitCode,
|
|
33
|
+
seconds: (performance.now() - started) / 1000,
|
|
34
|
+
...(result.memoryBytes !== undefined ? { memoryBytes: result.memoryBytes } : {}),
|
|
35
|
+
});
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
for (const job of jobs) {
|
|
39
|
+
if ((await run(job)).exitCode !== 0)
|
|
40
|
+
return { ok: false, output, steps, objects };
|
|
41
|
+
}
|
|
42
|
+
if (request.output === undefined || library)
|
|
43
|
+
return { ok: true, output, steps, objects };
|
|
44
|
+
// The libraries the objects ask for, the build's own objects (other
|
|
45
|
+
// modules' among them) as much as the module's.
|
|
46
|
+
const linked = [...objects, ...(request.objects ?? [])];
|
|
47
|
+
const autolink = extractAutolink(await Promise.all(linked.map((object) => host.readFile(object))));
|
|
48
|
+
const staticExecutableArgs = new TextDecoder().decode(await host.readFile(host.sdk.staticExecutableArgs));
|
|
49
|
+
const link = linkJob({
|
|
50
|
+
sdk: host.sdk,
|
|
51
|
+
objects: linked,
|
|
52
|
+
autolink,
|
|
53
|
+
staticExecutableArgs,
|
|
54
|
+
output: request.output,
|
|
55
|
+
...(request.linkerFlags ? { flags: request.linkerFlags } : {}),
|
|
56
|
+
});
|
|
57
|
+
const result = await run(link);
|
|
58
|
+
return { ok: result.exitCode === 0, output, steps, objects };
|
|
59
|
+
}
|
|
60
|
+
/** A C target's sources compiled with clang, one job per source, as SwiftPM builds one. */
|
|
61
|
+
export async function compileC(host, request) {
|
|
62
|
+
let output = '';
|
|
63
|
+
const steps = [];
|
|
64
|
+
const objects = [];
|
|
65
|
+
for (const source of request.sources) {
|
|
66
|
+
const relative = source.startsWith(`${request.root}/`) ? source.slice(request.root.length + 1) : source.split('/').pop();
|
|
67
|
+
const job = clangJob({
|
|
68
|
+
sdk: host.sdk,
|
|
69
|
+
source,
|
|
70
|
+
output: `${request.objectDir}/${relative.replace(/\//g, '_')}.o`,
|
|
71
|
+
...(request.flags ? { flags: request.flags } : {}),
|
|
72
|
+
cxx: /\.(cc|cpp|cxx|c\+\+)$/.test(source),
|
|
73
|
+
});
|
|
74
|
+
request.onStep?.(job);
|
|
75
|
+
const started = performance.now();
|
|
76
|
+
const result = await host.run(job.tool, job.args);
|
|
77
|
+
output += result.output;
|
|
78
|
+
steps.push({
|
|
79
|
+
tool: job.tool,
|
|
80
|
+
exitCode: result.exitCode,
|
|
81
|
+
seconds: (performance.now() - started) / 1000,
|
|
82
|
+
...(result.memoryBytes !== undefined ? { memoryBytes: result.memoryBytes } : {}),
|
|
83
|
+
});
|
|
84
|
+
if (result.exitCode !== 0)
|
|
85
|
+
return { ok: false, output, steps, objects };
|
|
86
|
+
objects.push(...job.outputs);
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, output, steps, objects };
|
|
89
|
+
}
|