@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 CHANGED
@@ -1,3 +1,300 @@
1
- # Temporary Holding Version
1
+ # @swiftbrowser/wasm-compiler
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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>;
@@ -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
+ }