From a3cfde6fef76d90a0c3191fdb17a8b3a7623176e Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 6 Oct 2026 16:19:30 +0800 Subject: [PATCH 1/2] wasm: enable browser LTO and qualify JS SIMD profiles --- .github/workflows/llgo.yml | 8 +++ dev/test_wasm_simd.sh | 30 +++++++++++ internal/build/build.go | 24 +++++++++ internal/build/package_archive_test.go | 63 ++++++++++++++++++++++ internal/crosscompile/crosscompile.go | 4 ++ internal/crosscompile/crosscompile_test.go | 20 +++++++ test/simd/README.md | 17 +++++- 7 files changed, 165 insertions(+), 1 deletion(-) create mode 100755 dev/test_wasm_simd.sh diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 4d2e8a2074..b8fe6dc64d 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -959,6 +959,14 @@ jobs: -v -count=1 -timeout=2m ./test/simd/... done + - name: Test SIMD on JavaScript targets with off, Thin and Full LTO + if: matrix.suite == 'test-command' + shell: bash + env: + LLGO: ${{ runner.temp }}/llgo-bin/llgo + LLGO_BUILD_CACHE: "1" + run: dev/test_wasm_simd.sh + - name: Compare JavaScript call semantics with Go if: matrix.suite == 'test-command' shell: bash diff --git a/dev/test_wasm_simd.sh b/dev/test_wasm_simd.sh new file mode 100755 index 0000000000..eff4c0495f --- /dev/null +++ b/dev/test_wasm_simd.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +: "${LLGO:=llgo}" +export GOEXPERIMENT=simd + +for profile in gojs emscripten emscripten-memory64; do + ( + # Avoid inheriting a host target from the caller for raw GoJS builds. + export GOOS=js GOARCH=wasm CGO_ENABLED=0 + target=(-emulator) + if [[ "$profile" != gojs ]]; then + target+=(-target "$profile") + fi + echo "SIMD: $profile O0 boundary" + # The full O0 test binary exceeds Node's per-function local-variable limit. + "$LLGO" run -O0 "${target[@]}" ./test/simd/testdata/boundary + for lto in off thin full; do + echo "SIMD: $profile O2 LTO=$lto" + flags=(-O2) + if [[ "$lto" != off ]]; then + flags+=("-lto=$lto") + fi + "$LLGO" test "${flags[@]}" "${target[@]}" \ + -v -count=1 -timeout=2m -pclntab=none ./test/simd/... + done + ) +done diff --git a/internal/build/build.go b/internal/build/build.go index 552d9fee1d..5de3dfae85 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -2959,6 +2959,9 @@ func (c *context) archiver() string { if ar := os.Getenv("LLGO_AR"); ar != "" { return ar } + if ar := c.emscriptenArchiver(); ar != "" { + return ar + } // First check toolchain directory (for cross-compilation) if llvmAr := siblingTool(c.crossCompile.CC, "llvm-ar"); llvmAr != "" { return llvmAr @@ -2979,6 +2982,9 @@ func (c *context) archiveMerger() (string, error) { if ar := os.Getenv("LLGO_AR"); ar != "" { return ar, nil } + if ar := c.emscriptenArchiver(); ar != "" { + return ar, nil + } if llvmAr := siblingTool(c.crossCompile.CC, "llvm-ar"); llvmAr != "" { return llvmAr, nil } @@ -2988,6 +2994,24 @@ func (c *context) archiveMerger() (string, error) { return "", errors.New("llvm-ar is required to create a flat c-archive") } +// Emscripten C++ bitcode can be newer than LLGo's linked LLVM. Its emar +// wrapper selects the SDK's matching llvm-ar for both objects and MRI merges. +func (c *context) emscriptenArchiver() string { + provider := c.crossCompile.WasmProvider + if provider != crosscompile.WasmProviderGoJS && provider != crosscompile.WasmProviderEmscripten { + return "" + } + if cc, err := exec.LookPath(c.crossCompile.CC); err == nil { + if ar, err := exec.LookPath(filepath.Join(filepath.Dir(cc), "emar")); err == nil { + return ar + } + } + if ar, err := exec.LookPath("emar"); err == nil { + return ar + } + return "" +} + func siblingTool(compiler, name string) string { if compiler == "" { return "" diff --git a/internal/build/package_archive_test.go b/internal/build/package_archive_test.go index c51fa941f3..8d5c1d5a4b 100644 --- a/internal/build/package_archive_test.go +++ b/internal/build/package_archive_test.go @@ -251,3 +251,66 @@ func TestNormalizeToArchiveFailsWithoutObjectFallback(t *testing.T) { t.Fatalf("ArchiveFile = %q after failure", pkg.ArchiveFile) } } + +func TestEmscriptenArchiverUsesSDK(t *testing.T) { + root := t.TempDir() + tool := func(dir, name string) string { + t.Helper() + if runtime.GOOS == "windows" { + name += ".exe" + } + path := filepath.Join(root, dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("unused tool fixture"), 0755); err != nil { + t.Fatal(err) + } + return path + } + cc := tool("sdk", "emcc") + emar := tool("sdk", "emar") + hostAR := tool("host", "llvm-ar") + t.Setenv("PATH", filepath.Dir(hostAR)) + t.Setenv("LLGO_AR", "") + for _, provider := range []crosscompile.WasmProvider{crosscompile.WasmProviderGoJS, crosscompile.WasmProviderEmscripten} { + ctx := &context{buildConf: &Config{Goarch: "wasm"}, crossCompile: crosscompile.Export{CC: cc, WasmProvider: provider}} + if got := ctx.archiver(); got != emar { + t.Fatalf("%s archiver = %q, want SDK emar %q", provider, got, emar) + } + if got, err := ctx.archiveMerger(); err != nil || got != emar { + t.Fatalf("%s MRI archiver = %q, %v; want %q", provider, got, err, emar) + } + } + // A command found through PATH should still select its own SDK first. + t.Setenv("PATH", filepath.Dir(cc)+string(os.PathListSeparator)+filepath.Dir(hostAR)) + ctx := &context{crossCompile: crosscompile.Export{CC: filepath.Base(cc), WasmProvider: crosscompile.WasmProviderGoJS}} + if got := ctx.emscriptenArchiver(); got != emar { + t.Fatalf("PATH compiler archiver = %q, want %q", got, emar) + } + // A compiler wrapper outside the SDK can use emar from PATH. + ctx.crossCompile.CC = tool("wrapper", "emcc") + if got := ctx.emscriptenArchiver(); got != emar { + t.Fatalf("wrapped compiler archiver = %q, want %q", got, emar) + } + ctx.crossCompile.WasmProvider = crosscompile.WasmProviderNone + if got := ctx.emscriptenArchiver(); got != "" { + t.Fatalf("non-Emscripten target selected %q", got) + } + t.Setenv("PATH", filepath.Dir(hostAR)) + ctx = &context{buildConf: &Config{Goarch: "wasm"}, crossCompile: crosscompile.Export{CC: "missing-emcc", WasmProvider: crosscompile.WasmProviderGoJS}} + if got := ctx.archiver(); got != hostAR { + t.Fatalf("missing SDK fallback = %q, want %q", got, hostAR) + } + if got, err := ctx.archiveMerger(); err != nil || got != hostAR { + t.Fatalf("missing SDK MRI fallback = %q, %v; want %q", got, err, hostAR) + } + t.Setenv("LLGO_AR", hostAR) + ctx = &context{crossCompile: crosscompile.Export{CC: cc, WasmProvider: crosscompile.WasmProviderGoJS}} + if got := ctx.archiver(); got != hostAR { + t.Fatalf("ignored explicit LLGO_AR: %q", got) + } + if got, err := ctx.archiveMerger(); err != nil || got != hostAR { + t.Fatalf("MRI ignored explicit LLGO_AR: %q, %v", got, err) + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index cbed540518..ebe0e85999 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -889,6 +889,10 @@ func useWithGOARMAndToolchain(goos, goarch, goarm string, forceEspClang bool, le // before an overflow can overwrite another goroutine's memory. "-sSTACK_OVERFLOW_CHECK=2", }...) + if ltoMode.Enabled() { + export.CCFLAGS = append(export.CCFLAGS, ltoMode.ClangFlag()) + export.LDFLAGS = append(export.LDFLAGS, ltoMode.ClangFlag()) + } appendEmscriptenLibffiSearchPath(&export, llgoRoot, wasmProfile) default: err = errors.New("unsupported GOOS for WebAssembly: " + goos) diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 9b3e8cada3..6db888f9f6 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -1457,3 +1457,23 @@ func TestUseTargetCodegenFlagsOnlyAddedToLDFlagsWithLTO(t *testing.T) { t.Fatalf("missing full LTO ccflag: %v", fullLTO.CCFLAGS) } } + +func TestEmscriptenLTOFlags(t *testing.T) { + for _, target := range []string{"", "emscripten", "emscripten-memory64"} { + for _, mode := range []lto.Mode{lto.Off, lto.Thin, lto.Full} { + t.Run(target+"/"+mode.String(), func(t *testing.T) { + export, err := Use("js", "wasm", target, false, optlevel.O2, mode, false) + if err != nil { + t.Fatal(err) + } + for name, flags := range map[string][]string{"compile": export.CCFLAGS, "link": export.LDFLAGS} { + for _, candidate := range []lto.Mode{lto.Thin, lto.Full} { + if slices.Contains(flags, candidate.ClangFlag()) != (mode == candidate) { + t.Errorf("%s flags for %s: incorrect %s in %v", name, mode, candidate.ClangFlag(), flags) + } + } + } + }) + } + } +} diff --git a/test/simd/README.md b/test/simd/README.md index 72b53ffad3..4e3f4779cc 100644 --- a/test/simd/README.md +++ b/test/simd/README.md @@ -63,7 +63,21 @@ threads, and standard Wasm exception handling; Wasmer selects an available backend automatically. The official Go WASI comparison uses Wasmtime. Emscripten execution requires a compatible SDK and Node.js. The local qualification used Go 1.27.0, LLVM 22.1.8, Emscripten 6.0.8, and Node 24.19.0. -Existing CI runs native amd64/arm64 and WASI at O0/O2. +CI runs native amd64/arm64 and WASI at O0/O2. The JavaScript matrix runs +GoJS (`GOOS=js GOARCH=wasm`), Emscripten, and Emscripten Memory64 with the +default JavaScript SjLj/Asyncify configuration: + +```sh +LLGO=/path/to/llgo dev/test_wasm_simd.sh +``` + +Each profile runs the O0 boundary executable and the complete O2 test suite +with LTO disabled, ThinLTO, and Full LTO. The LTO modes pass `-flto=thin` or +`-flto=full` to both compilation and final linking. Earlier browser builds +accepted `-lto` but did not forward these driver flags, so their successful +runs did not qualify actual link-time optimization. Archives use the SDK's +`emar` to index bitcode produced by its Clang; `LLGO_AR` remains an explicit +override. The complete O0 Emscripten test executable exceeds Node's local-variable limit. A small executable covers SIMD initialization, cross-package calls, @@ -72,6 +86,7 @@ recovery, and scheduling at O0 without importing the testing framework: ```sh GOEXPERIMENT=simd llgo run -O0 -target wasi -emulator ./test/simd/testdata/boundary GOEXPERIMENT=simd llgo run -O0 -target emscripten -emulator ./test/simd/testdata/boundary +GOEXPERIMENT=simd llgo run -O0 -target emscripten-memory64 -emulator ./test/simd/testdata/boundary GOEXPERIMENT=simd GOOS=js GOARCH=wasm llgo run -O0 -emulator ./test/simd/testdata/boundary GOEXPERIMENT=simd llgo run -O2 -lto=thin -target emscripten -emulator ./test/simd/testdata/boundary GOEXPERIMENT=simd llgo run -O2 -lto=full -target emscripten -emulator ./test/simd/testdata/boundary From 6cb2cbe98e0c4a6532c0695c406900aebae43d36 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 6 Oct 2026 18:05:57 +0800 Subject: [PATCH 2/2] wasm: forward LTO levels and preserve SIMD bridge parameters --- dev/test_wasm_simd.sh | 5 +++ internal/build/build.go | 7 +++- internal/build/package_archive_test.go | 13 ++++++- internal/build/wasm_simd_calls.go | 6 ++- internal/build/wasm_simd_calls_test.go | 43 ++++++++++++++++++++++ internal/crosscompile/crosscompile.go | 6 +++ internal/crosscompile/crosscompile_test.go | 40 ++++++++++++++------ test/simd/README.md | 17 ++++++--- 8 files changed, 116 insertions(+), 21 deletions(-) diff --git a/dev/test_wasm_simd.sh b/dev/test_wasm_simd.sh index eff4c0495f..85946028e8 100755 --- a/dev/test_wasm_simd.sh +++ b/dev/test_wasm_simd.sh @@ -26,5 +26,10 @@ for profile in gojs emscripten emscripten-memory64; do "$LLGO" test "${flags[@]}" "${target[@]}" \ -v -count=1 -timeout=2m -pclntab=none ./test/simd/... done + for lto in thin full; do + echo "SIMD: $profile O3 LTO=$lto boundary" + # O3 argument promotion must preserve the JS SjLj memory bridge. + "$LLGO" run -O3 "-lto=$lto" "${target[@]}" ./test/simd/testdata/boundary + done ) done diff --git a/internal/build/build.go b/internal/build/build.go index 5de3dfae85..c966aefac2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -2994,14 +2994,17 @@ func (c *context) archiveMerger() (string, error) { return "", errors.New("llvm-ar is required to create a flat c-archive") } -// Emscripten C++ bitcode can be newer than LLGo's linked LLVM. Its emar -// wrapper selects the SDK's matching llvm-ar for both objects and MRI merges. +// Both GoJS and the named Emscripten targets build C/C++ through emcc. That +// bitcode can be newer than LLGo's linked LLVM, so use the SDK's emar wrapper +// to select its matching llvm-ar for both objects and MRI merges. func (c *context) emscriptenArchiver() string { provider := c.crossCompile.WasmProvider if provider != crosscompile.WasmProviderGoJS && provider != crosscompile.WasmProviderEmscripten { return "" } if cc, err := exec.LookPath(c.crossCompile.CC); err == nil { + // LookPath handles the SDK's emar.bat via PATHEXT on Windows; + // siblingTool only handles native executables such as llvm-ar.exe. if ar, err := exec.LookPath(filepath.Join(filepath.Dir(cc), "emar")); err == nil { return ar } diff --git a/internal/build/package_archive_test.go b/internal/build/package_archive_test.go index 8d5c1d5a4b..80cb432212 100644 --- a/internal/build/package_archive_test.go +++ b/internal/build/package_archive_test.go @@ -257,7 +257,11 @@ func TestEmscriptenArchiverUsesSDK(t *testing.T) { tool := func(dir, name string) string { t.Helper() if runtime.GOOS == "windows" { - name += ".exe" + if name == "emcc" || name == "emar" { + name += ".bat" + } else { + name += ".exe" + } } path := filepath.Join(root, dir, name) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { @@ -268,10 +272,15 @@ func TestEmscriptenArchiverUsesSDK(t *testing.T) { } return path } + if runtime.GOOS == "windows" { + t.Setenv("PATHEXT", ".EXE;.BAT;.CMD") + } cc := tool("sdk", "emcc") emar := tool("sdk", "emar") hostAR := tool("host", "llvm-ar") - t.Setenv("PATH", filepath.Dir(hostAR)) + otherEMAR := tool("other-sdk", "emar") + // A different SDK on PATH must not override emcc's sibling emar. + t.Setenv("PATH", filepath.Dir(otherEMAR)+string(os.PathListSeparator)+filepath.Dir(hostAR)) t.Setenv("LLGO_AR", "") for _, provider := range []crosscompile.WasmProvider{crosscompile.WasmProviderGoJS, crosscompile.WasmProviderEmscripten} { ctx := &context{buildConf: &Config{Goarch: "wasm"}, crossCompile: crosscompile.Export{CC: cc, WasmProvider: provider}} diff --git a/internal/build/wasm_simd_calls.go b/internal/build/wasm_simd_calls.go index 4da6fe5aad..2ec1a57272 100644 --- a/internal/build/wasm_simd_calls.go +++ b/internal/build/wasm_simd_calls.go @@ -152,7 +152,7 @@ func bridgeEmscriptenSIMDCall(mod llvm.Module, call llvm.Value, id int) { types, args = append(types, typ), append(args, arg) } // Each bridge retains the original call and its complete ABI attributes. - // noinline/optnone keep late optimization from exposing a v128 JS call again. + // noinline/optnone keep late inlining from exposing a v128 JS call again. bridgeType := llvm.FunctionType(retType, types, false) bridge := llvm.AddFunction(mod, fmt.Sprintf("__llgo_simd_sjlj.%d", id), bridgeType) bridge.SetLinkage(llvm.InternalLinkage) @@ -174,6 +174,10 @@ func bridgeEmscriptenSIMDCall(mod llvm.Module, call llvm.Value, id int) { arg := bridge.Param(paramOffset + i) if typ.TypeKind() == llvm.VectorTypeKind { arg = b.CreateLoad(typ, arg, "") + // LLVM 22's O3 argument promotion ignores optnone and can turn + // this pointer back into a v128 parameter. A volatile load keeps + // the memory ABI required by the JavaScript SjLj wrapper. + arg.SetVolatile(true) } call.SetOperand(i, arg) } diff --git a/internal/build/wasm_simd_calls_test.go b/internal/build/wasm_simd_calls_test.go index 386a879dfb..9e3b139460 100644 --- a/internal/build/wasm_simd_calls_test.go +++ b/internal/build/wasm_simd_calls_test.go @@ -153,3 +153,46 @@ func TestEmscriptenSIMDCallBridgeMode(t *testing.T) { }) } } + +func TestEmscriptenSIMDCallBridgeArgumentPromotion(t *testing.T) { + const source = ` +target datalayout = "e-p:64:64-i64:64-n32:64-S128" +target triple = "wasm64-unknown-emscripten" +declare i32 @setjmp(ptr) returns_twice +declare <4 x float> @callee(<4 x float>) +define <4 x float> @withjmp(ptr %jmp, <4 x float> %x) "target-features"="+simd128" { + %saved = call i32 @setjmp(ptr %jmp) + %v = call <4 x float> @callee(<4 x float> %x) + ret <4 x float> %v +} +` + for _, pipeline := range []string{"cgscc(argpromotion)", "thinlto-pre-link", "lto-pre-link"} { + t.Run(pipeline, func(t *testing.T) { + mod := parseWasmAggregateIR(t, source) + ctx := &context{crossCompile: crosscompile.Export{ + WasmProvider: crosscompile.WasmProviderEmscripten, + WasmProfile: crosscompile.WasmProfileJ64, + }} + if got := lowerEmscriptenSIMDCalls(ctx, mod); got != 1 { + t.Fatalf("bridges = %d, want 1", got) + } + opts := llvm.NewPassBuilderOptions() + defer opts.Dispose() + if err := mod.RunPasses(pipeline, llvm.TargetMachine{}, opts); err != nil { + t.Fatal(err) + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + bridge := mod.NamedFunction("__llgo_simd_sjlj.0") + if bridge.IsNil() || bridge.GlobalValueType().ReturnType().TypeKind() != llvm.VoidTypeKind { + t.Fatalf("lost the memory bridge:\n%s", mod.String()) + } + for _, param := range bridge.GlobalValueType().ParamTypes() { + if param.TypeKind() == llvm.VectorTypeKind { + t.Fatalf("late optimization promoted a bridge pointer to v128:\n%s", bridge.String()) + } + } + }) + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index ebe0e85999..2eb8d5be1a 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -892,6 +892,12 @@ func useWithGOARMAndToolchain(goos, goarch, goarm string, forceEspClang bool, le if ltoMode.Enabled() { export.CCFLAGS = append(export.CCFLAGS, ltoMode.ClangFlag()) export.LDFLAGS = append(export.LDFLAGS, ltoMode.ClangFlag()) + // emcc's -O controls its compile and post-link pipelines, but can + // leave wasm-ld's LTO optimizer at its default O2 (e.g. SDK 6.0.8). + // Set the LTO level explicitly, as for the WASI linker above. + if optFlag := ltoLinkerOptFlag(level); optFlag != "" { + export.LDFLAGS = append(export.LDFLAGS, "-Wl,"+optFlag) + } } appendEmscriptenLibffiSearchPath(&export, llgoRoot, wasmProfile) default: diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 6db888f9f6..54e5f1a7ab 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -1461,19 +1461,37 @@ func TestUseTargetCodegenFlagsOnlyAddedToLDFlagsWithLTO(t *testing.T) { func TestEmscriptenLTOFlags(t *testing.T) { for _, target := range []string{"", "emscripten", "emscripten-memory64"} { for _, mode := range []lto.Mode{lto.Off, lto.Thin, lto.Full} { - t.Run(target+"/"+mode.String(), func(t *testing.T) { - export, err := Use("js", "wasm", target, false, optlevel.O2, mode, false) - if err != nil { - t.Fatal(err) - } - for name, flags := range map[string][]string{"compile": export.CCFLAGS, "link": export.LDFLAGS} { - for _, candidate := range []lto.Mode{lto.Thin, lto.Full} { - if slices.Contains(flags, candidate.ClangFlag()) != (mode == candidate) { - t.Errorf("%s flags for %s: incorrect %s in %v", name, mode, candidate.ClangFlag(), flags) + for _, level := range []optlevel.Level{optlevel.O0, optlevel.O1, optlevel.O2, optlevel.O3, optlevel.Os, optlevel.Oz} { + t.Run(target+"/"+mode.String()+"/"+level.Name(), func(t *testing.T) { + export, err := Use("js", "wasm", target, false, level, mode, false) + if err != nil { + t.Fatal(err) + } + for name, flags := range map[string][]string{"compile": export.CCFLAGS, "link": export.LDFLAGS} { + if !slices.Contains(flags, level.Flag()) { + t.Errorf("%s flags do not preserve %s: %v", name, level, flags) + } + for _, candidate := range []lto.Mode{lto.Thin, lto.Full} { + if slices.Contains(flags, candidate.ClangFlag()) != (mode == candidate) { + t.Errorf("%s flags for %s: incorrect %s in %v", name, mode, candidate.ClangFlag(), flags) + } } } - } - }) + var got []string + for _, flag := range export.LDFLAGS { + if strings.HasPrefix(flag, "-Wl,--lto-O") { + got = append(got, flag) + } + } + var want []string + if mode.Enabled() { + want = []string{"-Wl," + ltoLinkerOptFlag(level)} + } + if !slices.Equal(got, want) { + t.Errorf("LTO optimizer flags = %v, want %v", got, want) + } + }) + } } } } diff --git a/test/simd/README.md b/test/simd/README.md index 4e3f4779cc..f11add414a 100644 --- a/test/simd/README.md +++ b/test/simd/README.md @@ -71,9 +71,14 @@ default JavaScript SjLj/Asyncify configuration: LLGO=/path/to/llgo dev/test_wasm_simd.sh ``` -Each profile runs the O0 boundary executable and the complete O2 test suite -with LTO disabled, ThinLTO, and Full LTO. The LTO modes pass `-flto=thin` or -`-flto=full` to both compilation and final linking. Earlier browser builds +Each profile runs the O0 boundary executable, the complete O2 test suite +with LTO disabled, ThinLTO, and Full LTO, and the O3 boundary executable with +ThinLTO and Full LTO. The O3 runs guard against late argument promotion +replacing a bridge pointer with a vector parameter. The LTO modes pass `-flto=thin` or +`-flto=full` to both compilation and final linking. The requested `-O` level +also reaches compilation and emcc's post-link pipeline. LTO additionally gets +an explicit `--lto-O0` through `--lto-O3`; `-Os`/`-Oz` use `--lto-O2` and retain +their size attributes and post-link size optimizations. Earlier browser builds accepted `-lto` but did not forward these driver flags, so their successful runs did not qualify actual link-time optimization. Archives use the SDK's `emar` to index bitcode produced by its Clang; `LLGO_AR` remains an explicit @@ -96,8 +101,10 @@ The default GoJS and Emscripten JavaScript SjLj wrappers cannot carry `v128`. Ca containing `setjmp` use a memory bridge for vector arguments/results; ordinary Wasm vector calls retain their vector ABI. These bridges cannot be inlined, and bodies retaining vector calls cannot be moved into recovery functions by -a later backend/LTO inliner. The earlier LLVM optimization still runs at the -requested level. When compilation and linking select native Wasm SjLj (for +a later backend/LTO inliner. Volatile vector loads in the bridge also prevent +LLVM 22's O3 argument promotion from replacing its pointer parameters with +vectors, even though the bridge is `optnone`. The earlier LLVM optimization +still runs at the requested level. When compilation and linking select native Wasm SjLj (for example, `EMCC_CFLAGS='-fwasm-exceptions -sSUPPORT_LONGJMP=wasm'`), these bridges and late-inlining restrictions are unnecessary and omitted. Memory64 IR still uses explicit JS SjLj codegen and retains the bridge.