From e4f15ab55910130c8c61094a4cc0fb4b53e6e596 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 21 May 2026 21:03:53 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20integrate=20synth=20=E2=80=94=20promote?= =?UTF-8?q?=20synth=5Fcompile=20to=20a=20hermetic=20toolchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit synth (pulseengine/synth) published its first real release — v0.3.1, with 4 platform binaries + SHA256SUMS — so it can now be a first-class downloaded toolchain instead of a user-supplied binary. synth is the final stage of the PulseEngine pipeline: it AOT-compiles a WASM core module to an ARM Cortex-M ELF. - checksums/tools/synth.json — JSON registry entry, 4 platforms (no Windows), verified SHA256 - toolchains/tool_registry.bzl — synth URL pattern - toolchains/synth_toolchain.bzl + toolchains/BUILD.bazel — synth_toolchain_type - wasm/extensions.bzl — synth module extension - MODULE.bazel — register the synth toolchain - wasm/private/synth_compile.bzl — resolve the synth binary from the toolchain; the `synth` attr is now optional (escape hatch for a locally-built binary) rather than mandatory - examples/synth_example — compiles a core module to an ARM Cortex-M ELF Validated: //examples/synth_example:math_firmware builds — synth 0.3.1 downloads checksum-verified and emits a 32-bit ARM EABI5 ELF. Completes the tool-integration track (rivet DD-003): spar, witness, and now synth are all first-class toolchains — synth is no longer deferred. Co-Authored-By: Claude Opus 4.7 (1M context) --- MODULE.bazel | 10 +++ MODULE.bazel.lock | 46 +++++++--- checksums/tools/synth.json | 31 +++++++ examples/synth_example/BUILD.bazel | 29 +++++++ toolchains/BUILD.bazel | 6 ++ toolchains/synth_toolchain.bzl | 129 +++++++++++++++++++++++++++++ toolchains/tool_registry.bzl | 5 ++ wasm/extensions.bzl | 40 +++++++++ wasm/private/synth_compile.bzl | 22 +++-- 9 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 checksums/tools/synth.json create mode 100644 examples/synth_example/BUILD.bazel create mode 100644 toolchains/synth_toolchain.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 0f742aa9..8946c955 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -280,6 +280,16 @@ use_repo(witness, "witness_toolchain") register_toolchains("@witness_toolchain//:witness_toolchain") +# synth toolchain: WebAssembly-to-ARM ahead-of-time compiler +synth = use_extension("//wasm:extensions.bzl", "synth") +synth.register( + name = "synth", + version = "0.3.1", +) +use_repo(synth, "synth_toolchain") + +register_toolchains("@synth_toolchain//:synth_toolchain") + # MoonBit hermetic toolchain for WebAssembly component builds (dev only) # NOTE: Not eagerly loaded - MoonBit uses rolling /latest/ URLs that cause # checksum drift and build failures, same as componentize-py. diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index e39691bd..f2d7f810 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -284,7 +284,7 @@ }, "//wasm:extensions.bzl%binaryen": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "c5GClIZ+xfHSKFn9WL/02Ag7wuihInOMqTGH5CxR/U8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -308,7 +308,7 @@ }, "//wasm:extensions.bzl%cpp_component": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "ZtIrdMAeTaET/8t3kmG14kQp8U4FKMqhGB1+JS825Do=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -333,7 +333,7 @@ }, "//wasm:extensions.bzl%jco": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "MRHYkIS73wv1wYllXhdZBYX6dRIp7VySTL4edmOH2/M=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -358,7 +358,7 @@ }, "//wasm:extensions.bzl%meld": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "BnGoVHC5IiwLNc8XvxkOniYvo+CFiIGA27V/CaLd4Gw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -382,7 +382,7 @@ }, "//wasm:extensions.bzl%spar": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "mOtiPROZSFmtouSVXHtDGjJNUiB1I315FvLubt4AxCo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -404,9 +404,33 @@ ] } }, + "//wasm:extensions.bzl%synth": { + "general": { + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", + "usagesDigest": "eXuFY1lYMxkRiJiqviUHAStVodluY02BJpodlh+aNQ8=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "synth_toolchain": { + "repoRuleId": "@@//toolchains:synth_toolchain.bzl%synth_repository", + "attributes": { + "version": "0.3.1" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "//wasm:extensions.bzl%tinygo": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "esnFdrH+qxI9awhZ/uW4dIkm843wWmTCzO4b1pdmifs=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -430,7 +454,7 @@ }, "//wasm:extensions.bzl%wasi_sdk": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "4CDKvALAslODuYVBSBBBKgDbmaqWRdwJJ4yfDrzWeYg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -804,7 +828,7 @@ }, "//wasm:extensions.bzl%wasm_toolchain": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "Keg+j4249N8787+5NDDpsnW8S0P3hp9HSaC26H2SeJg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -838,7 +862,7 @@ }, "//wasm:extensions.bzl%wasmtime": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "W3m1ohl2c96vXpGAao2C6XIl3CGB+kZYZN2+bagGv0M=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -863,7 +887,7 @@ }, "//wasm:extensions.bzl%witness": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "vz4JVaRGcdcwU0XOHYN0kb54nuI69CDjEu/MXFK6r/g=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -887,7 +911,7 @@ }, "//wasm:extensions.bzl%wkg": { "general": { - "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "bzlTransitiveDigest": "8mvNFsd+OgwKwH39+OUS+IRxspZYjyviLWm+jNYQTXQ=", "usagesDigest": "ks+Q/IL0nntNP6PabzUcF0ruF4X9hTKhbmck/ueoPTg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/checksums/tools/synth.json b/checksums/tools/synth.json new file mode 100644 index 00000000..f199be8d --- /dev/null +++ b/checksums/tools/synth.json @@ -0,0 +1,31 @@ +{ + "tool_name": "synth", + "github_repo": "pulseengine/synth", + "description": "WebAssembly-to-ARM ahead-of-time compiler (WASM core module -> Cortex-M ELF)", + "latest_version": "0.3.1", + "last_checked": "2026-05-21T00:00:00Z", + "supported_platforms": ["darwin_amd64", "darwin_arm64", "linux_amd64", "linux_arm64"], + "versions": { + "0.3.1": { + "release_date": "2026-05-21", + "platforms": { + "darwin_amd64": { + "sha256": "ae2b5b9767304b30e4e7416a4e63fe0581940f3ef193ff9879838ff2fd4419a9", + "url_suffix": "x86_64-apple-darwin.tar.gz" + }, + "darwin_arm64": { + "sha256": "bc50fbd4e07585b10316d665a2b68e2c5ab2391e91f1bbec827de29a50ce5a6e", + "url_suffix": "aarch64-apple-darwin.tar.gz" + }, + "linux_amd64": { + "sha256": "521e05065f9058374e99e8dfd3d2fce29dd2bbe1a5e16d0e9b082c1a933a0af9", + "url_suffix": "x86_64-unknown-linux-gnu.tar.gz" + }, + "linux_arm64": { + "sha256": "938bf641daf4de8e9fa6aac9cf2b5d265f45db0570f81a5aa1b990469aa24515", + "url_suffix": "aarch64-unknown-linux-gnu.tar.gz" + } + } + } + } +} diff --git a/examples/synth_example/BUILD.bazel b/examples/synth_example/BUILD.bazel new file mode 100644 index 00000000..067cc77f --- /dev/null +++ b/examples/synth_example/BUILD.bazel @@ -0,0 +1,29 @@ +"""Example: compile a WASM core module to an ARM Cortex-M ELF with synth. + +synth is the final stage of the PulseEngine pipeline — it performs ahead-of- +time compilation from WebAssembly to bare-metal ARM machine code: + + ... -> meld (fuse to core module) -> synth_compile -> ARM Cortex-M ELF + +synth operates on core modules. Here the subject is a small pre-built core +module (math.wat -> math.wasm); in a real pipeline the `wasm_module` would be +a meld_fuse target. The synth binary comes from the registered toolchain +(pulseengine/synth releases) — no locally-built binary needed. +""" + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_wasm_component//wasm:defs.bzl", "synth_compile") + +package(default_visibility = ["//visibility:public"]) + +# math.wasm exports two plain-integer functions (see math.wat for the source). +synth_compile( + name = "math_firmware", + src = "math.wasm", + target = "cortex-m3", +) + +build_test( + name = "synth_example_test", + targets = [":math_firmware"], +) diff --git a/toolchains/BUILD.bazel b/toolchains/BUILD.bazel index 6a532bf5..d24d1885 100644 --- a/toolchains/BUILD.bazel +++ b/toolchains/BUILD.bazel @@ -102,6 +102,12 @@ toolchain_type( visibility = ["//visibility:public"], ) +# Toolchain type for synth (WebAssembly-to-ARM ahead-of-time compiler) +toolchain_type( + name = "synth_toolchain_type", + visibility = ["//visibility:public"], +) + # Bzl library for tool versions (single source of truth) bzl_library( name = "tool_versions", diff --git a/toolchains/synth_toolchain.bzl b/toolchains/synth_toolchain.bzl new file mode 100644 index 00000000..6c579f3a --- /dev/null +++ b/toolchains/synth_toolchain.bzl @@ -0,0 +1,129 @@ +# Copyright 2026 Ralf Anton Beier. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""synth toolchain: WebAssembly-to-ARM ahead-of-time compiler. + +synth compiles a WebAssembly core module to a bare-metal ARM Cortex-M ELF — +the final stage of the PulseEngine pipeline. It is distributed as per-platform +tar.gz archives with a flat layout (the `synth` binary at the archive root). +No Windows binary is published. +""" + +load("//checksums:registry.bzl", "validate_tool_exists") +load("//toolchains:tool_registry.bzl", "tool_registry") + +# Platforms where synth release archives are published (no Windows). +_SUPPORTED_PLATFORMS = [ + "darwin_amd64", + "darwin_arm64", + "linux_amd64", + "linux_arm64", +] + +def _synth_toolchain_impl(ctx): + """Implementation of synth_toolchain rule.""" + return [platform_common.ToolchainInfo( + synth = ctx.file.synth, + )] + +synth_toolchain = rule( + implementation = _synth_toolchain_impl, + attrs = { + "synth": attr.label( + allow_single_file = True, + executable = True, + cfg = "exec", + doc = "synth binary for WebAssembly-to-ARM compilation", + ), + }, + doc = "Declares a synth toolchain for ahead-of-time ARM compilation", +) + +_STUB_BUILD = '''"""synth toolchain stub: unsupported platform. + +synth publishes no release archive for this host (e.g. Windows), so we +register a toolchain marked incompatible with any target. Toolchain +resolution for synth_compile targets fails cleanly here; builds that never +touch synth_compile are unaffected. +""" + +load("@rules_wasm_component//toolchains:synth_toolchain.bzl", "synth_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["synth_stub"]) + +synth_toolchain( + name = "synth_toolchain_impl", + synth = "synth_stub", +) + +toolchain( + name = "synth_toolchain", + target_compatible_with = ["@platforms//:incompatible"], + toolchain = ":synth_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:synth_toolchain_type", +) +''' + +def _synth_repository_impl(repository_ctx): + """Download the synth archive and create a toolchain repository.""" + platform = tool_registry.detect_platform(repository_ctx) + version = repository_ctx.attr.version + + # Unsupported host (Windows / unknown): emit a stub so module resolution + # still works. synth_compile targets simply won't be buildable there. + if platform not in _SUPPORTED_PLATFORMS or not validate_tool_exists(repository_ctx, "synth", version, platform): + print("synth: no release archive for platform {} (version {}); emitting stub".format(platform, version)) + repository_ctx.file("synth_stub", content = "", executable = True) + repository_ctx.file("BUILD.bazel", _STUB_BUILD) + return + + print("Setting up synth {} for platform {}".format(version, platform)) + + # Extract the release archive into dist/ (flat layout: dist/synth). + tool_registry.download( + repository_ctx, + "synth", + version, + platform, + output_dir = "dist", + ) + + repository_ctx.file("BUILD.bazel", '''"""synth toolchain repository""" + +load("@rules_wasm_component//toolchains:synth_toolchain.bzl", "synth_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["dist/synth"]) + +synth_toolchain( + name = "synth_toolchain_impl", + synth = "dist/synth", +) + +toolchain( + name = "synth_toolchain", + exec_compatible_with = [], + target_compatible_with = [], + toolchain = ":synth_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:synth_toolchain_type", +) +''') + +synth_repository = repository_rule( + implementation = _synth_repository_impl, + attrs = { + "version": attr.string( + default = "0.3.1", + doc = "synth version to download", + ), + }, + doc = "Downloads synth and creates a toolchain repository", +) diff --git a/toolchains/tool_registry.bzl b/toolchains/tool_registry.bzl index b2844dc4..fb233706 100644 --- a/toolchains/tool_registry.bzl +++ b/toolchains/tool_registry.bzl @@ -153,6 +153,11 @@ _URL_PATTERNS = { "base": "https://github.com/{repo}/releases/download/v{version}", "filename": "witness-v{version}-{suffix}", }, + "synth": { + # synth releases are tar.gz archives: synth-v{version}-{triple}.tar.gz + "base": "https://github.com/{repo}/releases/download/v{version}", + "filename": "synth-v{version}-{suffix}", + }, } def _build_download_url(tool_name, version, platform, tool_info, github_repo): diff --git a/wasm/extensions.bzl b/wasm/extensions.bzl index 2bfaaec9..da822b51 100644 --- a/wasm/extensions.bzl +++ b/wasm/extensions.bzl @@ -7,6 +7,7 @@ load("//toolchains:jco_toolchain.bzl", "jco_toolchain_repository") load("//toolchains:meld_toolchain.bzl", "meld_repository") load("//toolchains:spar_toolchain.bzl", "spar_repository") load("//toolchains:witness_toolchain.bzl", "witness_repository") +load("//toolchains:synth_toolchain.bzl", "synth_repository") load("//toolchains:tinygo_toolchain.bzl", "tinygo_toolchain_repository") load("//toolchains:wasi_sdk_toolchain.bzl", "wasi_sdk_repository") load("//toolchains:wasm_toolchain.bzl", "wasm_toolchain_repository") @@ -778,3 +779,42 @@ witness = module_extension( ), }, ) + +def _synth_extension_impl(module_ctx): + """Implementation of the synth module extension.""" + registrations = {} + + for mod in module_ctx.modules: + for registration in mod.tags.register: + registrations[registration.name] = registration + + for name, registration in registrations.items(): + synth_repository( + name = name + "_toolchain", + version = registration.version, + ) + + if not registrations: + synth_repository( + name = "synth_toolchain", + version = "0.3.1", + ) + +# Module extension for synth (WebAssembly-to-ARM ahead-of-time compiler) +synth = module_extension( + implementation = _synth_extension_impl, + tag_classes = { + "register": tag_class( + attrs = { + "name": attr.string( + doc = "Name for this synth registration", + default = "synth", + ), + "version": attr.string( + doc = "synth version to use", + default = "0.3.1", + ), + }, + ), + }, +) diff --git a/wasm/private/synth_compile.bzl b/wasm/private/synth_compile.bzl index b8dd0091..e50f245e 100644 --- a/wasm/private/synth_compile.bzl +++ b/wasm/private/synth_compile.bzl @@ -14,8 +14,12 @@ def _synth_compile_impl(ctx): """Implementation of synth_compile rule.""" output_elf = ctx.actions.declare_file(ctx.attr.out or (ctx.label.name + ".elf")) - # Get the synth binary - synth = ctx.executable.synth + # Resolve the synth binary: an explicit `synth` attr (escape hatch for a + # locally-built binary) takes precedence over the registered toolchain. + if ctx.executable.synth: + synth = ctx.executable.synth + else: + synth = ctx.toolchains["@rules_wasm_component//toolchains:synth_toolchain_type"].synth # Determine input file if ctx.attr.wasm_module: @@ -88,6 +92,7 @@ def _synth_compile_impl(ctx): arguments = [args], mnemonic = "SynthCompile", progress_message = "Compiling WebAssembly to ARM ELF: %{label}", + tools = [synth], ) return [ @@ -162,13 +167,13 @@ cortex-m7dp, cortex-m55, cortex-r5, cortex-a53, riscv32imac""", allow_single_file = [".o", ".a"], ), "synth": attr.label( - doc = """The synth CLI binary. Synth has no published releases yet; users must -supply a locally built binary target (see pulseengine/synth for build instructions).""", + doc = "Optional explicit synth CLI binary, overriding the registered " + + "synth toolchain. Leave unset to use the hermetic toolchain.", executable = True, cfg = "exec", - mandatory = True, ), }, + toolchains = ["@rules_wasm_component//toolchains:synth_toolchain_type"], doc = """Compile a WebAssembly module to an ARM Cortex-M ELF binary using Synth. Synth performs ahead-of-time compilation from WebAssembly to bare-metal ARM @@ -187,9 +192,9 @@ Memory layout (Cortex-M): - RAM at 0x20000000: linear memory (R11=base) + stack (grows down) - R10 = memory size, R11 = memory base, R9 = globals base -Note: Synth does not yet have published releases. The `synth` attribute must -point to a locally-built synth binary target. See pulseengine/synth for build -instructions. +The synth binary is provided by the registered synth toolchain (downloaded +from pulseengine/synth releases). Set the optional `synth` attribute only to +override it with a locally-built binary. Example: load("@rules_wasm_component//wasm:defs.bzl", "synth_compile") @@ -201,7 +206,6 @@ Example: loom_compat = True, link = True, builtins = "@kiln//builtins:kiln_builtins", - synth = "@synth//:synth", ) """, )