From 90df8a97253fb642d038d504663dc2b673c66b88 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 20 May 2026 20:12:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20integrate=20spar=20=E2=80=94=20AADL?= =?UTF-8?q?-to-WIT=20generation=20(aadl=5Fwit=5Flibrary=20rule)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds spar (pulseengine/spar v0.9.3) as a first-class toolchain and rule. spar generates WIT interfaces from AADL v2.3 architecture models — the front of the PulseEngine certification pipeline (see docs/certification-pipeline.md). - checksums/tools/spar.json — JSON registry entry, 5 platforms, verified SHA256 - toolchains/tool_registry.bzl — spar URL pattern (archive: spar-v{version}-{triple}) - toolchains/spar_toolchain.bzl + toolchains/BUILD.bazel — spar_toolchain_type - wasm/extensions.bzl — spar module extension - MODULE.bazel — register the spar toolchain - wasm/private/aadl_wit_library.bzl — the aadl_wit_library rule: runs `spar codegen --format wit`, emits the generated WIT as a tree artifact - wasm/defs.bzl — export aadl_wit_library - examples/spar_example — AADL building-control model -> generated WIT Validated: //examples/spar_example:building_control_wit builds — spar 0.9.3 downloads checksum-verified, codegen produces wit/tcp.wit from the model. Part of the tool-integration track (rivet DD-003); witness is next. Co-Authored-By: Claude Opus 4.7 (1M context) --- MODULE.bazel | 10 ++ checksums/tools/spar.json | 35 +++++ examples/spar_example/BUILD.bazel | 26 ++++ examples/spar_example/building_control.aadl | 147 ++++++++++++++++++++ toolchains/BUILD.bazel | 6 + toolchains/spar_toolchain.bzl | 131 +++++++++++++++++ toolchains/tool_registry.bzl | 5 + wasm/defs.bzl | 5 + wasm/extensions.bzl | 40 ++++++ wasm/private/aadl_wit_library.bzl | 83 +++++++++++ 10 files changed, 488 insertions(+) create mode 100644 checksums/tools/spar.json create mode 100644 examples/spar_example/BUILD.bazel create mode 100644 examples/spar_example/building_control.aadl create mode 100644 toolchains/spar_toolchain.bzl create mode 100644 wasm/private/aadl_wit_library.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 2e05c0bb..a42fe642 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -260,6 +260,16 @@ use_repo(meld, "meld_toolchain") register_toolchains("@meld_toolchain//:meld_toolchain") +# spar toolchain: AADL architecture model -> WIT generation +spar = use_extension("//wasm:extensions.bzl", "spar") +spar.register( + name = "spar", + version = "0.9.3", +) +use_repo(spar, "spar_toolchain") + +register_toolchains("@spar_toolchain//:spar_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/checksums/tools/spar.json b/checksums/tools/spar.json new file mode 100644 index 00000000..7e7ba58a --- /dev/null +++ b/checksums/tools/spar.json @@ -0,0 +1,35 @@ +{ + "tool_name": "spar", + "github_repo": "pulseengine/spar", + "description": "AADL v2.3 architecture toolchain — generates WIT interfaces from AADL models", + "latest_version": "0.9.3", + "last_checked": "2026-05-20T00:00:00Z", + "supported_platforms": ["darwin_amd64", "darwin_arm64", "linux_amd64", "linux_arm64", "windows_amd64"], + "versions": { + "0.9.3": { + "release_date": "2026-05-11", + "platforms": { + "darwin_amd64": { + "sha256": "8fae9cd2e049ded4f5a3ab6320459549983655ae19dad7509d787bdccbb3fbfb", + "url_suffix": "x86_64-apple-darwin.tar.gz" + }, + "darwin_arm64": { + "sha256": "cddd41e1397b336cbedf6aa8502fdbad3cda0cf06fe0f8068a5892846874bb87", + "url_suffix": "aarch64-apple-darwin.tar.gz" + }, + "linux_amd64": { + "sha256": "b084297b4cc368e60c1374be4dd698e9d99e2178300ab6f267449bd8f4a5b6ea", + "url_suffix": "x86_64-unknown-linux-gnu.tar.gz" + }, + "linux_arm64": { + "sha256": "bf012f4644df19c6742d461d7d0d83611476f4a557ef53d55c8b5e129e9eac79", + "url_suffix": "aarch64-unknown-linux-gnu.tar.gz" + }, + "windows_amd64": { + "sha256": "3cc2ba69b5d8a616aedc9a36c24ffa4ac91e04506db95b7961c1d4b1fefc2b83", + "url_suffix": "x86_64-pc-windows-msvc.zip" + } + } + } + } +} diff --git a/examples/spar_example/BUILD.bazel b/examples/spar_example/BUILD.bazel new file mode 100644 index 00000000..9423fbfb --- /dev/null +++ b/examples/spar_example/BUILD.bazel @@ -0,0 +1,26 @@ +"""Example: generate WIT interfaces from an AADL architecture model with spar. + +spar instantiates the AADL system named by `root` and emits one `.wit` file +per AADL `process` instance. This is the front of the PulseEngine pipeline: + + AADL model -> spar -> WIT -> wit_library -> wit_bindgen -> component +""" + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_wasm_component//wasm:defs.bzl", "aadl_wit_library") + +package(default_visibility = ["//visibility:public"]) + +# building_control.aadl is the canonical AADL "building control" model +# (vendored from pulseengine/spar test data). Its system implementation +# BuildingControlDemo.Impl contains one process, TempControlProcess. +aadl_wit_library( + name = "building_control_wit", + srcs = ["building_control.aadl"], + root = "BuildingControl::BuildingControlDemo.Impl", +) + +build_test( + name = "spar_example_test", + targets = [":building_control_wit"], +) diff --git a/examples/spar_example/building_control.aadl b/examples/spar_example/building_control.aadl new file mode 100644 index 00000000..6e743583 --- /dev/null +++ b/examples/spar_example/building_control.aadl @@ -0,0 +1,147 @@ +package BuildingControl +public + with Base_Types, Data_Model; + + system BuildingControlDemo + -- features + -- setpoint: in event data port SetPoint; + end BuildingControlDemo; + + system implementation BuildingControlDemo.Impl + subcomponents + proc: processor TempControlProcessor.Impl; + tcp: process TempControlProcess.Impl; + -- connections + -- sp: port setpoint -> tcp.setpoint; + properties + Actual_Processor_Binding => reference (proc) applies to tcp; + -- annex resolute {** + -- check CASE_Tools + -- **}; + + end BuildingControlDemo.Impl; + + processor TempControlProcessor + end TempControlProcessor; + + processor implementation TempControlProcessor.Impl + end TempControlProcessor.Impl; + + process TempControlProcess + -- features + -- setpoint: in event data port SetPoint; + end TempControlProcess; + + process implementation TempControlProcess.Impl + subcomponents + tempsensor: thread TempSensor.Impl; + tempcontrol: thread TempControl.Impl; + fan: thread Fan.Impl; + driver: thread SetPointDriver.Impl; + connections + sp: port driver.setpoint -> tempcontrol.setpoint; + ct: port tempsensor.currenttemp -> tempcontrol.currenttemp; + tc: port tempsensor.tempchanged -> tempcontrol.tempchanged; + fc: port tempcontrol.fancmd -> fan.fancmd; + fa: port fan.fanack -> tempcontrol.fanack; + end TempControlProcess.Impl; + + thread TempSensor + features + currenttemp: out data port Temperature; + tempchanged: out event port; + end TempSensor; + + thread implementation TempSensor.Impl + properties + Dispatch_Protocol => Periodic; + Period => 1000 ms; + Stack_Size => 114688 Bytes; + Priority => 1; + end TempSensor.Impl; + + thread Fan + features + fancmd: in event data port FanCmd; + fanack: out event data port FanAck; + end Fan; + + thread implementation Fan.Impl + properties + Dispatch_Protocol => Sporadic; + Period => 1000 ms; + Stack_Size => 114688 Bytes; + Priority => 1; + end Fan.Impl; + + thread TempControl + features + currenttemp: in data port Temperature; + tempchanged: in event port; + fanack: in event data port FanAck; + setpoint: in event data port SetPoint; + fancmd: out event data port FanCmd; + end TempControl; + + thread implementation TempControl.Impl + properties + Dispatch_Protocol => Sporadic; + Period => 1000 ms; + Stack_Size => 114688 Bytes; + Priority => 1; + end TempControl.Impl; + + thread SetPointDriver + features + setpoint: out event data port SetPoint; + end SetPointDriver; + + thread implementation SetPointDriver.Impl + properties + Dispatch_Protocol => Periodic; + Period => 5000 ms; + Stack_Size => 114688 Bytes; + Priority => 1; + end SetPointDriver.Impl; + + data Temperature + properties + Data_Model::Data_Representation => Struct; + end Temperature; + + data implementation Temperature.Impl + subcomponents + degrees: data Base_Types::Float_32; + unit: data TempUnit; + end Temperature.Impl; + + data SetPoint + properties + Data_Model::Data_Representation => Struct; + end SetPoint; + + data implementation SetPoint.Impl + subcomponents + low: data Temperature; + high: data Temperature; + end SetPoint.Impl; + + data TempUnit + properties + Data_Model::Data_Representation => Enum; + Data_Model::Enumerators => ("Fahrenheit", "Celsius", "Kelvin"); + end TempUnit; + + data FanAck + properties + Data_Model::Data_Representation => Enum; + Data_Model::Enumerators => ("Ok", "Error"); + end FanAck; + + data FanCmd + properties + Data_Model::Data_Representation => Enum; + Data_Model::Enumerators => ("On", "Off"); + end FanCmd; + +end BuildingControl; \ No newline at end of file diff --git a/toolchains/BUILD.bazel b/toolchains/BUILD.bazel index 81bd7b2b..a71f419f 100644 --- a/toolchains/BUILD.bazel +++ b/toolchains/BUILD.bazel @@ -90,6 +90,12 @@ toolchain_type( visibility = ["//visibility:public"], ) +# Toolchain type for spar (AADL architecture model -> WIT generation) +toolchain_type( + name = "spar_toolchain_type", + visibility = ["//visibility:public"], +) + # Bzl library for tool versions (single source of truth) bzl_library( name = "tool_versions", diff --git a/toolchains/spar_toolchain.bzl b/toolchains/spar_toolchain.bzl new file mode 100644 index 00000000..349b8a58 --- /dev/null +++ b/toolchains/spar_toolchain.bzl @@ -0,0 +1,131 @@ +# 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 + +"""spar toolchain: AADL v2.3 architecture toolchain. + +spar generates WIT interfaces from AADL architecture models (`spar codegen +--format wit`). It is distributed as per-platform tar.gz/zip archives with a +flat layout (the `spar` binary at the archive root). +""" + +load("//checksums:registry.bzl", "validate_tool_exists") +load("//toolchains:tool_registry.bzl", "tool_registry") + +# Platforms where spar release archives are published. +_SUPPORTED_PLATFORMS = [ + "darwin_amd64", + "darwin_arm64", + "linux_amd64", + "linux_arm64", + "windows_amd64", +] + +def _spar_toolchain_impl(ctx): + """Implementation of spar_toolchain rule.""" + return [platform_common.ToolchainInfo( + spar = ctx.file.spar, + )] + +spar_toolchain = rule( + implementation = _spar_toolchain_impl, + attrs = { + "spar": attr.label( + allow_single_file = True, + executable = True, + cfg = "exec", + doc = "spar binary for AADL-to-WIT generation", + ), + }, + doc = "Declares a spar toolchain for AADL architecture model processing", +) + +_STUB_BUILD = '''"""spar toolchain stub: unsupported platform. + +spar has no release archive for this host, so we register a toolchain marked +incompatible with any target. Toolchain resolution for aadl_wit_library +targets fails cleanly here; builds that never touch aadl_wit_library are +unaffected. +""" + +load("@rules_wasm_component//toolchains:spar_toolchain.bzl", "spar_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["spar_stub"]) + +spar_toolchain( + name = "spar_toolchain_impl", + spar = "spar_stub", +) + +toolchain( + name = "spar_toolchain", + target_compatible_with = ["@platforms//:incompatible"], + toolchain = ":spar_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:spar_toolchain_type", +) +''' + +def _spar_repository_impl(repository_ctx): + """Download the spar archive and create a toolchain repository.""" + platform = tool_registry.detect_platform(repository_ctx) + version = repository_ctx.attr.version + + # Unsupported/unknown host: emit a stub so module resolution still works. + if platform not in _SUPPORTED_PLATFORMS or not validate_tool_exists(repository_ctx, "spar", version, platform): + print("spar: no release archive for platform {} (version {}); emitting stub".format(platform, version)) + repository_ctx.file("spar_stub", content = "", executable = True) + repository_ctx.file("BUILD.bazel", _STUB_BUILD) + return + + print("Setting up spar {} for platform {}".format(version, platform)) + + # Archives are flat: the binary sits at the archive root. + bin_name = "spar.exe" if platform.startswith("windows") else "spar" + + # Extract the release archive into dist/. + tool_registry.download( + repository_ctx, + "spar", + version, + platform, + output_dir = "dist", + ) + + repository_ctx.file("BUILD.bazel", '''"""spar toolchain repository""" + +load("@rules_wasm_component//toolchains:spar_toolchain.bzl", "spar_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["dist/{bin}"]) + +spar_toolchain( + name = "spar_toolchain_impl", + spar = "dist/{bin}", +) + +toolchain( + name = "spar_toolchain", + exec_compatible_with = [], + target_compatible_with = [], + toolchain = ":spar_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:spar_toolchain_type", +) +'''.format(bin = bin_name)) + +spar_repository = repository_rule( + implementation = _spar_repository_impl, + attrs = { + "version": attr.string( + default = "0.9.3", + doc = "spar version to download", + ), + }, + doc = "Downloads spar and creates a toolchain repository", +) diff --git a/toolchains/tool_registry.bzl b/toolchains/tool_registry.bzl index e17cf962..aaa63768 100644 --- a/toolchains/tool_registry.bzl +++ b/toolchains/tool_registry.bzl @@ -143,6 +143,11 @@ _URL_PATTERNS = { "filename": "{suffix}", # e.g., "meld-aarch64-apple-darwin" "is_binary": True, }, + "spar": { + # spar releases are tar.gz/zip archives: spar-v{version}-{triple}.{ext} + "base": "https://github.com/{repo}/releases/download/v{version}", + "filename": "spar-v{version}-{suffix}", + }, } def _build_download_url(tool_name, version, platform, tool_info, github_repo): diff --git a/wasm/defs.bzl b/wasm/defs.bzl index 0cecb854..b2732c62 100644 --- a/wasm/defs.bzl +++ b/wasm/defs.bzl @@ -59,6 +59,10 @@ load( "//wasm/private:meld_fuse.bzl", _meld_fuse = "meld_fuse", ) +load( + "//wasm/private:aadl_wit_library.bzl", + _aadl_wit_library = "aadl_wit_library", +) load( "//wasm/private:ssh_keygen.bzl", _ssh_keygen = "ssh_keygen", @@ -153,5 +157,6 @@ wasm_optimize = _wasm_optimize binaryen_optimize = _binaryen_optimize # PulseEngine pipeline rules +aadl_wit_library = _aadl_wit_library meld_fuse = _meld_fuse synth_compile = _synth_compile diff --git a/wasm/extensions.bzl b/wasm/extensions.bzl index 61d99437..12f31911 100644 --- a/wasm/extensions.bzl +++ b/wasm/extensions.bzl @@ -5,6 +5,7 @@ load("//toolchains:componentize_py_toolchain.bzl", "componentize_py_toolchain_re load("//toolchains:cpp_component_toolchain.bzl", "cpp_component_toolchain_repository") load("//toolchains:jco_toolchain.bzl", "jco_toolchain_repository") load("//toolchains:meld_toolchain.bzl", "meld_repository") +load("//toolchains:spar_toolchain.bzl", "spar_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") @@ -698,3 +699,42 @@ meld = module_extension( ), }, ) + +def _spar_extension_impl(module_ctx): + """Implementation of the spar 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(): + spar_repository( + name = name + "_toolchain", + version = registration.version, + ) + + if not registrations: + spar_repository( + name = "spar_toolchain", + version = "0.9.3", + ) + +# Module extension for spar (AADL architecture model -> WIT generation) +spar = module_extension( + implementation = _spar_extension_impl, + tag_classes = { + "register": tag_class( + attrs = { + "name": attr.string( + doc = "Name for this spar registration", + default = "spar", + ), + "version": attr.string( + doc = "spar version to use", + default = "0.9.3", + ), + }, + ), + }, +) diff --git a/wasm/private/aadl_wit_library.bzl b/wasm/private/aadl_wit_library.bzl new file mode 100644 index 00000000..1dd2d757 --- /dev/null +++ b/wasm/private/aadl_wit_library.bzl @@ -0,0 +1,83 @@ +"""spar AADL-to-WIT generation integration. + +Provides the aadl_wit_library rule, which runs `spar codegen` to generate WIT +interfaces from a formal AADL v2.3 architecture model. This is the front of +the PulseEngine pipeline: + + AADL model → spar → WIT → wit_library → wit_bindgen → build → component + +spar emits one `.wit` file per AADL `process` instance, written under a +`wit/` subdirectory of the rule's output. Because the generated filenames +derive from process names in the model, the output is declared as a single +tree artifact (directory) rather than statically-named files. +""" + +def _aadl_wit_library_impl(ctx): + """Implementation of the aadl_wit_library rule.""" + spar_toolchain = ctx.toolchains["@rules_wasm_component//toolchains:spar_toolchain_type"] + spar = spar_toolchain.spar + + # Tree artifact: spar writes generated WIT under /wit/*.wit. + out_dir = ctx.actions.declare_directory(ctx.label.name) + + # spar codegen --root --format wit --output + args = ctx.actions.args() + args.add("codegen") + args.add("--root", ctx.attr.root) + args.add("--format", "wit") + args.add("--output", out_dir.path) + args.add_all(ctx.files.srcs) + + ctx.actions.run( + inputs = ctx.files.srcs, + outputs = [out_dir], + executable = spar, + arguments = [args], + mnemonic = "SparCodegen", + progress_message = "Generating WIT from AADL model %s" % ctx.label, + tools = [spar], + ) + + return [ + DefaultInfo( + files = depset([out_dir]), + ), + OutputGroupInfo( + wit = depset([out_dir]), + ), + ] + +aadl_wit_library = rule( + implementation = _aadl_wit_library_impl, + attrs = { + "srcs": attr.label_list( + doc = "AADL source files (.aadl) — a model plus any library files.", + mandatory = True, + allow_files = [".aadl"], + ), + "root": attr.string( + doc = "The AADL system implementation to instantiate, as " + + "`Package::Type.Impl` (e.g. `BuildingControl::BuildingControlDemo.Impl`).", + mandatory = True, + ), + }, + toolchains = ["@rules_wasm_component//toolchains:spar_toolchain_type"], + doc = """Generate WIT interfaces from an AADL v2.3 architecture model using spar. + +spar instantiates the system named by `root`, then emits one `.wit` file per +AADL `process` instance into a `wit/` directory. The output is a single tree +artifact; downstream rules consume the generated WIT. + +Pipeline position: + aadl_wit_library → wit_library → wit_bindgen → rust_wasm_component → ... + +Example: + load("@rules_wasm_component//wasm:defs.bzl", "aadl_wit_library") + + aadl_wit_library( + name = "building_control_wit", + srcs = ["building_control.aadl"], + root = "BuildingControl::BuildingControlDemo.Impl", + ) +""", +) From 87479b16a08ad6651e722867748d9847b58ae328 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 20 May 2026 20:45:56 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20integrate=20witness=20=E2=80=94=20M?= =?UTF-8?q?C/DC=20coverage=20rule=20(wasm=5Fmodule=5Fcoverage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds witness (pulseengine/witness v0.22.0) as a first-class toolchain and rule. witness measures MC/DC-style branch coverage of WebAssembly core modules — the verification stage of the PulseEngine certification pipeline. - checksums/tools/witness.json — JSON registry entry, 5 platforms, verified SHA256 - toolchains/tool_registry.bzl — witness URL pattern - toolchains/witness_toolchain.bzl + toolchains/BUILD.bazel — witness_toolchain_type - wasm/extensions.bzl — witness module extension - MODULE.bazel — register the witness toolchain - wasm/private/wasm_module_coverage.bzl — the wasm_module_coverage rule: a three-stage witness pipeline (instrument -> run -> lcov). Consumes a core module; accepts meld_fuse's MeldFusedInfo provider or a plain .wasm. - wasm/defs.bzl — export wasm_module_coverage - examples/witness_example — coverage of a minimal core module Validated: //examples/witness_example:coverage_demo_coverage builds — witness 0.22.0 downloads checksum-verified, and instrument/run/lcov all execute (4 actions). The demo module carries no DWARF, so its LCOV report is structural only; a DWARF-bearing subject is needed for source-level MC/DC data (noted in the example). witness operates on core modules, not components — meld_fuse is the bridge that produces its input. Part of the tool-integration track (rivet DD-003). Co-Authored-By: Claude Opus 4.7 (1M context) --- MODULE.bazel | 10 ++ MODULE.bazel.lock | 70 +++++++++-- checksums/tools/witness.json | 35 ++++++ examples/witness_example/BUILD.bazel | 36 ++++++ toolchains/BUILD.bazel | 6 + toolchains/tool_registry.bzl | 5 + toolchains/witness_toolchain.bzl | 131 ++++++++++++++++++++ wasm/defs.bzl | 5 + wasm/extensions.bzl | 40 +++++++ wasm/private/wasm_module_coverage.bzl | 164 ++++++++++++++++++++++++++ 10 files changed, 491 insertions(+), 11 deletions(-) create mode 100644 checksums/tools/witness.json create mode 100644 examples/witness_example/BUILD.bazel create mode 100644 toolchains/witness_toolchain.bzl create mode 100644 wasm/private/wasm_module_coverage.bzl diff --git a/MODULE.bazel b/MODULE.bazel index a42fe642..0f742aa9 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -270,6 +270,16 @@ use_repo(spar, "spar_toolchain") register_toolchains("@spar_toolchain//:spar_toolchain") +# witness toolchain: MC/DC branch coverage for WASM core modules +witness = use_extension("//wasm:extensions.bzl", "witness") +witness.register( + name = "witness", + version = "0.22.0", +) +use_repo(witness, "witness_toolchain") + +register_toolchains("@witness_toolchain//:witness_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 d72436fd..e39691bd 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -218,7 +218,7 @@ "moduleExtensions": { "//toolchains:extensions.bzl%wasm_tool_repositories": { "general": { - "bzlTransitiveDigest": "kz99eZJOX/mWkFm8AXOHb89wWQHw271cNzFXTQmJ6wg=", + "bzlTransitiveDigest": "8IkwWwsP3wUGG95inOiQpavHx3PV99Q67vpFO77G+No=", "usagesDigest": "clQqyOwvm/I5edLjq5P4LnONWTjXO0N1AnoQTVKRaOY=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -284,7 +284,7 @@ }, "//wasm:extensions.bzl%binaryen": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "c5GClIZ+xfHSKFn9WL/02Ag7wuihInOMqTGH5CxR/U8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -308,7 +308,7 @@ }, "//wasm:extensions.bzl%cpp_component": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "ZtIrdMAeTaET/8t3kmG14kQp8U4FKMqhGB1+JS825Do=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -333,7 +333,7 @@ }, "//wasm:extensions.bzl%jco": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "MRHYkIS73wv1wYllXhdZBYX6dRIp7VySTL4edmOH2/M=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -358,7 +358,7 @@ }, "//wasm:extensions.bzl%meld": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "BnGoVHC5IiwLNc8XvxkOniYvo+CFiIGA27V/CaLd4Gw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -380,9 +380,33 @@ ] } }, + "//wasm:extensions.bzl%spar": { + "general": { + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "usagesDigest": "mOtiPROZSFmtouSVXHtDGjJNUiB1I315FvLubt4AxCo=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "spar_toolchain": { + "repoRuleId": "@@//toolchains:spar_toolchain.bzl%spar_repository", + "attributes": { + "version": "0.9.3" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "//wasm:extensions.bzl%tinygo": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "esnFdrH+qxI9awhZ/uW4dIkm843wWmTCzO4b1pdmifs=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -406,7 +430,7 @@ }, "//wasm:extensions.bzl%wasi_sdk": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "4CDKvALAslODuYVBSBBBKgDbmaqWRdwJJ4yfDrzWeYg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -432,7 +456,7 @@ }, "//wasm:extensions.bzl%wasi_wit": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "aprKQAVHUGZU3Qda4GY+rceEATrn/fard2WlVtmwyIU=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -780,7 +804,7 @@ }, "//wasm:extensions.bzl%wasm_toolchain": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "Keg+j4249N8787+5NDDpsnW8S0P3hp9HSaC26H2SeJg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -814,7 +838,7 @@ }, "//wasm:extensions.bzl%wasmtime": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "W3m1ohl2c96vXpGAao2C6XIl3CGB+kZYZN2+bagGv0M=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -837,9 +861,33 @@ ] } }, + "//wasm:extensions.bzl%witness": { + "general": { + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", + "usagesDigest": "vz4JVaRGcdcwU0XOHYN0kb54nuI69CDjEu/MXFK6r/g=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "witness_toolchain": { + "repoRuleId": "@@//toolchains:witness_toolchain.bzl%witness_repository", + "attributes": { + "version": "0.22.0" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "//wasm:extensions.bzl%wkg": { "general": { - "bzlTransitiveDigest": "aLbUXcFxIm6Mr7hTXS6NeZQiFpAitEu+9TlXa4UwTbk=", + "bzlTransitiveDigest": "UFfBprXhjOyZGqtzTTHy4xJvtnfZ5+TqD5dis4Qyw/0=", "usagesDigest": "ks+Q/IL0nntNP6PabzUcF0ruF4X9hTKhbmck/ueoPTg=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/checksums/tools/witness.json b/checksums/tools/witness.json new file mode 100644 index 00000000..32b566a5 --- /dev/null +++ b/checksums/tools/witness.json @@ -0,0 +1,35 @@ +{ + "tool_name": "witness", + "github_repo": "pulseengine/witness", + "description": "MC/DC-style branch coverage for WebAssembly core modules", + "latest_version": "0.22.0", + "last_checked": "2026-05-20T00:00:00Z", + "supported_platforms": ["darwin_amd64", "darwin_arm64", "linux_amd64", "linux_arm64", "windows_amd64"], + "versions": { + "0.22.0": { + "release_date": "2026-05-18", + "platforms": { + "darwin_amd64": { + "sha256": "fa93f9f375c62f5bcf1a53e168a77654b4f2c29f0711c80c002bcba593f5ce95", + "url_suffix": "x86_64-apple-darwin.tar.gz" + }, + "darwin_arm64": { + "sha256": "4f2f5202c8b749a7b0b7a940ccf74eaddc75f14012ea2065a88c1a61ac10bddf", + "url_suffix": "aarch64-apple-darwin.tar.gz" + }, + "linux_amd64": { + "sha256": "51d6bcbb05e82467d68b62d0e178f32ff06393f07ecb734c925efafebb79954f", + "url_suffix": "x86_64-unknown-linux-gnu.tar.gz" + }, + "linux_arm64": { + "sha256": "b61786fdda273b972a9e1e2b5ec3c1b44935eab1a9e4e7d5ada97c0b5029d055", + "url_suffix": "aarch64-unknown-linux-gnu.tar.gz" + }, + "windows_amd64": { + "sha256": "517d0d068c492f6a96685ead7e86478909096c40960bea9159812dc4c3741701", + "url_suffix": "x86_64-pc-windows-msvc.zip" + } + } + } + } +} diff --git a/examples/witness_example/BUILD.bazel b/examples/witness_example/BUILD.bazel new file mode 100644 index 00000000..f95bcc1c --- /dev/null +++ b/examples/witness_example/BUILD.bazel @@ -0,0 +1,36 @@ +"""Example: measure MC/DC branch coverage of a WASM core module with witness. + +witness instruments a core module, executes it via its embedded runtime, and +emits an LCOV coverage report. This is the verification stage of the +PulseEngine pipeline: + + ... -> meld (fuse to core module) -> wasm_module_coverage -> coverage evidence + +witness operates on core modules, not components. Here the subject is a +small pre-built core module (coverage_demo.wat -> coverage_demo.wasm); in a +real pipeline the `module` would be a meld_fuse target. +""" + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_wasm_component//wasm:defs.bzl", "wasm_module_coverage") + +package(default_visibility = ["//visibility:public"]) + +# coverage_demo.wasm is a minimal core module (see coverage_demo.wat for the +# source). Its no-argument export `run` exercises a branch. +# +# NOTE: this hand-written module carries no DWARF debug info, so the LCOV +# report is structural only (no source-line records) — it proves the +# instrument -> run -> lcov pipeline works. Meaningful MC/DC coverage needs a +# subject built with debug info (e.g. a Rust core module with debug = true); +# see docs/certification-pipeline.md. +wasm_module_coverage( + name = "coverage_demo_coverage", + module = "coverage_demo.wasm", + invoke = ["run"], +) + +build_test( + name = "witness_example_test", + targets = [":coverage_demo_coverage"], +) diff --git a/toolchains/BUILD.bazel b/toolchains/BUILD.bazel index a71f419f..6a532bf5 100644 --- a/toolchains/BUILD.bazel +++ b/toolchains/BUILD.bazel @@ -96,6 +96,12 @@ toolchain_type( visibility = ["//visibility:public"], ) +# Toolchain type for witness (MC/DC branch coverage for WASM core modules) +toolchain_type( + name = "witness_toolchain_type", + visibility = ["//visibility:public"], +) + # Bzl library for tool versions (single source of truth) bzl_library( name = "tool_versions", diff --git a/toolchains/tool_registry.bzl b/toolchains/tool_registry.bzl index aaa63768..b2844dc4 100644 --- a/toolchains/tool_registry.bzl +++ b/toolchains/tool_registry.bzl @@ -148,6 +148,11 @@ _URL_PATTERNS = { "base": "https://github.com/{repo}/releases/download/v{version}", "filename": "spar-v{version}-{suffix}", }, + "witness": { + # witness releases are tar.gz/zip archives: witness-v{version}-{triple}.{ext} + "base": "https://github.com/{repo}/releases/download/v{version}", + "filename": "witness-v{version}-{suffix}", + }, } def _build_download_url(tool_name, version, platform, tool_info, github_repo): diff --git a/toolchains/witness_toolchain.bzl b/toolchains/witness_toolchain.bzl new file mode 100644 index 00000000..ba285493 --- /dev/null +++ b/toolchains/witness_toolchain.bzl @@ -0,0 +1,131 @@ +# 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 + +"""witness toolchain: MC/DC branch coverage for WebAssembly core modules. + +witness instruments a WASM core module, executes it, and reports MC/DC-style +branch coverage. It is distributed as per-platform tar.gz/zip archives with a +flat layout (the `witness` binary at the archive root). +""" + +load("//checksums:registry.bzl", "validate_tool_exists") +load("//toolchains:tool_registry.bzl", "tool_registry") + +# Platforms where witness release archives are published. +_SUPPORTED_PLATFORMS = [ + "darwin_amd64", + "darwin_arm64", + "linux_amd64", + "linux_arm64", + "windows_amd64", +] + +def _witness_toolchain_impl(ctx): + """Implementation of witness_toolchain rule.""" + return [platform_common.ToolchainInfo( + witness = ctx.file.witness, + )] + +witness_toolchain = rule( + implementation = _witness_toolchain_impl, + attrs = { + "witness": attr.label( + allow_single_file = True, + executable = True, + cfg = "exec", + doc = "witness binary for WASM coverage instrumentation", + ), + }, + doc = "Declares a witness toolchain for WebAssembly coverage measurement", +) + +_STUB_BUILD = '''"""witness toolchain stub: unsupported platform. + +witness has no release archive for this host, so we register a toolchain +marked incompatible with any target. Toolchain resolution for +wasm_module_coverage targets fails cleanly here; builds that never touch +wasm_module_coverage are unaffected. +""" + +load("@rules_wasm_component//toolchains:witness_toolchain.bzl", "witness_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["witness_stub"]) + +witness_toolchain( + name = "witness_toolchain_impl", + witness = "witness_stub", +) + +toolchain( + name = "witness_toolchain", + target_compatible_with = ["@platforms//:incompatible"], + toolchain = ":witness_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:witness_toolchain_type", +) +''' + +def _witness_repository_impl(repository_ctx): + """Download the witness archive and create a toolchain repository.""" + platform = tool_registry.detect_platform(repository_ctx) + version = repository_ctx.attr.version + + # Unsupported/unknown host: emit a stub so module resolution still works. + if platform not in _SUPPORTED_PLATFORMS or not validate_tool_exists(repository_ctx, "witness", version, platform): + print("witness: no release archive for platform {} (version {}); emitting stub".format(platform, version)) + repository_ctx.file("witness_stub", content = "", executable = True) + repository_ctx.file("BUILD.bazel", _STUB_BUILD) + return + + print("Setting up witness {} for platform {}".format(version, platform)) + + # Archives are flat: the binary sits at the archive root. + bin_name = "witness.exe" if platform.startswith("windows") else "witness" + + # Extract the release archive into dist/. + tool_registry.download( + repository_ctx, + "witness", + version, + platform, + output_dir = "dist", + ) + + repository_ctx.file("BUILD.bazel", '''"""witness toolchain repository""" + +load("@rules_wasm_component//toolchains:witness_toolchain.bzl", "witness_toolchain") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["dist/{bin}"]) + +witness_toolchain( + name = "witness_toolchain_impl", + witness = "dist/{bin}", +) + +toolchain( + name = "witness_toolchain", + exec_compatible_with = [], + target_compatible_with = [], + toolchain = ":witness_toolchain_impl", + toolchain_type = "@rules_wasm_component//toolchains:witness_toolchain_type", +) +'''.format(bin = bin_name)) + +witness_repository = repository_rule( + implementation = _witness_repository_impl, + attrs = { + "version": attr.string( + default = "0.22.0", + doc = "witness version to download", + ), + }, + doc = "Downloads witness and creates a toolchain repository", +) diff --git a/wasm/defs.bzl b/wasm/defs.bzl index b2732c62..2393c343 100644 --- a/wasm/defs.bzl +++ b/wasm/defs.bzl @@ -63,6 +63,10 @@ load( "//wasm/private:aadl_wit_library.bzl", _aadl_wit_library = "aadl_wit_library", ) +load( + "//wasm/private:wasm_module_coverage.bzl", + _wasm_module_coverage = "wasm_module_coverage", +) load( "//wasm/private:ssh_keygen.bzl", _ssh_keygen = "ssh_keygen", @@ -160,3 +164,4 @@ binaryen_optimize = _binaryen_optimize aadl_wit_library = _aadl_wit_library meld_fuse = _meld_fuse synth_compile = _synth_compile +wasm_module_coverage = _wasm_module_coverage diff --git a/wasm/extensions.bzl b/wasm/extensions.bzl index 12f31911..2bfaaec9 100644 --- a/wasm/extensions.bzl +++ b/wasm/extensions.bzl @@ -6,6 +6,7 @@ load("//toolchains:cpp_component_toolchain.bzl", "cpp_component_toolchain_reposi 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:tinygo_toolchain.bzl", "tinygo_toolchain_repository") load("//toolchains:wasi_sdk_toolchain.bzl", "wasi_sdk_repository") load("//toolchains:wasm_toolchain.bzl", "wasm_toolchain_repository") @@ -738,3 +739,42 @@ spar = module_extension( ), }, ) + +def _witness_extension_impl(module_ctx): + """Implementation of the witness 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(): + witness_repository( + name = name + "_toolchain", + version = registration.version, + ) + + if not registrations: + witness_repository( + name = "witness_toolchain", + version = "0.22.0", + ) + +# Module extension for witness (MC/DC branch coverage for WASM core modules) +witness = module_extension( + implementation = _witness_extension_impl, + tag_classes = { + "register": tag_class( + attrs = { + "name": attr.string( + doc = "Name for this witness registration", + default = "witness", + ), + "version": attr.string( + doc = "witness version to use", + default = "0.22.0", + ), + }, + ), + }, +) diff --git a/wasm/private/wasm_module_coverage.bzl b/wasm/private/wasm_module_coverage.bzl new file mode 100644 index 00000000..434f61c5 --- /dev/null +++ b/wasm/private/wasm_module_coverage.bzl @@ -0,0 +1,164 @@ +"""witness MC/DC coverage integration. + +Provides the wasm_module_coverage rule, which measures MC/DC-style branch +coverage of a WebAssembly **core module** using witness. This is the +verification stage of the PulseEngine pipeline: + + ... -> meld (fuse to core module) -> wasm_module_coverage -> coverage evidence + +witness operates on core modules, not components. The natural input is the +core module produced by meld_fuse (a MeldFusedInfo provider); a plain .wasm +core module is also accepted. + +The rule runs witness as a three-stage pipeline, each stage a distinct action: + 1. instrument — rewrite the module with branch counters (+ sidecar manifest) + 2. run — execute the instrumented module, capturing counter data + 3. lcov — emit a standard LCOV report from the counter data +""" + +load("//providers:providers.bzl", "MeldFusedInfo") + +def _resolve_module(dep): + """Resolve the input core module .wasm file from a dependency.""" + if MeldFusedInfo in dep: + return dep[MeldFusedInfo].fused_wasm + + wasm_files = [f for f in dep[DefaultInfo].files.to_list() if f.extension == "wasm"] + if len(wasm_files) != 1: + fail("wasm_module_coverage: target '{}' must produce exactly one .wasm core module (found {})".format( + dep.label, + len(wasm_files), + )) + return wasm_files[0] + +def _wasm_module_coverage_impl(ctx): + """Implementation of the wasm_module_coverage rule.""" + witness = ctx.toolchains["@rules_wasm_component//toolchains:witness_toolchain_type"].witness + + module = _resolve_module(ctx.attr.module) + + if not (ctx.attr.invoke or ctx.attr.invoke_with_args or ctx.attr.call_start or ctx.attr.invoke_all): + fail("wasm_module_coverage: specify at least one of invoke, invoke_with_args, " + + "call_start, or invoke_all so witness has an entry point to execute.") + + name = ctx.label.name + instrumented = ctx.actions.declare_file(name + "_instrumented.wasm") + # witness writes the branch manifest as a sidecar: .witness.json + manifest = ctx.actions.declare_file(name + "_instrumented.wasm.witness.json") + run_data = ctx.actions.declare_file(name + "_witness-run.json") + lcov = ctx.actions.declare_file(name + ".lcov.info") + + # Stage 1: instrument + instrument_args = ctx.actions.args() + instrument_args.add("instrument") + instrument_args.add(module) + instrument_args.add("-o", instrumented) + ctx.actions.run( + inputs = [module], + outputs = [instrumented, manifest], + executable = witness, + arguments = [instrument_args], + mnemonic = "WitnessInstrument", + progress_message = "Instrumenting %s for coverage" % module.short_path, + tools = [witness], + ) + + # Stage 2: run (executes the instrumented module via witness's embedded runtime) + run_args = ctx.actions.args() + run_args.add("run") + run_args.add(instrumented) + run_args.add("--manifest", manifest) + run_args.add("-o", run_data) + for export in ctx.attr.invoke: + run_args.add("--invoke", export) + for spec in ctx.attr.invoke_with_args: + run_args.add("--invoke-with-args", spec) + if ctx.attr.invoke_all: + run_args.add("--invoke-all") + if ctx.attr.call_start: + run_args.add("--call-start") + ctx.actions.run( + inputs = [instrumented, manifest], + outputs = [run_data], + executable = witness, + arguments = [run_args], + mnemonic = "WitnessRun", + progress_message = "Running coverage for %s" % ctx.label, + tools = [witness], + ) + + # Stage 3: lcov report + lcov_args = ctx.actions.args() + lcov_args.add("lcov") + lcov_args.add("--run", run_data) + lcov_args.add("--manifest", manifest) + lcov_args.add("-o", lcov) + ctx.actions.run( + inputs = [run_data, manifest], + outputs = [lcov], + executable = witness, + arguments = [lcov_args], + mnemonic = "WitnessLcov", + progress_message = "Generating LCOV coverage report for %s" % ctx.label, + tools = [witness], + ) + + return [ + DefaultInfo( + files = depset([lcov]), + ), + OutputGroupInfo( + coverage = depset([lcov]), + instrumented = depset([instrumented]), + run_data = depset([run_data]), + ), + ] + +wasm_module_coverage = rule( + implementation = _wasm_module_coverage_impl, + attrs = { + "module": attr.label( + doc = "The WebAssembly core module to measure. Accepts a meld_fuse " + + "target (MeldFusedInfo) or any target producing a single .wasm.", + mandatory = True, + allow_files = [".wasm"], + ), + "invoke": attr.string_list( + doc = "Names of no-argument exports for witness to invoke.", + ), + "invoke_with_args": attr.string_list( + doc = "Exports to invoke with typed arguments, as 'func:args' " + + "(e.g. 'classify:2024').", + ), + "call_start": attr.bool( + doc = "Invoke the WASI `_start` entry point.", + default = False, + ), + "invoke_all": attr.bool( + doc = "Auto-discover and invoke every no-argument export.", + default = False, + ), + }, + toolchains = ["@rules_wasm_component//toolchains:witness_toolchain_type"], + doc = """Measure MC/DC-style branch coverage of a WASM core module with witness. + +witness instruments the module with branch counters, executes it via its +embedded runtime (invoking the exports named by `invoke` / `invoke_with_args` +/ `call_start` / `invoke_all`), and emits a standard LCOV report as the +default output. + +witness operates on core modules, not components — pair it with `meld_fuse`, +which fuses a composed component down to a core module: + + rust_wasm_component -> wasm_sign -> meld_fuse -> wasm_module_coverage + +Example: + load("@rules_wasm_component//wasm:defs.bzl", "wasm_module_coverage") + + wasm_module_coverage( + name = "service_coverage", + module = ":fused_service", # a meld_fuse target + invoke = ["run"], + ) +""", +)