Skip to content

feat(tx3c): generate the first-party Swift client with --template swift-client - #365

Merged
scarmuega merged 2 commits into
mainfrom
work/work-d81e13ab62e0f3e649dc0e6d3d6fe82d2d0fe5e7
Sep 27, 2026
Merged

scarmuega merged 2 commits into
mainfrom
work/work-d81e13ab62e0f3e649dc0e6d3d6fe82d2d0fe5e7

Conversation

@scarmuega

Copy link
Copy Markdown
Contributor

Implements plans/lang-codegen-swift-client.md (decisions 0015 and 0016): the first-party Swift client ships inside tx3c as the built-in template swift-client.

What it renders

tx3c codegen --template swift-client emits a standalone SwiftPM package: Package.swift, README.md, and Sources/<ProtocolName>Client/{Client,Types}.swift, where the package, product, module and facade are all <ProtocolName>Client.

  • Types.swift: the merged Swift backend's declarations (Sendable structs, enums with associated values, keyword escaping, deterministic collision errors). Every record, tuple and variant now spells its own argValue: ArgValue property, mirroring the SDK's encoder (records as constructor 0 in declared order, variants by case index, tuples positional, maps as key-sorted pairs).
  • Client.swift: PROTOCOL_NAME / PROTOCOL_VERSION / TARGET_TII_VERSION, one public <TX>_TIR envelope per transaction, one private <PROFILE>_PROFILE SDK Profile value per profile, a nested Profile enum when profiles exist (init(options:profile:), otherwise init(options:)), typed with<Party> setters routed through withPartyUnchecked, and one typed transaction method per tx that constructs ArgValue values statically and calls argTagged. No TII, schema, ParamType or dynamic loader is embedded.
  • Package.swift: depends on Tx3SDK from tx3-lang/swift-sdk from: "0.15.0", plus attaswift/BigInt from: "5.7.0" (the SDK's pinned version) only when integers appear. Every generated file states the v1beta0 target and the source tii.version.

Codegen changes

  • Templated output paths lay out Sources/{{identifier tii.protocol.name 'swift' 'type'}}Client/. The path uses single-quoted Handlebars literals because the release workflow checks the repository out on Windows, where " cannot appear in a file name.
  • The planner records an Encoding next to every member's type and resolves component aliases to their targets once all components are planned. The Swift backend spells conversions from it; templates never branch on schema shapes.
  • New helpers: argValue <tii> <transaction> <param> <language> <params-expr> (spelled from the same plan as the params declaration, so it always matches the declared field types), profile <profile> <language> (the SDK Profile value), and usesModule <tii> <language> <module>.
  • Golden output added for the transfer, complex and edge fixtures under expected/swift-client/. The existing custom/swift, custom/compat-swift and custom/templated-paths goldens gain the argValue properties (additive only).

CI compile check

codegen compile (swift-client) runs on macos-15 with Xcode 16.4, the swift-sdk repository's own baseline. No swift-sdk 0.15.0 tag exists yet, so the script clones tx3-lang/swift-sdk at 02fa0f2cc70d38f2623141035b3eb18509854b9a, tags that commit 0.15.0 locally and points a SwiftPM mirror at the clone; the generated Package.swift is then built exactly as rendered. swift package edit --path was tried first but SwiftPM resolves before editing and fails on the missing tag. Once the tag exists, the block in .github/scripts/codegen-compile-check.sh is deleted and the check resolves the published release directly (tracked in the Swift initiative plan).

Verification

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features --locked -- -D warnings, cargo test -p tx3c --locked: pass.
  • .github/scripts/codegen-compile-check.sh swift-client: transfer and complex render and swift build against the pinned SDK commit (Swift 6.2.3 locally).
  • Rendering each fixture twice gives byte-identical output.
  • A consumer package in a temporary directory with no .tii file constructs UnknownClient for every profile, binds all three parties, and gets a TxBuilder from transfer(_:); it constructs ComplexTypesClient(options:profile: .local) and asserts ComplexParams(...).argValue == ArgEncoder.encode(sameNativeValue, as: ParamType.fromJSONSchema(...)), including a unit variant case. The edge fixture (no parties, no profiles) pins the no-profile constructor in the golden corpus.
  • Fixture hashes match the plan: transfer 8d5d715f…da31, complex 0a7195b2…ae83.

🤖 Generated with Claude Code

…ft-client

`tx3c codegen --template swift-client` renders a standalone SwiftPM package
(`Package.swift`, `Sources/<ProtocolName>Client/{Client,Types}.swift`,
`README.md`) whose typed client wraps the SDK's `Tx3Client`: embedded TIR
envelopes and profiles seed `Tx3ClientBuilder.fromParts`, a nested `Profile`
enum locks the profile in at construction, parties bind through typed
`with<Party>` setters, and every transaction method constructs the SDK's
canonical `ArgValue` values statically and hands them to `argTagged`. The
package carries no TII, schema, `ParamType` or dynamic loader.

The template lays its sources out with templated output paths. Static
argument construction is a planner and backend concern: the planner now
records an `Encoding` next to every member's type (resolving component
aliases to their targets), the Swift backend spells each record, tuple and
variant's `argValue` property from it, and the new `argValue` helper spells a
transaction parameter's expression from the same plan as its params
declaration, so templates never branch on schema shapes. `profile` spells the
SDK `Profile` value for one profile and `usesModule` lets `Package.swift`
depend on BigInt only when integers appear.

CI compiles the transfer and complex fixtures against tx3-lang/swift-sdk at
02fa0f2cc70d38f2623141035b3eb18509854b9a (no 0.15.0 tag exists yet): the
check clones that commit, tags it 0.15.0 locally and points a SwiftPM mirror
at the clone, so the generated Package.swift is built exactly as rendered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`resolve_components` only detected an alias that aliased itself directly;
an alias reached again through a list or map item recursed with a fresh
visited set and overflowed the stack for any template. The visited path
now follows the member down through containers, and an alias declaration
starts with its own name on the path, so the value passes through as the
comment already promised.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@scarmuega

Copy link
Copy Markdown
Contributor Author

QA (code-qa) review at head 2ca43858d1133279b2f6cdee97ac25b53494feeb.

One correcting commit pushed, 2ca4385 — fix(tx3c): cut alias cycles that pass through lists and maps. The new resolve_components only detected an alias that aliased itself directly; a hand-written TII with "Loop": { "type": "array", "items": { "$ref": "#/components/schemas/Loop" } } overflowed the stack for every template (ts-client included), where the base revision rendered typealias Loop = [Loop] and moved on. The visited path now follows the member through list and map items, an alias declaration starts with its own name on the path, and a unit test covers the direct, aliased-alias and map-nested cases. Goldens are unchanged.

Everything else verified as delivered: cargo fmt, clippy -D warnings, cargo test -p tx3c --locked (41 unit + golden); codegen-compile-check.sh swift-client builds transfer and complex against the pinned SDK commit with Swift 6.2.3; the generated argValue rules match ArgEncoder at 02fa0f2 (record → constructor 0 in declared order, variant → case index, tuple positional, map key-sorted, unit → empty constructor 0); TARGET_TII_VERSION follows the other templates' convention. PR stays draft; readiness is published by the runtime after QA is recorded.

@scarmuega
scarmuega marked this pull request as ready for review September 27, 2026 13:17
@scarmuega

scarmuega commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge observed.

Plan: plans/lang-codegen-swift-client.md
QA-approved head: 2ca43858d1133279b2f6cdee97ac25b53494feeb

Trellis will verify the human merge evidence, adopt the domain pin, and retire the plan. No further merge action is needed.

@scarmuega
scarmuega merged commit 9538676 into main Sep 27, 2026
15 checks passed
@scarmuega
scarmuega deleted the work/work-d81e13ab62e0f3e649dc0e6d3d6fe82d2d0fe5e7 branch September 27, 2026 13:44
scarmuega added a commit that referenced this pull request Sep 27, 2026
Reconciles the java-client template with the swift-client template that
landed in #365. Both added static argument construction to the codegen
core in different shapes; the merge keeps main's `Encoding` model and
`argValue`/`member` backend surface and ports the Java backend onto it:

- `Backend::argument` spells from a member's `Encoding`; the shape-based
  `argument`/`accessor` pair and `Member::argument` are gone. Java overrides
  `member` for record accessors and keeps `sanitize` and `string_literal`.
- `Backend::declares_aliases` (Java: true) tells component resolution that
  an alias is a wrapper record converting itself, so a reference to an
  alias component spells `.toArgValue()` instead of the target's encoding.
- The Java template calls main's `argValue <tii> <tx> <param> <lang> <expr>`.
- Both templates are registered, listed in the CLI help and error, and in
  the CI codegen matrix and compile-check script.

Rendered java-client and swift-client output is byte-identical to the
committed goldens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant