Skip to content

Generate metadata name lookups as Swift source instead of loading JSON at runtime - #232

Merged
colemancda merged 15 commits into
masterfrom
feature/metadata-codegen
Aug 3, 2026
Merged

colemancda merged 15 commits into
masterfrom
feature/metadata-codegen

Conversation

@colemancda

Copy link
Copy Markdown
Member

Summary

Replaces CompanyIdentifier.name, BluetoothUUID.metadata, and UnitIdentifier.name/type's runtime JSON loading (Bundle.module + Data(contentsOf:) + JSONDecoder, via the BluetoothMetadata module) with build-time-generated Swift switch statements, extending the existing GenerateBluetoothDefinitions plugin/GenerateBluetooth tool that already generates the numeric identifier constants (CompanyIdentifier.apple and friends) the same way.

  • Sources/GenerateBluetooth/CompanyIdentifierMetadata.swift / Plugins/.../CompanyIdentifierMetadata.swift: generates CompanyIdentifier.generatedName(for:), a switch over all 4012 company identifiers.
  • Sources/GenerateBluetooth/BluetoothUUIDMetadata.swift / Plugins/.../BluetoothUUIDMetadata.swift: merges the six UUID category JSON files (service, characteristic, declaration, descriptor, member, unit — in that fixed precedence order) into BluetoothUUID.generatedMetadata(for:). Collisions across categories are reported at build time; none currently exist.
  • Sources/Bluetooth/{CompanyIdentifierMetadata,BluetoothUUIDMetadata,UnitIdentifierMetadata}.swift now call the generated switch functions instead of loading JSON, and no longer import Foundation or BluetoothMetadata.

The BluetoothMetadata module/product itself is untouched — it's still available for anyone who wants raw programmatic access to the JSON, and the build-time generator tool still depends on it to read the source JSON (a build-time-only dependency, never shipped).

Breaking API change

BluetoothUUID.metadata's return type changes from BluetoothMetadata.BluetoothUUID to a new BluetoothUUID.Metadata struct (name: String, type: String?) declared in Sources/Bluetooth. This is necessary because the old type's home module transitively pulls in Foundation, which would have defeated the point of this change. #if Metadata conditions that previously also gated on canImport(Foundation) && !os(WASI) && !hasFeature(Embedded) are simplified to just #if Metadata, since the lookup no longer touches any of those.

Incidental fixes

While wiring this up, found three pre-existing, previously-latent bugs (all masked each other, so none had ever actually been exercised):

  1. Package.swift used package.targets[4].plugins = [...] to attach GenerateBluetoothDefinitions to BluetoothTests, but that index silently pointed at BluetoothHCI after BluetoothSDP was inserted ahead of it — the plugin was never actually attached to the test target. Now looked up by name.
  2. Once fixed, .unitIdentifierTests crashed with an out-of-range index: arguments[3] should have been arguments[2].
  3. Once that was fixed, the generated UnitIdentifierTests.swift failed — its description expectation was built as "0x... (name)", which never matched UnitIdentifier.description's actual output (just the name, consistent with CompanyIdentifier.description).

Test generation redesign

The original generated test files unrolled one #expect() call per data entry (~4012 × 3 for company identifiers alone). That's pathologically slow for the type checker — many minutes, not seconds — since swift-testing's macro expansion cost scales with the number of source-level #expect() call sites, not how many times each executes. Chunking into many smaller @Test func bodies didn't help either, and neither did building the test data as one large array literal (a similar type-checker scaling problem, independent of macros).

Both CompanyIdentifierTests and UnitIdentifierTests are now generated as a single @Test(arguments:) parameterized test over an entries array built with sequential .append() calls (straight-line statements, same shape as the metadata switch generators, which already compile fast at this scale). BluetoothTests now builds in under a minute instead of hanging indefinitely.

Test plan

  • swift build --traits Metadata --target BluetoothTests — 38s
  • swift test --traits Metadata — 497 tests, 34 suites, pass
  • swift test (default, no traits) — 495 tests, 32 suites, pass
  • swift build (default, no traits) — builds clean

Emits a switch statement mapping raw company identifier values to
their SIG-assigned names, mirroring the existing numeric-constant
generator instead of loading Resources/CompanyIdentifier.json at
runtime.
Merges the six UUID category files (service, characteristic,
declaration, descriptor, member, unit) into a single switch keyed by
raw value, returning name and type, in a fixed category precedence
order. Collisions across categories are reported at build time and
resolved by keeping the first category in that order.
Also fixes a pre-existing arguments[3]/arguments[2] typo in the
.unitIdentifierTests case that crashed with an out-of-range index
whenever this command path was actually invoked.
Replaces thousands of individually unrolled #expect() calls with a
single @test(arguments:) call site over a generated entries array,
built with sequential append() calls rather than one large array
literal. Both unrolled #expect() chains and large array literals are
pathologically slow for the type checker at this data's scale
(~4000 entries); a single parameterized test site with straight-line
data construction compiles in seconds instead of minutes.
Same rewrite as CompanyIdentifierTests. Also fixes the generated
description expectation, which composed a "0x... (name)" string that
never matched UnitIdentifier.description's actual output (just the
name, consistent with CompanyIdentifier.description) — previously
undetected because this generated test was never actually invoked
due to the arguments[3] bug and the Package.swift target-index bug.
Invokes GenerateBluetooth companyIdentifierMetadata to emit
CompanyIdentifierNames.swift into the Bluetooth target.
Invokes GenerateBluetooth uuidMetadata to emit
BluetoothUUIDMetadataNames.swift into the Bluetooth target.
BluetoothUUID.metadata now reads from the build-time-generated
generatedMetadata(for:) switch instead of loading and merging all six
UUID category JSON files (and their BluetoothMetadata module) at
runtime. Introduces a Foundation-free BluetoothUUID.Metadata struct
in place of the old BluetoothMetadata.BluetoothUUID return type —
this is an intentional breaking API change, since the old type's home
module pulls in Foundation unconditionally.
CompanyIdentifier.name now reads from the build-time-generated
generatedName(for:) switch instead of loading and parsing
CompanyIdentifier.json (via the BluetoothMetadata module) at runtime.
UnitIdentifier.name/type already delegate to BluetoothUUID.metadata,
which no longer needs Foundation or the BluetoothMetadata module now
that it reads from generated Swift code.
description and init?(_:) only reach through self.metadata, which no
longer touches Foundation, WASI, or Embedded Swift concerns now that
it's backed by generated Swift code rather than a JSON-loading
runtime dependency.
package.targets[4] no longer pointed at BluetoothTests once
BluetoothSDP was inserted ahead of it, so
GenerateBluetoothDefinitions was never actually attached to the test
target and its generated test files were never compiled or run. Look
targets up by name instead of a hardcoded index.
@github-code-quality

github-code-quality Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Code Coverage Overview

Languages: Swift

Swift / code-coverage/llvm-cov

The overall coverage in commit 306c0fe in the feature/metadata-cod... branch remains at 89%, unchanged from commit a0b85b2 in the master branch.


Updated August 03, 2026 04:31 UTC

@colemancda
colemancda merged commit 9a6119b into master Aug 3, 2026
47 checks passed
@colemancda
colemancda deleted the feature/metadata-codegen branch August 3, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant