From dbce652ea074ba0c9adfe19535fb94ef36c44c59 Mon Sep 17 00:00:00 2001 From: "peaceful-conductor[bot]" <285126712+peaceful-conductor[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:53:29 +0000 Subject: [PATCH 1/2] Release 0.4.1-preview.1 Promote 0.4.1-preview.1 to public. See CHANGELOG.md for consumer-facing changes. --- Directory.Build.props | 6 +- jvm-helper/build.sbt | 4 +- .../helper/AstToIntermediateSpec.scala | 8 +- .../helper/PartyExpressionAnalyzerSpec.scala | 2 +- .../codegen/helper/SignatureErasureSpec.scala | 18 +- .../Generated/Quickstart/Iou.cs | 129 ++++++-- samples/QuickstartExample/daml/daml.yaml | 2 +- samples/TokenStandardV2/NuGet.config | 17 ++ samples/TokenStandardV2/Program.cs | 59 ++++ samples/TokenStandardV2/README.md | 45 +++ .../TokenStandardV2/TokenStandardV2.csproj | 22 ++ .../ChoiceEmitter.ContractIdExercisers.cs | 89 ++++-- .../ChoiceEmitter.InterfaceExtensions.cs | 93 ++++-- .../ChoiceEmitter.NonContractExercisers.cs | 112 ++++--- .../CodeGen/ChoiceEmitter.cs | 26 +- .../CodeGen/DamlTypeMapper.cs | 121 ++++++-- .../CodeGen/EmitterHelpers.cs | 25 ++ .../CodeGen/PackageEmitContext.cs | 12 +- .../CodeGen/RecordEmitter.cs | 57 +++- .../CodeGen/RecordSerializationEmitter.cs | 48 ++- .../CodeGen/RuntimeTypeNames.cs | 10 +- .../CodeGen/TemplateEmitter.cs | 15 +- .../CodeGen/TypeReferenceQualifier.cs | 38 ++- .../CodeGen/VariantEmitter.cs | 33 ++- .../Generated/Richtypes/Asset.cs | 91 +++++- .../Generated/Richtypes/Holding.cs | 23 +- .../Generated/Richtypes/IHolding.cs | 27 +- .../Generated/Richtypes/Marker.cs | 89 +++++- .../Generated/Richtypes/RichRecord.cs | 129 ++++++-- .../LedgerClientConformanceTests.cs | 120 +++++++- .../README.md | 12 + .../WriteConformanceFixture.cs | 44 +++ .../Extensions/CreateByExercise.cs | 45 +-- .../Extensions/PartyOverloads.cs | 32 ++ .../Extensions/ThrowingExercise.cs | 8 +- .../ILedgerStreamer.cs | 66 ++++- src/Daml.Ledger.Abstractions/ILedgerWriter.cs | 12 + src/Daml.Runtime/Commands/Choice.cs | 2 +- src/Daml.Runtime/StakeholderResume.cs | 17 ++ src/Daml.Runtime/Stdlib/GenericStub.cs | 10 +- src/Daml.Runtime/Streams/AcsSnapshotEntry.cs | 13 +- .../CliExitCodeTests.cs | 4 +- .../ChoiceAsyncExerciserTests.cs | 2 +- .../ChoiceCodeGenTests.cs | 8 +- .../ChoiceEmitterArchiveChoiceTests.cs | 38 ++- .../ChoiceEmitterContractIdExerciserTests.cs | 27 +- .../ChoiceEmitterInterfaceExtensionTests.cs | 34 ++- .../ChoiceEmitterNonContractExerciserTests.cs | 29 +- .../ChoiceEmitterValueReturnExerciserTests.cs | 2 +- .../CodeGenEdgeCaseTests.Conversions.cs | 2 +- .../CodeGenEdgeCaseTests.ForeignPackages.cs | 7 +- ...eGenEdgeCaseTests.InterfacePlaceholders.cs | 4 +- .../DamlTypeMapperTests.cs | 101 +++++++ .../DriftDetectionTests.cs | 32 +- .../EmittedTemplateChoiceCompilesTests.cs | 57 +++- .../EmittedVariantCompilesTests.cs | 8 +- .../GenericTypeSerializationRoundTripTests.cs | 210 +++++++++++++ .../IdentifiersTests.cs | 6 +- .../NamedSubmitterTests.cs | 28 +- ...ntractChoiceUnitExerciserRoundTripTests.cs | 275 ++++++++++++++++++ .../QuickstartSample/README.md | 2 +- .../QuickstartSample/intermediate.binpb | Bin 10908 -> 10998 bytes .../RecordEmitterTests.cs | 72 ++++- .../RuntimeTypeNameDriftGuardTests.cs | 12 +- .../Crossmodulecollision/Agreement.cs | 129 ++++++-- .../expected/Crossmodulecollision/Offer.cs | 129 ++++++-- .../cross-module-collision/intermediate.binpb | Bin 11356 -> 11446 bytes .../splice-amulet-name-service/README.md | 21 +- .../Name/Service/AmuletConversionRateFeed.cs | 135 +++++++-- .../Splice/Amulet/Name/Service/AnsEntry.cs | 135 +++++++-- .../Amulet/Name/Service/AnsEntryContext.cs | 191 +++++++++--- ...Context_RejectEntryInitialPaymentResult.cs | 4 +- .../Splice/Amulet/Name/Service/AnsRules.cs | 191 +++++++++--- ...nsRules_RejectEntryInitialPaymentResult.cs | 4 +- .../intermediate.binpb | Bin 165927 -> 215934 bytes .../splice-amulet-name-service.dar | Bin 1085328 -> 1515595 bytes .../README.md | 39 +++ .../expected/.daml-langversion | 0 .../Instruction/V2/AllocationFactory.cs | 60 ++++ .../Instruction/V2/AllocationFactoryView.cs | 29 ++ .../V2/AllocationFactory_Allocate.cs | 41 +++ .../V2/AllocationFactory_PublicFetch.cs | 29 ++ .../Instruction/V2/AllocationInstruction.cs | 60 ++++ .../V2/AllocationInstructionAction.cs | 64 ++++ .../AllocationInstructionAction_AIA_Custom.cs | 27 ++ .../V2/AllocationInstructionResult.cs | 34 +++ .../V2/AllocationInstructionResult_Output.cs | 64 ++++ ...t_AllocationInstructionResult_Completed.cs | 28 ++ ...put_AllocationInstructionResult_Pending.cs | 28 ++ .../V2/AllocationInstructionView.cs | 45 +++ .../V2/AllocationInstruction_Accept.cs | 31 ++ .../V2/AllocationInstruction_Withdraw.cs | 31 ++ .../Instruction/V2/IAllocationFactory.cs | 213 ++++++++++++++ .../Instruction/V2/IAllocationInstruction.cs | 213 ++++++++++++++ .../intermediate.binpb | Bin 0 -> 35447 bytes ...ce-api-token-allocation-instruction-v2.dar | Bin 0 -> 545894 bytes .../README.md | 37 +++ .../expected/.daml-langversion | 0 .../Request/V2/AllocationRequest.cs | 60 ++++ .../Request/V2/AllocationRequestAction.cs | 64 ++++ .../V2/AllocationRequestAction_ARA_Custom.cs | 27 ++ .../Request/V2/AllocationRequestView.cs | 43 +++ .../Request/V2/AllocationRequest_Accept.cs | 31 ++ .../V2/AllocationRequest_AcceptResult.cs | 27 ++ .../Request/V2/AllocationRequest_Reject.cs | 31 ++ .../V2/AllocationRequest_RejectResult.cs | 27 ++ .../Request/V2/AllocationRequest_Withdraw.cs | 31 ++ .../V2/AllocationRequest_WithdrawResult.cs | 27 ++ .../Request/V2/IAllocationRequest.cs | 267 +++++++++++++++++ .../intermediate.binpb | Bin 0 -> 32263 bytes ...splice-api-token-allocation-request-v2.dar | Bin 0 -> 534706 bytes .../splice-api-token-allocation-v2/README.md | 37 +++ .../expected/.daml-langversion | 0 .../Api/Token/Allocation/V2/Allocation.cs | 60 ++++ .../Token/Allocation/V2/AllocationAction.cs | 75 +++++ .../V2/AllocationAction_AA_Custom.cs | 27 ++ .../Token/Allocation/V2/AllocationResult.cs | 34 +++ .../Allocation/V2/AllocationResult_Output.cs | 75 +++++ ...nResult_Output_AllocationResult_Pending.cs | 28 ++ ...nResult_Output_AllocationResult_Settled.cs | 28 ++ .../Allocation/V2/AllocationSpecification.cs | 42 +++ .../Api/Token/Allocation/V2/AllocationView.cs | 47 +++ .../Token/Allocation/V2/Allocation_Cancel.cs | 31 ++ .../Token/Allocation/V2/Allocation_Settle.cs | 35 +++ .../Allocation/V2/Allocation_Withdraw.cs | 31 ++ .../Allocation/V2/FinalizedAllocation.cs | 34 +++ .../Api/Token/Allocation/V2/IAllocation.cs | 267 +++++++++++++++++ .../Token/Allocation/V2/ISettlementFactory.cs | 213 ++++++++++++++ .../Token/Allocation/V2/SettlementFactory.cs | 60 ++++ .../Allocation/V2/SettlementFactoryView.cs | 29 ++ .../V2/SettlementFactory_PublicFetch.cs | 29 ++ .../V2/SettlementFactory_SettleBatch.cs | 37 +++ .../V2/SettlementFactory_SettleBatchResult.cs | 31 ++ .../Api/Token/Allocation/V2/SettlementInfo.cs | 36 +++ .../Api/Token/Allocation/V2/TransferLeg.cs | 37 +++ .../Token/Allocation/V2/TransferLegSide.cs | 37 +++ .../Api/Token/Allocation/V2/TransferSide.cs | 50 ++++ .../intermediate.binpb | Bin 0 -> 26574 bytes .../splice-api-token-allocation-v2.dar | Bin 0 -> 541882 bytes .../Splice/Api/Token/Holding/V1/Holding.cs | 23 +- .../Splice/Api/Token/Holding/V1/IHolding.cs | 21 +- .../splice-api-token-holding-v2/README.md | 41 +++ .../expected/.daml-langversion | 0 .../Splice/Api/Token/Holding/V2/Account.cs | 31 ++ .../Splice/Api/Token/Holding/V2/Holding.cs | 60 ++++ .../Api/Token/Holding/V2/HoldingView.cs | 35 +++ .../Splice/Api/Token/Holding/V2/IHolding.cs | 105 +++++++ .../Api/Token/Holding/V2/InstrumentId.cs | 29 ++ .../Splice/Api/Token/Holding/V2/Lock.cs | 37 +++ .../intermediate.binpb | Bin 0 -> 14752 bytes .../splice-api-token-holding-v2.dar | Bin 0 -> 482765 bytes .../README.md | 39 +++ .../expected/.daml-langversion | 0 .../Api/Token/Transfer/Events/V2/EventLog.cs | 60 ++++ .../Token/Transfer/Events/V2/EventLogView.cs | 29 ++ .../Events/V2/EventLog_HoldingsChange.cs | 42 +++ .../V2/EventLog_HoldingsChangeResult.cs | 23 ++ .../Api/Token/Transfer/Events/V2/IEventLog.cs | 159 ++++++++++ .../Transfer/Events/V2/TransferLegSide.cs | 37 +++ .../Token/Transfer/Events/V2/TransferSide.cs | 50 ++++ .../intermediate.binpb | Bin 0 -> 17433 bytes .../splice-api-token-transfer-events-v2.dar | Bin 0 -> 497880 bytes .../README.md | 38 +++ .../expected/.daml-langversion | 0 .../Instruction/V2/ITransferFactory.cs | 213 ++++++++++++++ .../Instruction/V2/ITransferInstruction.cs | 267 +++++++++++++++++ .../Token/Transfer/Instruction/V2/Transfer.cs | 45 +++ .../Instruction/V2/TransferFactory.cs | 60 ++++ .../Instruction/V2/TransferFactoryView.cs | 29 ++ .../V2/TransferFactory_PublicFetch.cs | 29 ++ .../V2/TransferFactory_Transfer.cs | 33 +++ .../Instruction/V2/TransferInstruction.cs | 60 ++++ .../V2/TransferInstructionAction.cs | 75 +++++ .../TransferInstructionAction_TIA_Custom.cs | 27 ++ .../V2/TransferInstructionResult.cs | 34 +++ .../V2/TransferInstructionResult_Output.cs | 64 ++++ ...put_TransferInstructionResult_Completed.cs | 30 ++ ...utput_TransferInstructionResult_Pending.cs | 28 ++ .../Instruction/V2/TransferInstructionView.cs | 39 +++ .../V2/TransferInstruction_Accept.cs | 31 ++ .../V2/TransferInstruction_Reject.cs | 31 ++ .../V2/TransferInstruction_Withdraw.cs | 31 ++ .../intermediate.binpb | Bin 0 -> 24750 bytes ...lice-api-token-transfer-instruction-v2.dar | Bin 0 -> 519855 bytes .../Snapshots/splice-test-token-v1/README.md | 132 +++++++++ .../expected/.daml-langversion | 0 .../Splice/Test/Token/ContractIdentifiers.cs | 42 +++ .../expected/Splice/Test/Token/V1/Token.cs | 195 +++++++++++++ .../Splice/Test/Token/V1/TokenAllocation.cs | 195 +++++++++++++ .../Splice/Test/Token/V1/TokenRules.cs | 195 +++++++++++++ .../Test/Token/V1/TokenTransferOffer.cs | 195 +++++++++++++ .../splice-test-token-v1/intermediate.binpb | Bin 0 -> 56097 bytes .../splice-test-token-v1.dar | Bin 0 -> 738157 bytes .../splice-token-standard-utils/README.md | 49 ++++ .../emits-no-types | 9 + .../expected/.daml-langversion | 0 .../intermediate.binpb | Bin 0 -> 81247 bytes .../splice-token-standard-utils.dar | Bin 0 -> 897982 bytes .../TemplateEmitterTests.cs | 54 +++- .../TypeReferenceQualifierTests.cs | 20 ++ .../FakeLedgerClient.cs | 7 +- .../MarkerTests.cs | 2 +- .../ActAsIgnoringSubmitterFakeClient.cs | 98 +++++++ .../AuthorizingFakeClient.cs | 99 +++++++ .../ConformingFakeClient.cs | 6 +- .../FakeConformanceTests.cs | 13 +- .../HangingSubscriptionConformanceTests.cs | 4 +- .../SubmitterAuthorityConformanceTests.cs | 81 ++++++ .../SubmitterMergingFakeClient.cs | 99 +++++++ .../Daml.Ledger.Abstractions.Tests.csproj | 4 + .../LedgerClientExtensionsTests.cs | 2 + .../LedgerClientSubmitterInfoTests.cs | 122 +++++++- .../StakeholderResumeCompileGuardTests.cs | 109 +++++++ .../AcsSnapshotEntryTests.cs | 8 +- tests/Daml.Runtime.Tests/CommandTypesTests.cs | 15 + .../ContractTypesTests.ContractIds.cs | 8 +- .../DamlJsonSerializerTextInferenceTests.cs | 8 +- .../DamlJsonSerializerValueOverloadsTests.cs | 4 +- .../DamlTypesTests.Scalars.cs | 2 +- .../StakeholderResumeTests.cs | 30 ++ tests/Directory.Packages.props | 2 +- 221 files changed, 9973 insertions(+), 569 deletions(-) create mode 100644 samples/TokenStandardV2/NuGet.config create mode 100644 samples/TokenStandardV2/Program.cs create mode 100644 samples/TokenStandardV2/README.md create mode 100644 samples/TokenStandardV2/TokenStandardV2.csproj create mode 100644 src/Daml.Ledger.Abstractions.Testing.Conformance/WriteConformanceFixture.cs create mode 100644 src/Daml.Runtime/StakeholderResume.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/GenericTypeSerializationRoundTripTests.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/NonContractChoiceUnitExerciserRoundTripTests.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationFactoryView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationFactory_Allocate.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationFactory_PublicFetch.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstruction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionAction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionAction_AIA_Custom.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionResult_Output.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionResult_Output_AllocationInstructionResult_Completed.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionResult_Output_AllocationInstructionResult_Pending.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstructionView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstruction_Accept.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/AllocationInstruction_Withdraw.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/IAllocationFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/expected/Splice/Api/Token/Allocation/Instruction/V2/IAllocationInstruction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-instruction-v2/splice-api-token-allocation-instruction-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequestAction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequestAction_ARA_Custom.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequestView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_Accept.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_AcceptResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_Reject.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_RejectResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_Withdraw.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/AllocationRequest_WithdrawResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/expected/Splice/Api/Token/Allocation/Request/V2/IAllocationRequest.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-request-v2/splice-api-token-allocation-request-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/Allocation.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationAction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationAction_AA_Custom.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationResult_Output.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationResult_Output_AllocationResult_Pending.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationResult_Output_AllocationResult_Settled.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationSpecification.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/AllocationView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/Allocation_Cancel.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/Allocation_Settle.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/Allocation_Withdraw.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/FinalizedAllocation.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/IAllocation.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/ISettlementFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementFactoryView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementFactory_PublicFetch.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementFactory_SettleBatch.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementFactory_SettleBatchResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/SettlementInfo.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/TransferLeg.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/TransferLegSide.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/expected/Splice/Api/Token/Allocation/V2/TransferSide.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-allocation-v2/splice-api-token-allocation-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/Account.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/Holding.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/HoldingView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/IHolding.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/InstrumentId.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/expected/Splice/Api/Token/Holding/V2/Lock.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-holding-v2/splice-api-token-holding-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/EventLog.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/EventLogView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/EventLog_HoldingsChange.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/EventLog_HoldingsChangeResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/IEventLog.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/TransferLegSide.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/expected/Splice/Api/Token/Transfer/Events/V2/TransferSide.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-events-v2/splice-api-token-transfer-events-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/ITransferFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/ITransferInstruction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/Transfer.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferFactory.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferFactoryView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferFactory_PublicFetch.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferFactory_Transfer.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstruction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionAction.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionAction_TIA_Custom.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionResult.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionResult_Output.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionResult_Output_TransferInstructionResult_Completed.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionResult_Output_TransferInstructionResult_Pending.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstructionView.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstruction_Accept.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstruction_Reject.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/expected/Splice/Api/Token/Transfer/Instruction/V2/TransferInstruction_Withdraw.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-api-token-transfer-instruction-v2/splice-api-token-transfer-instruction-v2.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/Splice/Test/Token/ContractIdentifiers.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/Splice/Test/Token/V1/Token.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/Splice/Test/Token/V1/TokenAllocation.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/Splice/Test/Token/V1/TokenRules.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/expected/Splice/Test/Token/V1/TokenTransferOffer.cs create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-test-token-v1/splice-test-token-v1.dar create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-token-standard-utils/README.md create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-token-standard-utils/emits-no-types create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-token-standard-utils/expected/.daml-langversion create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-token-standard-utils/intermediate.binpb create mode 100644 tests/Daml.Codegen.CSharp.Tests/Snapshots/splice-token-standard-utils/splice-token-standard-utils.dar create mode 100644 tests/Daml.Ledger.Abstractions.Testing.Conformance.Tests/ActAsIgnoringSubmitterFakeClient.cs create mode 100644 tests/Daml.Ledger.Abstractions.Testing.Conformance.Tests/AuthorizingFakeClient.cs create mode 100644 tests/Daml.Ledger.Abstractions.Testing.Conformance.Tests/SubmitterAuthorityConformanceTests.cs create mode 100644 tests/Daml.Ledger.Abstractions.Testing.Conformance.Tests/SubmitterMergingFakeClient.cs create mode 100644 tests/Daml.Ledger.Abstractions.Tests/StakeholderResumeCompileGuardTests.cs create mode 100644 tests/Daml.Runtime.Tests/StakeholderResumeTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index b9822f2..8d5fa92 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,9 +20,9 @@ icon.png - 0.4.0-preview.3 - 0.4.0.0 - 0.4.0.0 + 0.4.1-preview.1 + 0.4.1.0 + 0.4.1.0 $(MSBuildThisFileDirectory)output/nuget diff --git a/jvm-helper/build.sbt b/jvm-helper/build.sbt index 34183b4..88353cf 100644 --- a/jvm-helper/build.sbt +++ b/jvm-helper/build.sbt @@ -4,7 +4,7 @@ ThisBuild / version := "0.1.0-SNAPSHOT" resolvers += "Daml" at "https://repo1.maven.org/maven2/" -lazy val damlLfArchiveVersion = "3.4.11" +lazy val damlLfArchiveVersion = "3.5.9" lazy val jvmHelper = (project in file(".")) .settings( @@ -14,7 +14,7 @@ lazy val jvmHelper = (project in file(".")) scalapb.gen() -> (Compile / sourceManaged).value / "scalapb" ), libraryDependencies ++= Seq( - "com.daml" %% "daml-lf-archive-reader" % damlLfArchiveVersion, + "com.daml" %% "daml-lf-archive" % damlLfArchiveVersion, "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "org.scalatest" %% "scalatest" % "3.2.19" % Test ), diff --git a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/AstToIntermediateSpec.scala b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/AstToIntermediateSpec.scala index 83879ed..2b7f0dc 100644 --- a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/AstToIntermediateSpec.scala +++ b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/AstToIntermediateSpec.scala @@ -104,7 +104,7 @@ class AstToIntermediateSpec extends AnyWordSpec with Matchers with OptionValues val pkg = Ast.PackageSignature( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-iface-in-defs"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -315,7 +315,7 @@ class AstToIntermediateSpec extends AnyWordSpec with Matchers with OptionValues val pkg = Ast.PackageSignature( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-tmpl-pkg"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -432,7 +432,7 @@ class AstToIntermediateSpec extends AnyWordSpec with Matchers with OptionValues val pkg = Ast.PackageSignature( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-builtins"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -479,7 +479,7 @@ class AstToIntermediateSpec extends AnyWordSpec with Matchers with OptionValues val pkg = Ast.PackageSignature( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-iface-method"), version = Ref.PackageVersion.assertFromString("0.0.0"), diff --git a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/PartyExpressionAnalyzerSpec.scala b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/PartyExpressionAnalyzerSpec.scala index ec86028..078d4bc 100644 --- a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/PartyExpressionAnalyzerSpec.scala +++ b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/PartyExpressionAnalyzerSpec.scala @@ -88,7 +88,7 @@ class PartyExpressionAnalyzerSpec extends AnyWordSpec with Matchers { ) ), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("party-analyzer-spec"), version = Ref.PackageVersion.assertFromString("0.0.0"), diff --git a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/SignatureErasureSpec.scala b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/SignatureErasureSpec.scala index 5583fa9..21a3c0c 100644 --- a/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/SignatureErasureSpec.scala +++ b/jvm-helper/src/test/scala/studio/peaceful/daml/codegen/helper/SignatureErasureSpec.scala @@ -81,7 +81,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val pkg = Ast.Package( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-filter"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -195,7 +195,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val pkg = Ast.Package( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-template"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -230,10 +230,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val targetPkgId = Ref.PackageId.assertFromString("synth-target-pkg") val targetTplId = Ref.Identifier( targetPkgId, - Ref.QualifiedName( - ModuleName.assertFromString("Target"), - DottedName.assertFromString("Tpl"), - ), + Ref.QualifiedName.assertFromString("Target:Tpl"), ) val methodName = Ref.Name.assertFromString("getOwner") val dummyExpr = Ast.EBuiltinCon(Ast.BCTrue) @@ -263,7 +260,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val pkg = Ast.Package( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-iface-coimpl"), version = Ref.PackageVersion.assertFromString("0.0.0"), @@ -290,10 +287,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val tplName = DottedName.assertFromString("Tpl") val ifaceTplId = Ref.Identifier( Ref.PackageId.assertFromString("synth-iface-pkg"), - Ref.QualifiedName( - ModuleName.assertFromString("Iface"), - DottedName.assertFromString("HasOwner"), - ), + Ref.QualifiedName.assertFromString("Iface:HasOwner"), ) val methodName = Ref.Name.assertFromString("getOwner") val dummyExpr = Ast.EBuiltinCon(Ast.BCTrue) @@ -331,7 +325,7 @@ class SignatureErasureSpec extends AnyWordSpec with Matchers with OptionValues w val pkg = Ast.Package( modules = Map(mod.name -> mod), directDeps = Set.empty, - languageVersion = LanguageVersion.default, + languageVersion = LanguageVersion.defaultLfVersion, metadata = Ast.PackageMetadata( name = Ref.PackageName.assertFromString("synth-tpl-impl"), version = Ref.PackageVersion.assertFromString("0.0.0"), diff --git a/samples/QuickstartExample/Generated/Quickstart/Iou.cs b/samples/QuickstartExample/Generated/Quickstart/Iou.cs index 68b8d60..d75f334 100644 --- a/samples/QuickstartExample/Generated/Quickstart/Iou.cs +++ b/samples/QuickstartExample/Generated/Quickstart/Iou.cs @@ -24,10 +24,10 @@ namespace Quickstart; public sealed partial record Iou([property: DamlFieldAttribute("issuer")] Party Issuer, [property: DamlFieldAttribute("owner")] Party Owner, [property: DamlFieldAttribute("currency")] string Currency, [property: DamlFieldAttribute("amount")] decimal Amount) : ITemplate { /// Gets the template identifier. - public static Identifier TemplateId { get; } = new("c6ae1a03c6a0e5c146dba48c5c577583e4e2bc12ef1dad7fa72429f733367aba", "Iou", "Iou"); + public static Identifier TemplateId { get; } = new("61d1c8472218a119a9e73167b9e9af82bfed91bf5ae5a89a82da27c10ea7f763", "Iou", "Iou"); /// Gets the package ID. - public static string PackageId => "c6ae1a03c6a0e5c146dba48c5c577583e4e2bc12ef1dad7fa72429f733367aba"; + public static string PackageId => "61d1c8472218a119a9e73167b9e9af82bfed91bf5ae5a89a82da27c10ea7f763"; /// Gets the package name. public static string PackageName => "quickstart"; @@ -62,7 +62,7 @@ public DamlRecord ToRecord() => DamlRecord.Create( { Name = new ChoiceName("Archive"), Consuming = true, - ArgumentEncoder = _ => DamlUnit.Instance, + ArgumentEncoder = _ => DamlRecord.Create(), ResultDecoder = _ => DamlUnit.Instance }; @@ -160,6 +160,24 @@ public static ExerciseOutcome FromCreatedContracts(IEnumerable public static class IouExtensions { + /// + /// Builds the for the Transfer choice on this contract id. + /// + /// The contract on which to exercise the choice. + /// The choice argument. + public static ExerciseCommand TransferCommand( + this ContractId contractId, + Iou.Transfer argument) + { + ArgumentNullException.ThrowIfNull(contractId); + ArgumentNullException.ThrowIfNull(argument); + return new ExerciseCommand( + Iou.TemplateId, + contractId, + new ChoiceName("Transfer"), + argument.ToRecord()); + } + /// /// Exercises the Transfer choice and projects the resulting transaction's created contracts to a typed . /// One Party parameter is emitted per Daml controller (declaration order). @@ -184,27 +202,20 @@ public static async Task> TransferAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contractId); ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(argument); SubmitterInfo submitter = owner; - var command = new ExerciseCommand( - Iou.TemplateId, - contractId, - new ChoiceName("Transfer"), - argument.ToRecord()); + var command = contractId.TransferCommand(argument); var submission = CommandsSubmission.Single(command) - .WithSubmitter(submitter) .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); if (!string.IsNullOrEmpty(workflowId)) { submission = submission.WithWorkflowId(new WorkflowId(workflowId)); } - var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); return outcome.ProjectCommitted(tx => TransferResult.FromCreatedContracts(tx.CreatedContracts)); } @@ -233,25 +244,18 @@ public static async Task> TransferAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contractId); ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(argument); - var command = new ExerciseCommand( - Iou.TemplateId, - contractId, - new ChoiceName("Transfer"), - argument.ToRecord()); + var command = contractId.TransferCommand(argument); var submission = CommandsSubmission.Single(command) - .WithSubmitter(submitter) .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); if (!string.IsNullOrEmpty(workflowId)) { submission = submission.WithWorkflowId(new WorkflowId(workflowId)); } - var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); return outcome.ProjectCommitted(tx => TransferResult.FromCreatedContracts(tx.CreatedContracts)); } @@ -328,3 +332,86 @@ public static Task>> CreateAsync( return client.TryCreateAsync(payload, submitter, cancellationToken: cancellationToken); } } + +/// +/// Async exerciser extensions for contract IDs whose choices +/// return a non-contract-id payload (Decimal, records, lists, Unit, etc.). +/// Each method submits the choice via +/// ILedgerWriter.TrySubmitAndWaitForTransactionAsync and lifts the typed result +/// into ExerciseOutcome<TReturn>. +/// +public static class IouNonContractExtensions +{ + /// + /// Builds the for the Archive choice on this contract id. + /// + /// The contract on which to exercise the choice. + public static ExerciseCommand ArchiveCommand( + this ContractId contractId) + { + ArgumentNullException.ThrowIfNull(contractId); + return new ExerciseCommand( + Iou.TemplateId, + contractId, + new ChoiceName("Archive"), + DamlRecord.Create()); + } + + /// + /// Exercises the Archive choice and lifts the choice's exercise result to + /// over Unit. Structured Canton/Daml errors + /// and infrastructure/transport errors pass through unchanged. + /// + /// The contract on which to exercise the choice. + /// The ledger client. + /// The party submitting the command. + /// Optional workflow id; passed through to the ledger when supplied. No default — workflow IDs are correlation keys, and a per-choice default would bucket every submission of the same choice under one ID. + /// Optional command id for deduplication; a fresh id is minted only when omitted. Pass the same id across a retry of a lost-but-accepted submission so the ledger deduplicates the resubmission instead of re-executing the choice. + /// Optional per-call deadline, enforced server-side; the default null applies no deadline. An overrun surfaces as an InfraError outcome. + /// Cancellation token. + public static async Task> ArchiveAsync( + this ContractId contractId, + ILedgerWriter client, + Party actAs, + string? workflowId = null, + CommandId? commandId = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(client); + + var command = contractId.ArchiveCommand(); + + var submission = CommandsSubmission.Single(command) + .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); + if (!string.IsNullOrEmpty(workflowId)) + { + submission = submission.WithWorkflowId(new WorkflowId(workflowId)); + } + + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + + return outcome.ProjectCommitted(tx => ProjectArchiveResult(tx, contractId.Value)); + } + + private static ExerciseOutcome ProjectArchiveResult(TransactionResult tx, string contractId) + { + foreach (var exercised in tx.ExercisedEvents) + { + if (string.Equals(exercised.ContractId, contractId, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.ModuleName, Iou.TemplateId.ModuleName, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.EntityName, Iou.TemplateId.EntityName, StringComparison.Ordinal) + && string.Equals(exercised.ChoiceName, "Archive", StringComparison.Ordinal)) + { + return new ExerciseOutcome.One(Unit.Value); + } + } + + throw new InvalidOperationException( + $"Submission succeeded but no 'Archive' exercise on contract '{contractId}' was recorded on transaction {tx.UpdateId}. " + + "This is most often caused by the ILedgerWriter implementation not populating TransactionResult.ExercisedEvents — " + + "your ILedgerWriter implementation must project the transaction's exercised events into TransactionResult.ExercisedEvents. " + + "If your implementation does populate ExercisedEvents, ensure the participant is configured to return " + + "LedgerEffects with verbose events so the exercise event survives projection."); + } +} diff --git a/samples/QuickstartExample/daml/daml.yaml b/samples/QuickstartExample/daml/daml.yaml index 6260ac7..d0502eb 100644 --- a/samples/QuickstartExample/daml/daml.yaml +++ b/samples/QuickstartExample/daml/daml.yaml @@ -1,6 +1,6 @@ # Copyright 2026 Peaceful Studio OÜ # SPDX-License-Identifier: Apache-2.0 -sdk-version: 3.4.11 +sdk-version: 3.5.2 name: quickstart source: . version: 0.0.1 diff --git a/samples/TokenStandardV2/NuGet.config b/samples/TokenStandardV2/NuGet.config new file mode 100644 index 0000000..0c56c53 --- /dev/null +++ b/samples/TokenStandardV2/NuGet.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/samples/TokenStandardV2/Program.cs b/samples/TokenStandardV2/Program.cs new file mode 100644 index 0000000..9240c6f --- /dev/null +++ b/samples/TokenStandardV2/Program.cs @@ -0,0 +1,59 @@ +// Copyright 2026 Peaceful Studio OÜ +// SPDX-License-Identifier: Apache-2.0 + +using Daml.Ledger.Abstractions; +using Daml.Runtime.Commands; +using Daml.Runtime.Contracts; +using Daml.Runtime.Data; +using Daml.Runtime.Streams; +using Splice.Api.Token.Allocation.V2; +using Splice.Api.Token.Holding.V2; +using Splice.Api.Token.Metadata.V1; +using Splice.Api.Token.Transfer.Instruction.V2; + +Console.WriteLine("Splice Token Standard V2 - Offline Showcase"); +Console.WriteLine("===========================================\n"); + +var alice = new Party("Alice::1220deadbeef"); + +var noExtraArgs = new ExtraArgs( + new ChoiceContext(new Dictionary()), + new Metadata(new Dictionary())); + +Console.WriteLine("1. ACS filter over the IHolding (V2) interface:"); +Console.WriteLine( + $" ACS query ready: {nameof(ILedgerStreamer)}.{nameof(ILedgerStreamer.SubscribeActiveAsync)}<{nameof(IHolding)}>()" + + $" → IAsyncEnumerable<{nameof(AcsSnapshotEntry)}<{nameof(IHolding)}>>\n"); + +Console.WriteLine("2. Accepting a TransferInstruction (V2) via its typed interface choice:"); +var transferId = new ContractId("00transferinstruction"); +var acceptArg = new TransferInstruction_Accept( + Actors: new[] { alice }, + ExtraArgs: noExtraArgs).ToRecord(); +var transfer = ExerciseCommand.ForInterface( + transferId, + new ChoiceName(nameof(TransferInstruction_Accept)), + acceptArg); +Console.WriteLine($" interface id on the wire: {transfer.TemplateId.FullyQualifiedName}"); +Console.WriteLine($" choice: {transfer.Choice}\n"); + +Console.WriteLine("3. Withdrawing an Allocation (V2) via its typed interface choice:"); +var allocationId = new ContractId("00allocation"); +var withdrawArg = new Allocation_Withdraw( + Actors: new[] { alice }, + ExtraArgs: noExtraArgs).ToRecord(); +var allocation = ExerciseCommand.ForInterface( + allocationId, + new ChoiceName(nameof(Allocation_Withdraw)), + withdrawArg); +Console.WriteLine($" interface id on the wire: {allocation.TemplateId.FullyQualifiedName}"); +Console.WriteLine($" choice: {allocation.Choice}\n"); + +Console.WriteLine("4. Assembling a single-command submission from the transfer accept (unsent):"); +var submission = CommandsSubmission.Single(transfer) + .WithActAs(alice) + .WithCommandId(new CommandId(Guid.NewGuid().ToString())); +Console.WriteLine($" commands: {submission.Commands.Count}, actAs: {string.Join(", ", submission.ActAs ?? [])}\n"); + +Console.WriteLine("Done. Against a live ledger these commands submit through an ILedgerWriter,"); +Console.WriteLine("and active IHolding contracts stream through ILedgerStreamer.SubscribeActiveAsync."); diff --git a/samples/TokenStandardV2/README.md b/samples/TokenStandardV2/README.md new file mode 100644 index 0000000..b6d8ac1 --- /dev/null +++ b/samples/TokenStandardV2/README.md @@ -0,0 +1,45 @@ +# Token Standard V2 sample + +An offline console app showing the Splice Token Standard V2 (CIP-0112) C# +packages composing with `Daml.Runtime` and `Daml.Ledger.Abstractions`. It +constructs a real V2 workflow — an `IHolding` ACS query, a `TransferInstruction` +accept exercised through its typed interface choice, and an `Allocation` +withdraw — assembled into an unsent command submission, all without contacting +a ledger. + +## How it is built + +Unlike `samples/QuickstartExample` (which references the `src` projects), this +sample references the **published NuGet surface**: the V2 API packages plus +`Daml.Runtime` / `Daml.Ledger.Abstractions`, resolved from a local feed. It is +deliberately excluded from `Daml.Codegen.CSharp.slnx` and from central package +management (`ManagePackageVersionsCentrally=false`) so the tool's own build +never tries to restore not-yet-published packages, and so package versions can +float on the release-time counter segment. + +To build it, populate `local-feed/` with the packed V2 `.nupkg` files and the +packed `Daml.Runtime` / `Daml.Ledger.Abstractions`, then `dotnet build`. CI does +this automatically in the Splice publish pipeline via +`.github/scripts/verify-sample-tokenstandard-v2.sh`, which builds the sample +against the freshly packed feed as a focused V2 compile-gate before publishing. + +## Package version floats + +The `Splice.*` references float `1.*-*` and the `Daml.*` references float +`0.*-*` — the widest prerelease pattern within each package's current major — +rather than pinning a specific minor/patch. The publish pipeline and +`verify-sample-tokenstandard-v2.sh` pack these into a private `local-feed`, and +`NuGet.config`'s package-source mapping resolves every `Splice.*` / `Daml.*` +package **only** from that feed. So the float always resolves whatever this repo +just packed — a `-preview.N` build today or a stable `M.m.p` build at GA — which +keeps the V2 compile-gate stable across the repo's own version bumps instead of +breaking each time `Directory.Build.props` moves to a new preview minor. + +## Why it references the whole V2 family + +The sample references the full V2 API family — including +`transfer-events-v2`, `allocation-instruction-v2`, and `allocation-request-v2`, +which its walkthrough does not directly exercise — so the compile-gate proves the +whole family restores and composes together, not only the packages this sample +touches. `splice-token-standard-utils` is deliberately absent: it emits no C# +types and is not published as a package. diff --git a/samples/TokenStandardV2/TokenStandardV2.csproj b/samples/TokenStandardV2/TokenStandardV2.csproj new file mode 100644 index 0000000..99e0daf --- /dev/null +++ b/samples/TokenStandardV2/TokenStandardV2.csproj @@ -0,0 +1,22 @@ + + + + Exe + Offline showcase for the Splice Token Standard V2 C# packages + false + false + + + + + + + + + + + + + + + diff --git a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.ContractIdExercisers.cs b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.ContractIdExercisers.cs index 08f3430..92ae6c8 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.ContractIdExercisers.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.ContractIdExercisers.cs @@ -143,6 +143,8 @@ internal void WriteChoiceAsyncExercisersClass( var controllers = party.ValidatePayloadParties(choice.Controllers, partyFields); var choiceObservers = party.ValidatePayloadParties(choice.Observers, partyFields); var effectiveReadAs = party.UnionStaticParties(templateObservers, choiceObservers); + WriteContractIdChoiceCommandBuilder(indent, choice, templateClassName, dataTypes); + indent.AppendLine(); WriteSingleChoiceAsyncExerciser( indent, choice, templateClassName, dataTypes, controllers, effectiveReadAs); @@ -278,12 +280,7 @@ private void WriteSingleChoiceAsyncExerciser( indent.AppendLine("{"); indent.Indent(); - indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); indent.AppendLine("ArgumentNullException.ThrowIfNull(client);"); - if (hasArg) - { - indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); - } // When controllers are statically resolvable, build the SubmitterInfo // locally — actAs from the named Party params, readAs from the @@ -385,12 +382,7 @@ private void WriteSubmitterInfoChoiceAsyncExerciser( indent.AppendLine("{"); indent.Indent(); - indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); indent.AppendLine("ArgumentNullException.ThrowIfNull(client);"); - if (hasArg) - { - indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); - } WriteExerciserCommandDispatchAndProject(indent, choice, templateClassName, dataTypes); @@ -398,20 +390,64 @@ private void WriteSubmitterInfoChoiceAsyncExerciser( indent.AppendLine("}"); } - private void WriteExerciserCommandDispatchAndProject( + /// + /// Emits the <Choice>Command(this ContractId<TemplateName> contractId, ...) + /// builder that constructs the choice's + /// without submitting it. Emitted once per choice and shared by both the named-Party and + /// SubmitterInfo <Choice>Async overloads (see + /// and ) — + /// they exercise the identical choice on the identical ContractId<T> type and + /// therefore build the identical command. + /// + private void WriteContractIdChoiceCommandBuilder( IndentWriter indent, DamlChoice choice, string templateClassName, IReadOnlyDictionary dataTypes) { var choiceName = SanitizeIdentifier(choice.Name); - var resultName = $"{choiceName}Result"; - var (argTypeName, _, _, _) = GetChoiceArgumentInfo(choice, dataTypes); + var commandMethodName = $"{choiceName}Command"; + var (argTypeName, _, _, isNestedTemplateArg) = GetChoiceArgumentInfo(choice, dataTypes); var hasArg = argTypeName != "DamlUnit"; - indent.AppendLine(); + if (options.GenerateXmlDocs) + { + indent.AppendLine("/// "); + indent.AppendLine($"/// Builds the for the {choice.Name} choice on this contract id."); + indent.AppendLine("/// "); + indent.AppendLine("/// The contract on which to exercise the choice."); + if (hasArg) + { + indent.AppendLine("/// The choice argument."); + } + } + + indent.AppendLine($"public static {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)} {commandMethodName}("); + indent.Indent(); + if (hasArg) + { + var argParamType = isNestedTemplateArg + ? $"{templateClassName}.{argTypeName}" + : argTypeName; + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{templateClassName}> contractId,"); + indent.AppendLine($"{argParamType} argument)"); + } + else + { + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{templateClassName}> contractId)"); + } + indent.Dedent(); + indent.AppendLine("{"); + indent.Indent(); + + indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); + if (hasArg) + { + indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); + } + var argExpr = hasArg ? "argument.ToRecord()" : $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance"; - indent.AppendLine($"var command = new {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}("); + indent.AppendLine($"return new {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}("); indent.Indent(); indent.AppendLine($"{templateClassName}.TemplateId,"); indent.AppendLine("contractId,"); @@ -419,10 +455,29 @@ private void WriteExerciserCommandDispatchAndProject( indent.AppendLine($"{argExpr});"); indent.Dedent(); + indent.Dedent(); + indent.AppendLine("}"); + } + + private void WriteExerciserCommandDispatchAndProject( + IndentWriter indent, + DamlChoice choice, + string templateClassName, + IReadOnlyDictionary dataTypes) + { + var choiceName = SanitizeIdentifier(choice.Name); + var resultName = $"{choiceName}Result"; + var (argTypeName, _, _, _) = GetChoiceArgumentInfo(choice, dataTypes); + var hasArg = argTypeName != "DamlUnit"; + + indent.AppendLine(); + indent.AppendLine(hasArg + ? $"var command = contractId.{choiceName}Command(argument);" + : $"var command = contractId.{choiceName}Command();"); + indent.AppendLine(); indent.AppendLine($"var submission = {context.Qualifier.Qualify(RuntimeTypeNames.CommandsSubmission, context.RootNamespace)}.Single(command)"); indent.Indent(); - indent.AppendLine(".WithSubmitter(submitter)"); indent.AppendLine($".WithCommandId(commandId ?? new {context.Qualifier.Qualify(RuntimeTypeNames.CommandId, context.RootNamespace)}(Guid.NewGuid().ToString()));"); indent.Dedent(); indent.AppendLine("if (!string.IsNullOrEmpty(workflowId))"); @@ -432,7 +487,7 @@ private void WriteExerciserCommandDispatchAndProject( indent.Dedent(); indent.AppendLine("}"); indent.AppendLine(); - indent.AppendLine("var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); + indent.AppendLine("var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); indent.AppendLine(); indent.AppendLine($"return outcome.ProjectCommitted(tx => {resultName}.FromCreatedContracts(tx.CreatedContracts));"); } diff --git a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.InterfaceExtensions.cs b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.InterfaceExtensions.cs index 8ec6c07..ee4a1fc 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.InterfaceExtensions.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.InterfaceExtensions.cs @@ -81,17 +81,85 @@ private void WriteInterfaceChoiceExtensionMethod( DamlChoice choice, string interfaceName) { - var methodName = $"{SanitizeIdentifier(choice.Name)}Async"; + var choiceName = SanitizeIdentifier(choice.Name); + var commandMethodName = $"{choiceName}Command"; + var methodName = $"{choiceName}Async"; var (argTypeName, hasArg) = ResolveInterfaceChoiceArgType(choice); + var requiresArgumentNullCheck = hasArg && choice.ArgumentType is DamlTypeRef; + var argExpr = hasArg + ? mapper.ToValue(choice.ArgumentType, "argument") + : EmptyArgumentExpression(choice); + + WriteInterfaceChoiceCommandBuilder(indent, choice, interfaceName, commandMethodName, argTypeName, hasArg, requiresArgumentNullCheck, argExpr); + indent.AppendLine(); + WriteInterfaceChoiceAsyncMethod(indent, choice, interfaceName, commandMethodName, methodName, argTypeName, hasArg); + } + private void WriteInterfaceChoiceCommandBuilder( + IndentWriter indent, + DamlChoice choice, + string interfaceName, + string commandMethodName, + string argTypeName, + bool hasArg, + bool requiresArgumentNullCheck, + string argExpr) + { if (options.GenerateXmlDocs) { indent.AppendLine("/// "); - indent.AppendLine($"/// Exercises the {choice.Name} interface choice on this contract id."); + indent.AppendLine($"/// Builds the interface-typed for the {choice.Name} choice on this contract id."); indent.AppendLine("/// The wire-level template_id slot carries the interface id — Canton's"); indent.AppendLine("/// ledger API resolves the concrete implementing template at the participant."); indent.AppendLine("/// "); indent.AppendLine("/// The interface-typed contract id to exercise on."); + if (hasArg) + { + indent.AppendLine("/// The choice argument."); + } + } + + indent.AppendLine($"public static {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)} {commandMethodName}("); + indent.Indent(); + if (hasArg) + { + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{interfaceName}> contractId,"); + indent.AppendLine($"{argTypeName} argument)"); + } + else + { + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{interfaceName}> contractId)"); + } + indent.Dedent(); + indent.AppendLine("{"); + indent.Indent(); + indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); + if (requiresArgumentNullCheck) + { + indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); + } + indent.AppendLine($"return {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}.ForInterface<{interfaceName}>(contractId, new {context.Qualifier.Qualify(RuntimeTypeNames.ChoiceName, context.RootNamespace)}(\"{choice.Name}\"), {argExpr});"); + indent.Dedent(); + indent.AppendLine("}"); + } + + private void WriteInterfaceChoiceAsyncMethod( + IndentWriter indent, + DamlChoice choice, + string interfaceName, + string commandMethodName, + string methodName, + string argTypeName, + bool hasArg) + { + if (options.GenerateXmlDocs) + { + indent.AppendLine("/// "); + indent.AppendLine($"/// Exercises the {choice.Name} interface choice on this contract id, submitting the"); + indent.AppendLine("/// resulting through"); + indent.AppendLine("/// and awaiting the outcome."); + indent.AppendLine("/// "); + indent.AppendLine("/// The interface-typed contract id to exercise on."); indent.AppendLine("/// The ledger client."); if (hasArg) { @@ -125,21 +193,13 @@ private void WriteInterfaceChoiceExtensionMethod( indent.AppendLine("{"); indent.Indent(); - indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); indent.AppendLine("ArgumentNullException.ThrowIfNull(client);"); - if (hasArg && choice.ArgumentType is DamlTypeRef) - { - indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); - } - - var argExpr = hasArg - ? mapper.ToValue(choice.ArgumentType, "argument") - : $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance"; - indent.AppendLine($"var command = {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}.ForInterface<{interfaceName}>(contractId, new {context.Qualifier.Qualify(RuntimeTypeNames.ChoiceName, context.RootNamespace)}(\"{choice.Name}\"), {argExpr});"); + indent.AppendLine(hasArg + ? $"var command = contractId.{commandMethodName}(argument);" + : $"var command = contractId.{commandMethodName}();"); indent.AppendLine(); indent.AppendLine($"var submission = {context.Qualifier.Qualify(RuntimeTypeNames.CommandsSubmission, context.RootNamespace)}.Single(command)"); indent.Indent(); - indent.AppendLine(".WithActAs(actAs)"); indent.AppendLine($".WithCommandId(commandId ?? new {context.Qualifier.Qualify(RuntimeTypeNames.CommandId, context.RootNamespace)}(Guid.NewGuid().ToString()));"); indent.Dedent(); indent.AppendLine("if (!string.IsNullOrEmpty(workflowId))"); @@ -149,7 +209,7 @@ private void WriteInterfaceChoiceExtensionMethod( indent.Dedent(); indent.AppendLine("}"); indent.AppendLine(); - indent.AppendLine("return await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); + indent.AppendLine("return await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); indent.Dedent(); indent.AppendLine("}"); @@ -161,10 +221,7 @@ private void WriteInterfaceChoiceExtensionMethod( { return ("DamlUnit", false); } - if (choice.ArgumentType is DamlTypeRef { Name: "Archive", Module: "DA.Internal.Template" } archiveRef - && !string.IsNullOrEmpty(archiveRef.PackageId) - && resolver.LookupPackage(archiveRef.PackageId) is { } archivePkg - && (IsStdlibPackage(archivePkg.Name) || IsPlaceholderPackageName(archivePkg.Name))) + if (IsSyntheticArchive(choice)) { return ("DamlUnit", false); } diff --git a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.NonContractExercisers.cs b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.NonContractExercisers.cs index 5422a7b..bc89c64 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.NonContractExercisers.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.NonContractExercisers.cs @@ -7,32 +7,6 @@ namespace Daml.Codegen.CSharp.CodeGen; internal sealed partial class ChoiceEmitter { - /// - /// Returns true when the choice is the synthetic Archive - /// imported from DA.Internal.Template in a Daml stdlib package. - /// Archive's choice machinery is already exposed via the existing - /// Choice<Choice> property; a typed exerciser would add nothing. - /// Gating on the stdlib package id prevents a user-defined choice named - /// Archive with the same module path from being falsely suppressed. - /// - private bool IsArchiveChoice(DamlChoice choice) - { - if (!string.Equals(choice.Name, "Archive", StringComparison.Ordinal)) - { - return false; - } - if (choice.ArgumentType is not DamlTypeRef { Module: "DA.Internal.Template", Name: "Archive" } archiveTypeRef) - { - return false; - } - if (string.IsNullOrEmpty(archiveTypeRef.PackageId)) - { - return false; - } - var pkg = resolver.LookupPackage(archiveTypeRef.PackageId); - return pkg is not null && IsStdlibPackage(pkg.Name); - } - private string MapNonContractReturnType(DamlType returnType) => returnType switch { DamlPrimitiveType { Primitive: DamlPrimitive.Unit } => context.Qualifier.Qualify(RuntimeTypeNames.Unit, context.RootNamespace), @@ -104,8 +78,7 @@ internal bool TryWriteNonContractChoiceExtensions( var emittable = template.Choices .Where(c => { - if (ChoiceCreatedSlots.Extract(context, resolver, mapper, c.ReturnType).Count > 0 - || IsArchiveChoice(c)) + if (ChoiceCreatedSlots.Extract(context, resolver, mapper, c.ReturnType).Count > 0) { return false; } @@ -142,6 +115,8 @@ internal bool TryWriteNonContractChoiceExtensions( { indent.AppendLine(); } + WriteNonContractChoiceCommandBuilder(indent, emittable[i], className, dataTypes); + indent.AppendLine(); WriteSingleNonContractChoiceAsyncExerciser(indent, emittable[i], className, dataTypes); } @@ -159,6 +134,72 @@ internal bool TryWriteNonContractChoiceExtensions( return true; } + /// + /// Emits the <Choice>Command(this ContractId<TemplateName> contractId, ...) + /// builder that constructs the choice's + /// without submitting it. Called from 's + /// generated <Choice>Async method. + /// + private void WriteNonContractChoiceCommandBuilder( + IndentWriter indent, + DamlChoice choice, + string templateClassName, + IReadOnlyDictionary dataTypes) + { + var choiceName = SanitizeIdentifier(choice.Name); + var commandMethodName = $"{choiceName}Command"; + var (argTypeName, _, _, isNestedTemplateArg) = GetChoiceArgumentInfo(choice, dataTypes); + var hasArg = argTypeName != "DamlUnit"; + + if (options.GenerateXmlDocs) + { + indent.AppendLine("/// "); + indent.AppendLine($"/// Builds the for the {choice.Name} choice on this contract id."); + indent.AppendLine("/// "); + indent.AppendLine("/// The contract on which to exercise the choice."); + if (hasArg) + { + indent.AppendLine("/// The choice argument."); + } + } + + indent.AppendLine($"public static {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)} {commandMethodName}("); + indent.Indent(); + if (hasArg) + { + var argParamType = isNestedTemplateArg + ? $"{templateClassName}.{argTypeName}" + : argTypeName; + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{templateClassName}> contractId,"); + indent.AppendLine($"{argParamType} argument)"); + } + else + { + indent.AppendLine($"this {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{templateClassName}> contractId)"); + } + indent.Dedent(); + indent.AppendLine("{"); + indent.Indent(); + + indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); + if (hasArg) + { + indent.AppendLine("ArgumentNullException.ThrowIfNull(argument);"); + } + + var argExpr = hasArg ? "argument.ToRecord()" : EmptyArgumentExpression(choice); + indent.AppendLine($"return new {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}("); + indent.Indent(); + indent.AppendLine($"{templateClassName}.TemplateId,"); + indent.AppendLine("contractId,"); + indent.AppendLine($"new {context.Qualifier.Qualify(RuntimeTypeNames.ChoiceName, context.RootNamespace)}(\"{choice.Name}\"),"); + indent.AppendLine($"{argExpr});"); + indent.Dedent(); + + indent.Dedent(); + indent.AppendLine("}"); + } + private void WriteSingleNonContractChoiceAsyncExerciser( IndentWriter indent, DamlChoice choice, @@ -212,23 +253,16 @@ private void WriteSingleNonContractChoiceAsyncExerciser( indent.AppendLine("{"); indent.Indent(); - indent.AppendLine("ArgumentNullException.ThrowIfNull(contractId);"); indent.AppendLine("ArgumentNullException.ThrowIfNull(client);"); - var argExpr = hasArg ? "argument.ToRecord()" : $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance"; indent.AppendLine(); - indent.AppendLine($"var command = new {context.Qualifier.Qualify(RuntimeTypeNames.ExerciseCommand, context.RootNamespace)}("); - indent.Indent(); - indent.AppendLine($"{templateClassName}.TemplateId,"); - indent.AppendLine("contractId,"); - indent.AppendLine($"new {context.Qualifier.Qualify(RuntimeTypeNames.ChoiceName, context.RootNamespace)}(\"{choice.Name}\"),"); - indent.AppendLine($"{argExpr});"); - indent.Dedent(); + indent.AppendLine(hasArg + ? $"var command = contractId.{choiceName}Command(argument);" + : $"var command = contractId.{choiceName}Command();"); indent.AppendLine(); indent.AppendLine($"var submission = {context.Qualifier.Qualify(RuntimeTypeNames.CommandsSubmission, context.RootNamespace)}.Single(command)"); indent.Indent(); - indent.AppendLine(".WithActAs(actAs)"); indent.AppendLine($".WithCommandId(commandId ?? new {context.Qualifier.Qualify(RuntimeTypeNames.CommandId, context.RootNamespace)}(Guid.NewGuid().ToString()));"); indent.Dedent(); indent.AppendLine("if (!string.IsNullOrEmpty(workflowId))"); @@ -239,7 +273,7 @@ private void WriteSingleNonContractChoiceAsyncExerciser( indent.AppendLine("}"); indent.AppendLine(); - indent.AppendLine("var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); + indent.AppendLine("var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false);"); indent.AppendLine(); indent.AppendLine($"return outcome.ProjectCommitted(tx => Project{choiceName}Result(tx, contractId.Value));"); diff --git a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.cs b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.cs index 26cd19f..123fe9d 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/ChoiceEmitter.cs @@ -60,10 +60,7 @@ internal void WriteChoiceDescriptors(IndentWriter indent, DamlTemplate template) if (choice.ArgumentType is DamlTypeRef externalRef) { - if (externalRef is { Name: "Archive", Module: "DA.Internal.Template" } - && !string.IsNullOrEmpty(externalRef.PackageId) - && resolver.LookupPackage(externalRef.PackageId) is { } archivePkg - && (IsStdlibPackage(archivePkg.Name) || IsPlaceholderPackageName(archivePkg.Name))) + if (IsSyntheticArchive(choice)) { return ("DamlUnit", null, false, false); } @@ -127,7 +124,7 @@ private void WriteChoiceMethod(IndentWriter indent, DamlChoice choice) if (argTypeName == "DamlUnit") { - indent.AppendLine($"ArgumentEncoder = _ => {context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance,"); + indent.AppendLine($"ArgumentEncoder = _ => {EmptyArgumentExpression(choice)},"); } else { @@ -178,6 +175,25 @@ private static void RequireAsyncExerciserNamespaces(IndentWriter indent) indent.Require(RuntimeNamespaces.Outcomes); } + /// + /// True for the built-in stdlib DA.Internal.Template:Archive choice, whose + /// argument type is the empty record Archive {} but is not code-generated (no + /// generated Archive record exists). Distinguishes it from a genuine + /// Unit-argument choice so the argument encodes as an empty record — Canton's + /// gRPC command preprocessor type-checks the argument and rejects Unit against + /// the Archive choice signature. + /// + private bool IsSyntheticArchive(DamlChoice choice) => + choice.ArgumentType is DamlTypeRef { Name: "Archive", Module: "DA.Internal.Template" } archiveRef + && !string.IsNullOrEmpty(archiveRef.PackageId) + && resolver.LookupPackage(archiveRef.PackageId) is { } archivePkg + && (IsStdlibPackage(archivePkg.Name) || IsPlaceholderPackageName(archivePkg.Name)); + + private string EmptyArgumentExpression(DamlChoice choice) => + IsSyntheticArchive(choice) + ? $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}.Create()" + : $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance"; + private static bool IsStdlibPackage(string packageName) => StdlibPackages.IsStdlibPackage(packageName); private static bool IsPlaceholderPackageName(string packageName) => StdlibPackages.IsPlaceholderPackageName(packageName); diff --git a/src/Daml.Codegen.CSharp/CodeGen/DamlTypeMapper.cs b/src/Daml.Codegen.CSharp/CodeGen/DamlTypeMapper.cs index acd6a68..f6b2c14 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/DamlTypeMapper.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/DamlTypeMapper.cs @@ -7,7 +7,7 @@ namespace Daml.Codegen.CSharp.CodeGen; /// /// Turns a into C#: produces a C# type -/// name, and produce the serialize and +/// name, ToValue and FromValue produce the serialize and /// deserialize expressions. Constructed once per package over a /// and an , which /// it calls into for cross-package names — it does not own resolution. Pure functions @@ -54,9 +54,18 @@ private string MapType(DamlType type, int depth) private const string FallbackTypeName = "object"; /// Produces the expression that serializes of to a Daml value. - public string ToValue(DamlType type, string fieldName) => ToValue(type, fieldName, depth: 0); - - private string ToValue(DamlType type, string fieldName, int depth) + /// The Daml type of the field being serialized. + /// The C# expression referencing the field value. + /// + /// Maps a Daml type-variable name to the injected converter-delegate parameter name + /// in scope, supplied when emitting a generic record or variant's own body so a + /// field resolves to its converter instead of the runtime + /// stub. null outside a generic body. + /// + public string ToValue(DamlType type, string fieldName, IReadOnlyDictionary? typeVarDelegates = null) => + ToValue(type, fieldName, typeVarDelegates, depth: 0); + + private string ToValue(DamlType type, string fieldName, IReadOnlyDictionary? typeVarDelegates, int depth) { ThrowIfTooDeep(depth, nameof(ToValue)); @@ -76,24 +85,28 @@ private string ToValue(DamlType type, string fieldName, int depth) // (`{fieldName}`) keeps its `@` escape so the original record property is // still addressable. DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.Optional } } app => - $"{fieldName} is {{ }} __{fieldName.TrimStart('@')} ? new {context.Qualifier.Qualify(RuntimeTypeNames.DamlOptional, context.RootNamespace)}({ToValue(app.Arguments[0], $"__{fieldName.TrimStart('@')}", depth + 1)}) : {context.Qualifier.Qualify(RuntimeTypeNames.DamlOptional, context.RootNamespace)}.None", + $"{fieldName} is {{ }} __{fieldName.TrimStart('@')} ? new {context.Qualifier.Qualify(RuntimeTypeNames.DamlOptional, context.RootNamespace)}({ToValue(app.Arguments[0], $"__{fieldName.TrimStart('@')}", typeVarDelegates, depth + 1)}) : {context.Qualifier.Qualify(RuntimeTypeNames.DamlOptional, context.RootNamespace)}.None", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.List } } app => - $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlList, context.RootNamespace)}({fieldName}.Select(x => ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "x", depth + 1)}).ToList())", + $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlList, context.RootNamespace)}({fieldName}.Select(x => ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "x", typeVarDelegates, depth + 1)}).ToList())", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.TextMap } } app => - $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlTextMap, context.RootNamespace)}({fieldName}.ToDictionary(kv => kv.Key, kv => ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "kv.Value", depth + 1)}))", + $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlTextMap, context.RootNamespace)}({fieldName}.ToDictionary(kv => kv.Key, kv => ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "kv.Value", typeVarDelegates, depth + 1)}))", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.GenMap } } app => - $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlGenMap, context.RootNamespace)}({fieldName}.Select(kv => (({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "kv.Key", depth + 1)}, ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[1], "kv.Value", depth + 1)})).ToList())", + $"new {context.Qualifier.Qualify(RuntimeTypeNames.DamlGenMap, context.RootNamespace)}({fieldName}.Select(kv => (({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[0], "kv.Key", typeVarDelegates, depth + 1)}, ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)}){ToValue(app.Arguments[1], "kv.Value", typeVarDelegates, depth + 1)})).ToList())", DamlTypeApp { Base: DamlTypeRef typeRef } app when StdlibPackages.IsStdlibTypeRef(resolver, typeRef, parametric: true) => - EmitParametricStdlibToValue(typeRef, app.Arguments, fieldName, depth), + EmitParametricStdlibToValue(typeRef, app.Arguments, fieldName, typeVarDelegates, depth), DamlTypeRef typeRef when IsLocalEnumTypeRef(typeRef) => $"{fieldName}.ToDamlEnum()", DamlTypeRef typeRef when IsCrossPackageEnumTypeRef(typeRef) => QualifiedEnumExtensionsCall(typeRef, "ToDamlEnum", fieldName), - DamlTypeApp { Base: DamlTypeRef typeRef } when IsVariantTypeRef(typeRef) => - $"{fieldName}.ToVariant()", + DamlTypeApp { Base: DamlTypeRef typeRef } app when IsVariantTypeRef(typeRef) => + $"{fieldName}.ToVariant({string.Join(", ", ToValueConverterLambdas(app.Arguments, typeVarDelegates, depth))})", + DamlTypeApp { Base: DamlTypeRef typeRef } app when IsRecordTypeRef(typeRef) => + $"{fieldName}.ToRecord({string.Join(", ", ToValueConverterLambdas(app.Arguments, typeVarDelegates, depth))})", DamlTypeRef typeRef when IsVariantTypeRef(typeRef) => $"{fieldName}.ToVariant()", + DamlTypeVar typeVar when TryResolveDelegate(typeVarDelegates, typeVar, out var convert) => + $"{convert}({fieldName})", DamlTypeVar => FallbackToValueStub(fieldName), _ when MapsToFallbackObject(type, depth) => FallbackToValueStub(fieldName), _ => $"{fieldName}.ToRecord()" @@ -106,9 +119,18 @@ private string FallbackToValueStub(string fieldName) => private bool MapsToFallbackObject(DamlType type, int depth = 0) => MapType(type, depth) == FallbackTypeName; /// Produces the expression that deserializes back into . - public string FromValue(DamlType type, string valueName) => FromValue(type, valueName, depth: 0); - - private string FromValue(DamlType type, string valueName, int depth) + /// The Daml type to reconstruct. + /// The C# expression referencing the Daml value. + /// + /// Maps a Daml type-variable name to the injected converter-delegate parameter name + /// in scope, supplied when emitting a generic record or variant's own body so a + /// field resolves to its converter instead of the runtime + /// stub. null outside a generic body. + /// + public string FromValue(DamlType type, string valueName, IReadOnlyDictionary? typeVarDelegates = null) => + FromValue(type, valueName, typeVarDelegates, depth: 0); + + private string FromValue(DamlType type, string valueName, IReadOnlyDictionary? typeVarDelegates, int depth) { ThrowIfTooDeep(depth, nameof(FromValue)); @@ -118,30 +140,49 @@ private string FromValue(DamlType type, string valueName, int depth) DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.Numeric } } => $"{valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlNumeric, context.RootNamespace)}>().Value", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.Optional }, Arguments: [var arg] } => - $"{valueName}.AsOptional().HasValue ? {FromValue(arg, $"{valueName}.AsOptional().Value!", depth + 1)} : null", + $"{valueName}.AsOptional().HasValue ? {FromValue(arg, $"{valueName}.AsOptional().Value!", typeVarDelegates, depth + 1)} : null", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.List }, Arguments: [var arg] } => - $"({context.Qualifier.Qualify("IReadOnlyList", context.RootNamespace)}<{MapType(arg, depth + 1)}>){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlList, context.RootNamespace)}>().Values.Select(x => {FromValue(arg, "x", depth + 1)}).ToList()", + $"({context.Qualifier.Qualify("IReadOnlyList", context.RootNamespace)}<{MapType(arg, depth + 1)}>){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlList, context.RootNamespace)}>().Values.Select(x => {FromValue(arg, "x", typeVarDelegates, depth + 1)}).ToList()", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.TextMap }, Arguments: [var arg] } => - $"({context.Qualifier.Qualify("IReadOnlyDictionary", context.RootNamespace)}){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlTextMap, context.RootNamespace)}>().Values.ToDictionary(kv => kv.Key, kv => {FromValue(arg, "kv.Value", depth + 1)})", + $"({context.Qualifier.Qualify("IReadOnlyDictionary", context.RootNamespace)}){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlTextMap, context.RootNamespace)}>().Values.ToDictionary(kv => kv.Key, kv => {FromValue(arg, "kv.Value", typeVarDelegates, depth + 1)})", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.GenMap }, Arguments: [var keyArg, var valueArg] } => - $"({context.Qualifier.Qualify("IReadOnlyDictionary", context.RootNamespace)}<{MapType(keyArg, depth + 1)}, {MapType(valueArg, depth + 1)}>){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlGenMap, context.RootNamespace)}>().Entries.ToDictionary(kv => {FromValue(keyArg, "kv.Key", depth + 1)}, kv => {FromValue(valueArg, "kv.Value", depth + 1)})", + $"({context.Qualifier.Qualify("IReadOnlyDictionary", context.RootNamespace)}<{MapType(keyArg, depth + 1)}, {MapType(valueArg, depth + 1)}>){valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlGenMap, context.RootNamespace)}>().Entries.ToDictionary(kv => {FromValue(keyArg, "kv.Key", typeVarDelegates, depth + 1)}, kv => {FromValue(valueArg, "kv.Value", typeVarDelegates, depth + 1)})", DamlTypeApp { Base: DamlPrimitiveType { Primitive: DamlPrimitive.ContractId }, Arguments: [var arg] } => $"new {context.Qualifier.Qualify(RuntimeTypeNames.ContractId, context.RootNamespace)}<{MapType(arg, depth + 1)}>({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlContractId, context.RootNamespace)}>().Value)", DamlTypeApp { Base: DamlTypeRef typeRef } app when StdlibPackages.IsStdlibTypeRef(resolver, typeRef, parametric: true) => - EmitParametricStdlibFromValue(typeRef, app.Arguments, valueName, depth), + EmitParametricStdlibFromValue(typeRef, app.Arguments, valueName, typeVarDelegates, depth), DamlTypeRef typeRef when IsEnumTypeRef(typeRef) => QualifiedEnumExtensionsCall(typeRef, "FromDamlEnum", $"{valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlEnum, context.RootNamespace)}>()"), DamlTypeRef typeRef when IsVariantTypeRef(typeRef) => $"{resolver.Resolve(typeRef, context)}.FromVariant({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)}>())", DamlTypeRef typeRef => $"{resolver.Resolve(typeRef, context)}.FromRecord({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}>())", DamlTypeApp { Base: DamlTypeRef typeRef } app when IsVariantTypeRef(typeRef) => - $"{resolver.Resolve(typeRef, context)}<{string.Join(", ", app.Arguments.Select(arg => MapType(arg, depth + 1)))}>.FromVariant({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)}>())", + $"{resolver.Resolve(typeRef, context)}<{string.Join(", ", app.Arguments.Select(arg => MapType(arg, depth + 1)))}>.FromVariant({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)}>(), {string.Join(", ", FromValueConverterLambdas(app.Arguments, typeVarDelegates, depth))})", + DamlTypeApp { Base: DamlTypeRef typeRef } app when IsRecordTypeRef(typeRef) => + $"{resolver.Resolve(typeRef, context)}<{string.Join(", ", app.Arguments.Select(arg => MapType(arg, depth + 1)))}>.FromRecord({valueName}.As<{context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}>(), {string.Join(", ", FromValueConverterLambdas(app.Arguments, typeVarDelegates, depth))})", + DamlTypeVar typeVar when TryResolveDelegate(typeVarDelegates, typeVar, out var convert) => + $"{convert}({valueName})", DamlTypeVar typeVar => $"{context.Qualifier.Qualify(RuntimeTypeNames.GenericStub, context.RootNamespace)}.NotImplemented<{EmitterHelpers.TypeParameterName(typeVar.Name)}>(\"{typeVar.Name}\")", _ => $"default({MapType(type, depth)})! /* TODO: Implement deserialization for {type} */" }; } + private static bool TryResolveDelegate( + IReadOnlyDictionary? typeVarDelegates, + DamlTypeVar typeVar, + out string convert) + { + if (typeVarDelegates is not null && typeVarDelegates.TryGetValue(typeVar.Name, out var resolved)) + { + convert = resolved; + return true; + } + + convert = string.Empty; + return false; + } + // CS8524 disabled (not CS8509): the no-default switch covers every named // DamlPrimitive so an out-of-range cast is the only uncovered input. CS8509 // (a newly-added named member left unhandled) stays an error — that is the @@ -245,27 +286,27 @@ private sealed record StdlibConversion( internal IReadOnlySet<(string Module, string Name)> StdlibConversionKeys => StdlibConversions.Keys.ToHashSet(); - private string EmitParametricStdlibToValue(DamlTypeRef typeRef, IReadOnlyList arguments, string fieldName, int depth) - { - var converters = arguments.Select((arg, i) => - $"({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)})({ToValue(arg, $"__t{i}", depth + 1)})").ToList(); - var lambdas = arguments.Select((_, i) => - $"__t{i} => {converters[i]}").ToList(); - return ConversionFor(typeRef).Serialize(fieldName, lambdas); - } + private string EmitParametricStdlibToValue(DamlTypeRef typeRef, IReadOnlyList arguments, string fieldName, IReadOnlyDictionary? typeVarDelegates, int depth) => + ConversionFor(typeRef).Serialize(fieldName, ToValueConverterLambdas(arguments, typeVarDelegates, depth)); - private string EmitParametricStdlibFromValue(DamlTypeRef typeRef, IReadOnlyList arguments, string valueName, int depth) + private string EmitParametricStdlibFromValue(DamlTypeRef typeRef, IReadOnlyList arguments, string valueName, IReadOnlyDictionary? typeVarDelegates, int depth) { var stdlibName = context.Qualifier.Qualify( StdlibPackages.MapStdlibType(typeRef.Module, typeRef.Name) ?? throw new InvalidOperationException($"No stdlib mapping for {typeRef.Module}:{typeRef.Name}"), context.RootNamespace); var typeArgs = string.Join(", ", arguments.Select(arg => MapType(arg, depth + 1))); - var lambdas = arguments.Select((arg, i) => - $"__v{i} => {FromValue(arg, $"__v{i}", depth + 1)}").ToList(); - return ConversionFor(typeRef).Deserialize(valueName, stdlibName, typeArgs, lambdas); + return ConversionFor(typeRef).Deserialize(valueName, stdlibName, typeArgs, FromValueConverterLambdas(arguments, typeVarDelegates, depth)); } + private IReadOnlyList ToValueConverterLambdas(IReadOnlyList arguments, IReadOnlyDictionary? typeVarDelegates, int depth) => + arguments.Select((arg, i) => + $"__t{i} => ({context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)})({ToValue(arg, $"__t{i}", typeVarDelegates, depth + 1)})").ToList(); + + private IReadOnlyList FromValueConverterLambdas(IReadOnlyList arguments, IReadOnlyDictionary? typeVarDelegates, int depth) => + arguments.Select((arg, i) => + $"__v{i} => {FromValue(arg, $"__v{i}", typeVarDelegates, depth + 1)}").ToList(); + private StdlibConversion ConversionFor(DamlTypeRef typeRef) => StdlibConversions.TryGetValue((typeRef.Module, typeRef.Name), out var conversion) ? conversion @@ -301,6 +342,22 @@ private bool IsCrossPackageVariantTypeRef(DamlTypeRef typeRef) => private bool IsVariantTypeRef(DamlTypeRef typeRef) => IsLocalVariantTypeRef(typeRef) || IsCrossPackageVariantTypeRef(typeRef); + private bool IsLocalRecordTypeRef(DamlTypeRef typeRef) => + context.IsLocalRef(typeRef) + && context.DataTypes.TryGetValue($"{typeRef.Module}:{typeRef.Name}", out var dataType) + && dataType.Definition is DamlRecordDefinition; + + private bool IsCrossPackageRecordTypeRef(DamlTypeRef typeRef) => + !context.IsLocalRef(typeRef) + && (resolver.LookupPackage(typeRef.PackageId)?.Modules + .Where(m => m.Name == typeRef.Module) + .SelectMany(m => m.DataTypes) + .Any(dt => dt.Name == typeRef.Name && dt.Definition is DamlRecordDefinition) + ?? false); + + private bool IsRecordTypeRef(DamlTypeRef typeRef) => + IsLocalRecordTypeRef(typeRef) || IsCrossPackageRecordTypeRef(typeRef); + private string QualifiedEnumExtensionsCall(DamlTypeRef typeRef, string method, string argument) => $"{resolver.Resolve(typeRef, context)}Extensions.{method}({argument})"; diff --git a/src/Daml.Codegen.CSharp/CodeGen/EmitterHelpers.cs b/src/Daml.Codegen.CSharp/CodeGen/EmitterHelpers.cs index eabfd0f..270cb6e 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/EmitterHelpers.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/EmitterHelpers.cs @@ -35,6 +35,31 @@ internal static string GetTypeParametersDeclaration(IReadOnlyList typePa return $"<{string.Join(", ", typeParams.Select(TypeParameterName))}>"; } + /// + /// Derives the injected converter-delegate parameter name for a Daml type + /// variable — the delegate a generic record/variant's ToRecord/FromRecord + /// (or ToVariant/FromVariant) accepts to bridge that type parameter's + /// concrete CLR argument to and from . + /// + internal static string ConverterParameterName(string damlTypeParam) => + $"convert{TypeParameterName(damlTypeParam)}"; + + /// + /// Maps each Daml type-variable name to its , + /// threaded into the 's ToValue/FromValue + /// so a field resolves to its injected converter. + /// + internal static IReadOnlyDictionary ConverterNameMap(IReadOnlyList typeParams) => + typeParams.ToDictionary(param => param, ConverterParameterName); + + internal static string SerializeConverterParameters(IReadOnlyList typeParams, string qualifiedDamlValue) => + string.Join(", ", typeParams.Select(param => + $"Func<{TypeParameterName(param)}, {qualifiedDamlValue}> {ConverterParameterName(param)}")); + + internal static string DeserializeConverterParameters(IReadOnlyList typeParams, string qualifiedDamlValue) => + string.Join(", ", typeParams.Select(param => + $"Func<{qualifiedDamlValue}, {TypeParameterName(param)}> {ConverterParameterName(param)}")); + internal static void WriteTypeParamDocs(IndentWriter indent, IReadOnlyList typeParams) { foreach (var param in typeParams) diff --git a/src/Daml.Codegen.CSharp/CodeGen/PackageEmitContext.cs b/src/Daml.Codegen.CSharp/CodeGen/PackageEmitContext.cs index 432318d..ce0f6d1 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/PackageEmitContext.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/PackageEmitContext.cs @@ -37,9 +37,11 @@ internal sealed class PackageEmitContext /// records (they are replaced by the marker itself, so counting them would falsely /// self-disambiguate) and choice-argument records (they are emitted nested inside /// their parent template, not at the top level). The package's C# namespace is flat - /// across all its modules, so this is the reserved-name input - /// disambiguates interface marker - /// names against. + /// across all its modules, so this set has two consumers: it is the reserved-name + /// input disambiguates interface marker + /// names against, and it is passed to so a package-declared + /// type that collides with an imported runtime/BCL name (e.g. a Daml enum Unit) + /// is qualified with global:: instead of silently shadowing it. /// public IReadOnlySet LocalReservedTypeNames { get; } @@ -127,7 +129,8 @@ public static PackageEmitContext ForPackage( ArgumentNullException.ThrowIfNull(options); var rootNamespace = options.RootNamespace ?? Identifiers.DeriveNamespace(package.Name); - var qualifier = new TypeReferenceQualifier([rootNamespace]); + var localReservedTypeNames = ReservedTopLevelTypeNames(package); + var qualifier = new TypeReferenceQualifier([rootNamespace], localReservedTypeNames); var dataTypes = new Dictionary(); var localEnumQualifiedNames = new HashSet(); @@ -181,7 +184,6 @@ public static PackageEmitContext ForPackage( } } - var localReservedTypeNames = ReservedTopLevelTypeNames(package); var localInterfaceMarkerNames = InterfaceMarkerNames(package, localReservedTypeNames); return new PackageEmitContext( diff --git a/src/Daml.Codegen.CSharp/CodeGen/RecordEmitter.cs b/src/Daml.Codegen.CSharp/CodeGen/RecordEmitter.cs index 6f202b3..50f2654 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/RecordEmitter.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/RecordEmitter.cs @@ -54,27 +54,40 @@ internal void WriteRecordType(IndentWriter indent, DamlModule module, DamlDataTy EmitterHelpers.WriteTypeParamDocs(indent, dataType.TypeParams); } + var recordInterface = InterfaceDeclaration(dataType.TypeParams); + if (options.UseRecordTypes && options.UsePrimaryConstructors && record.Fields.Count > 0) { indent.Append($"public sealed record {fullClassName}("); serialization.WriteRecordParameters(indent, record.Fields); - indent.AppendLine($") : {context.Qualifier.Qualify(RuntimeTypeNames.IDamlRecord, context.RootNamespace)}"); + indent.AppendLine($"){recordInterface}"); } else { - indent.AppendLine($"public sealed record {fullClassName} : {context.Qualifier.Qualify(RuntimeTypeNames.IDamlRecord, context.RootNamespace)}"); + indent.AppendLine($"public sealed record {fullClassName}{recordInterface}"); } indent.AppendLine("{"); indent.Indent(); - serialization.WriteToRecordMethod(indent, record.Fields); - serialization.WriteFromRecordMethod(indent, fullClassName, record.Fields); + serialization.WriteToRecordMethod(indent, record.Fields, dataType.TypeParams); + serialization.WriteFromRecordMethod(indent, fullClassName, record.Fields, dataType.TypeParams); indent.Dedent(); indent.AppendLine("}"); } + /// + /// Returns the record's interface clause: : IDamlRecord for non-generic records, + /// or empty for generic records, whose ToRecord takes one converter delegate per + /// type parameter and so cannot satisfy the parameterless IDamlRecord.ToRecord() + /// contract — matching the hand-written stdlib generics (Set, Tuple2, NonEmpty). + /// + private string InterfaceDeclaration(IReadOnlyList typeParams) => + typeParams.Count == 0 + ? $" : {context.Qualifier.Qualify(RuntimeTypeNames.IDamlRecord, context.RootNamespace)}" + : string.Empty; + /// /// Emits the C# placeholder for a Daml interface declaration. The Daml-LF emits a /// same-named empty record alongside every interface I where ... so that @@ -83,8 +96,9 @@ internal void WriteRecordType(IndentWriter indent, DamlModule module, DamlDataTy /// metadata: it lets ContractId<I> compile (the runtime constraint is /// where T : ITemplate) but loudly fails any code path that tries to read /// I.TemplateId directly — which would be a logic error, since interface - /// placeholders carry no template identity. Coerce the contract id to the - /// underlying template type before reading metadata or constructing commands. + /// placeholders carry no template identity. Interface choices remain exercisable + /// directly on ContractId<I>; there is no supported way to recover a + /// concrete ContractId<TConcrete> from this placeholder. /// private void WriteInterfacePlaceholderRecord(IndentWriter indent, DamlModule module, DamlDataType dataType) { @@ -95,7 +109,7 @@ private void WriteInterfacePlaceholderRecord(IndentWriter indent, DamlModule mod var throwMessage = $"'{className}' is the C# placeholder for the Daml interface " + $"'{qualifiedDamlName}' and carries no template metadata. " - + "Coerce ContractId<" + className + "> to a typed ContractId before reading template metadata or exercising commands."; + + "Exercise interface choices directly on ContractId<" + className + ">; there is no supported coercion to a concrete ContractId."; if (options.GenerateXmlDocs) { @@ -105,45 +119,62 @@ private void WriteInterfacePlaceholderRecord(IndentWriter indent, DamlModule mod indent.AppendLine("/// satisfies its where T : ITemplate constraint, but every static"); indent.AppendLine("/// metadata accessor throws — interface placeholders carry no template identity."); indent.AppendLine("/// "); + indent.AppendLine("/// "); + indent.AppendLine("/// Interface-instance choices need a concrete template to compile against, but the Daml"); + indent.AppendLine($"/// interface {qualifiedDamlName} has none, so every static metadata accessor on this"); + indent.AppendLine("/// placeholder throws by design. This does not affect interface choices, which the generated"); + indent.AppendLine($"/// extension methods exercise directly on ContractId<{className}> — no coercion"); + indent.AppendLine("/// needed. There is no supported way to recover a concrete ContractId<TConcrete>"); + indent.AppendLine("/// from this placeholder; obtain one independently if concrete metadata is required."); + indent.AppendLine("/// "); } indent.AppendLine($"public sealed record {className} : {context.Qualifier.Qualify(RuntimeTypeNames.ITemplate, context.RootNamespace)}"); indent.AppendLine("{"); indent.Indent(); - void WritePlaceholderDoc(string summary) + void WritePlaceholderDoc(string summary, string? identityPhrase = null) { if (options.GenerateXmlDocs) { indent.AppendLine($"/// {summary}"); + if (identityPhrase is not null) + { + indent.AppendLine($"/// Thrown unconditionally — this placeholder carries no {identityPhrase} to report."); + } } } - WritePlaceholderDoc($"Always throws — the {qualifiedDamlName} interface placeholder carries no template identity."); + void WriteThrowingMemberDoc(string identityPhrase) => + WritePlaceholderDoc( + $"Always throws — the {qualifiedDamlName} interface placeholder carries no {identityPhrase}.", + identityPhrase); + + WriteThrowingMemberDoc("template identity"); indent.AppendLine($"public static {context.Qualifier.Qualify(RuntimeTypeNames.Identifier, context.RootNamespace)} TemplateId =>"); indent.Indent(); indent.AppendLine($"throw new InvalidOperationException(\"{throwMessage}\");"); indent.Dedent(); indent.AppendLine(); - WritePlaceholderDoc($"Always throws — the {qualifiedDamlName} interface placeholder carries no package identity."); + WriteThrowingMemberDoc("package identity"); indent.AppendLine("public static string PackageId =>"); indent.Indent(); indent.AppendLine($"throw new InvalidOperationException(\"{throwMessage}\");"); indent.Dedent(); indent.AppendLine(); - WritePlaceholderDoc($"Always throws — the {qualifiedDamlName} interface placeholder carries no package identity."); + WriteThrowingMemberDoc("package identity"); indent.AppendLine("public static string PackageName =>"); indent.Indent(); indent.AppendLine($"throw new InvalidOperationException(\"{throwMessage}\");"); indent.Dedent(); indent.AppendLine(); - WritePlaceholderDoc($"Always throws — the {qualifiedDamlName} interface placeholder carries no package identity."); + WriteThrowingMemberDoc("package identity"); indent.AppendLine("public static Version PackageVersion =>"); indent.Indent(); indent.AppendLine($"throw new InvalidOperationException(\"{throwMessage}\");"); indent.Dedent(); indent.AppendLine(); - WritePlaceholderDoc($"Always throws — the {qualifiedDamlName} interface placeholder carries no Daml type identity."); + WriteThrowingMemberDoc("Daml type identity"); indent.AppendLine($"public static {context.Qualifier.Qualify(RuntimeTypeNames.DamlTypeDescriptor, context.RootNamespace)} DamlTypeId =>"); indent.Indent(); indent.AppendLine($"throw new InvalidOperationException(\"{throwMessage}\");"); diff --git a/src/Daml.Codegen.CSharp/CodeGen/RecordSerializationEmitter.cs b/src/Daml.Codegen.CSharp/CodeGen/RecordSerializationEmitter.cs index 4da22f5..487e498 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/RecordSerializationEmitter.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/RecordSerializationEmitter.cs @@ -68,30 +68,35 @@ internal void WriteProperties(IndentWriter indent, IReadOnlyList /// Writes the ToRecord method that serializes to a - /// DamlRecord into . + /// DamlRecord into . When is + /// non-empty the method accepts one Func<T, DamlValue> converter per type + /// parameter, and type-variable fields serialize through the matching converter. /// - internal void WriteToRecordMethod(IndentWriter indent, IReadOnlyList fields) + internal void WriteToRecordMethod(IndentWriter indent, IReadOnlyList fields, IReadOnlyList typeParams) { if (options.GenerateXmlDocs) { indent.AppendLine("/// Converts this value to a DamlRecord."); } + var parameters = ConverterParameters(indent, typeParams, EmitterHelpers.SerializeConverterParameters); + var delegates = EmitterHelpers.ConverterNameMap(typeParams); + if (fields.Count == 0) { - indent.AppendLine($"public {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} ToRecord() => {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}.Create();"); + indent.AppendLine($"public {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} ToRecord({parameters}) => {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}.Create();"); indent.AppendLine(); return; } - indent.AppendLine($"public {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} ToRecord() => {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}.Create("); + indent.AppendLine($"public {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} ToRecord({parameters}) => {context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)}.Create("); indent.Indent(); for (int i = 0; i < fields.Count; i++) { var field = fields[i]; var fieldName = MemberName(field.Name, indent.CurrentTypeName); - var conversion = mapper.ToValue(field.Type, fieldName); + var conversion = mapper.ToValue(field.Type, fieldName, delegates); var comma = i < fields.Count - 1 ? "," : ""; StdlibPackages.RequireForFieldType(resolver, indent, field.Type); @@ -108,16 +113,20 @@ internal void WriteToRecordMethod(IndentWriter indent, IReadOnlyList instance from a DamlRecord into /// . /// - internal void WriteFromRecordMethod(IndentWriter indent, string className, IReadOnlyList fields) + internal void WriteFromRecordMethod(IndentWriter indent, string className, IReadOnlyList fields, IReadOnlyList typeParams) { if (options.GenerateXmlDocs) { indent.AppendLine("/// Creates an instance from a DamlRecord."); } + var converterParameters = ConverterParameters(indent, typeParams, EmitterHelpers.DeserializeConverterParameters); + var parameters = $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} record{Prefixed(converterParameters)}"; + var delegates = EmitterHelpers.ConverterNameMap(typeParams); + if (fields.Count == 0) { - indent.AppendLine($"public static {className} FromRecord({context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} record) => new {className}();"); + indent.AppendLine($"public static {className} FromRecord({parameters}) => new {className}();"); indent.AppendLine(); return; } @@ -129,14 +138,14 @@ internal void WriteFromRecordMethod(IndentWriter indent, string className, IRead if (options.UseRecordTypes && options.UsePrimaryConstructors) { - indent.AppendLine($"public static {className} FromRecord({context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} record) => new {className}("); + indent.AppendLine($"public static {className} FromRecord({parameters}) => new {className}("); indent.Indent(); for (int i = 0; i < fields.Count; i++) { var field = fields[i]; var fieldName = MemberName(field.Name, indent.CurrentTypeName); - var conversion = mapper.FromValue(field.Type, $"record.GetRequiredField(\"{field.Name}\")"); + var conversion = mapper.FromValue(field.Type, $"record.GetRequiredField(\"{field.Name}\")", delegates); var comma = i < fields.Count - 1 ? "," : ""; indent.AppendLine($"{fieldName}: {conversion}{comma}"); @@ -147,7 +156,7 @@ internal void WriteFromRecordMethod(IndentWriter indent, string className, IRead } else { - indent.AppendLine($"public static {className} FromRecord({context.Qualifier.Qualify(RuntimeTypeNames.DamlRecord, context.RootNamespace)} record)"); + indent.AppendLine($"public static {className} FromRecord({parameters})"); indent.AppendLine("{"); indent.Indent(); @@ -158,7 +167,7 @@ internal void WriteFromRecordMethod(IndentWriter indent, string className, IRead foreach (var field in fields) { var fieldName = MemberName(field.Name, indent.CurrentTypeName); - var conversion = mapper.FromValue(field.Type, $"record.GetRequiredField(\"{field.Name}\")"); + var conversion = mapper.FromValue(field.Type, $"record.GetRequiredField(\"{field.Name}\")", delegates); indent.AppendLine($"{fieldName} = {conversion},"); } @@ -171,6 +180,23 @@ internal void WriteFromRecordMethod(IndentWriter indent, string className, IRead indent.AppendLine(); } + private string ConverterParameters( + IndentWriter indent, + IReadOnlyList typeParams, + Func, string, string> build) + { + if (typeParams.Count == 0) + { + return string.Empty; + } + + indent.Require("System"); + return build(typeParams, context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace)); + } + + private static string Prefixed(string parameters) => + string.IsNullOrEmpty(parameters) ? string.Empty : $", {parameters}"; + private string DamlFieldAttributeSyntax(string damlFieldName) => $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlFieldAttribute, context.RootNamespace)}(\"{damlFieldName}\")"; diff --git a/src/Daml.Codegen.CSharp/CodeGen/RuntimeTypeNames.cs b/src/Daml.Codegen.CSharp/CodeGen/RuntimeTypeNames.cs index f769ae6..085c41b 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/RuntimeTypeNames.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/RuntimeTypeNames.cs @@ -62,14 +62,16 @@ internal static class RuntimeTypeNames public const string Unit = nameof(Daml.Runtime.Stdlib.Unit); public const string GenericStub = nameof(Daml.Runtime.Stdlib.GenericStub); - // The next four stay string literals rather than nameof(...) because each - // constrains a type parameter on ITemplate, whose static-abstract members - // make it unusable as a nameof type argument (CS8920), and no concrete - // implementation is visible to the codegen project. + // These stay string literals rather than nameof(...) because each + // constrains a type parameter to a runtime interface (ITemplate or + // IDamlInterface) whose static-abstract members make it unusable as a + // nameof type argument (CS8920), and no concrete implementation is visible + // to the codegen project. public const string Contract = "Contract"; public const string IContract = "IContract"; public const string Choice = "Choice"; public const string IExercises = "IExercises"; + public const string IImplements = "IImplements"; public const string ILedgerClient = nameof(Daml.Ledger.Abstractions.ILedgerClient); public const string ILedgerWriter = nameof(Daml.Ledger.Abstractions.ILedgerWriter); diff --git a/src/Daml.Codegen.CSharp/CodeGen/TemplateEmitter.cs b/src/Daml.Codegen.CSharp/CodeGen/TemplateEmitter.cs index a8ff347..8eda0a9 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/TemplateEmitter.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/TemplateEmitter.cs @@ -9,8 +9,9 @@ namespace Daml.Codegen.CSharp.CodeGen; /// /// Emits the C# for a Daml template: the sealed template record with its /// facet (plus the optional -/// and -/// IUpgradeable facets), the static template metadata, the throwing +/// and IUpgradeable +/// facets, plus one IImplements per implemented interface), the static +/// template metadata, the throwing /// contract-key accessor, the nested ContractId / Contract records, /// and the namespace-level choice / submission extension surface. The /// field-bearing serialization surface (constructor parameters, properties, @@ -67,6 +68,8 @@ internal void WriteTemplateType( interfacesList.Add($"{context.Qualifier.Qualify(RuntimeTypeNames.IHasKey, context.RootNamespace)}<{keyType}>"); if (package.UpgradedPackageId is not null) interfacesList.Add(context.Qualifier.Qualify(RuntimeTypeNames.IUpgradeable, context.RootNamespace)); + foreach (var implemented in template.Implements) + interfacesList.Add($"{context.Qualifier.Qualify(RuntimeTypeNames.IImplements, context.RootNamespace)}<{resolver.Resolve(implemented, context)}>"); var interfaces = string.Join(", ", interfacesList); if (options.UseRecordTypes && options.UsePrimaryConstructors && fields.Count > 0) @@ -99,8 +102,8 @@ internal void WriteTemplateType( recordSerialization.WriteProperties(indent, fields); } - recordSerialization.WriteToRecordMethod(indent, fields); - recordSerialization.WriteFromRecordMethod(indent, className, fields); + recordSerialization.WriteToRecordMethod(indent, fields, []); + recordSerialization.WriteFromRecordMethod(indent, className, fields, []); choiceEmitter.WriteChoiceDescriptors(indent, template); @@ -159,8 +162,8 @@ internal void WriteNestedChoiceArgumentType( indent.AppendLine("{"); indent.Indent(); - recordSerialization.WriteToRecordMethod(indent, record.Fields); - recordSerialization.WriteFromRecordMethod(indent, choiceTypeName, record.Fields); + recordSerialization.WriteToRecordMethod(indent, record.Fields, []); + recordSerialization.WriteFromRecordMethod(indent, choiceTypeName, record.Fields, []); indent.Dedent(); indent.AppendLine("}"); diff --git a/src/Daml.Codegen.CSharp/CodeGen/TypeReferenceQualifier.cs b/src/Daml.Codegen.CSharp/CodeGen/TypeReferenceQualifier.cs index b28d90f..bee0c2e 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/TypeReferenceQualifier.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/TypeReferenceQualifier.cs @@ -7,11 +7,14 @@ namespace Daml.Codegen.CSharp.CodeGen; /// -/// Resolves whether an imported runtime/BCL simple type name is shadowed by a -/// generated namespace segment in a given file, and qualifies it with -/// global:: when it is. C# binds an unqualified simple name by walking -/// enclosing namespace scopes before consulting using directives, so a -/// generated namespace segment equal to an imported type name produces CS0118. +/// Resolves whether an imported runtime/BCL simple type name is shadowed — +/// either by a generated namespace segment, or by a top-level type the target +/// Daml package itself declares — and qualifies it with global:: when it +/// is. C# binds an unqualified simple name by walking enclosing namespace +/// scopes and sibling type declarations before consulting using +/// directives, so either a generated namespace segment or a package-declared +/// type equal to an imported type name (e.g. a Daml enum Unit) produces +/// CS0118 / CS0117 unless qualified. /// internal sealed class TypeReferenceQualifier { @@ -69,6 +72,7 @@ internal sealed class TypeReferenceQualifier [RuntimeTypeNames.ChoiceName] = RuntimeNamespaces.Commands, [RuntimeTypeNames.IDamlInterface] = RuntimeNamespaces.Contracts, [RuntimeTypeNames.IHasView] = RuntimeNamespaces.Contracts, + [RuntimeTypeNames.IImplements] = RuntimeNamespaces.Contracts, [RuntimeTypeNames.CreatedEvent] = RuntimeNamespaces.Contracts, [RuntimeTypeNames.DamlTypeDescriptor] = RuntimeNamespaces.Contracts, [RuntimeTypeNames.DamlTypeKind] = RuntimeNamespaces.Contracts, @@ -93,8 +97,22 @@ internal sealed class TypeReferenceQualifier /// Every generated namespace plus all its ancestor prefixes, used for shadowing checks. public IReadOnlySet AllNamespaces { get; } - /// Creates a qualifier scoped to the given generated namespaces (ancestors are derived automatically). - public TypeReferenceQualifier(IEnumerable generatedNamespaces) + /// + /// Sanitised C# names of every top-level type the target Daml package declares (see + /// ), used for shadowing checks + /// alongside . Since the package's C# namespace is flat + /// across all its modules, any name in this set shadows the corresponding imported + /// runtime/BCL type everywhere in the package regardless of which module the reference + /// is emitted into. + /// + public IReadOnlySet DeclaredTypeNames { get; } + + /// + /// Creates a qualifier scoped to the given generated namespaces (ancestors are derived + /// automatically) and the package's own declared top-level type names. + /// + public TypeReferenceQualifier( + IEnumerable generatedNamespaces, IEnumerable? declaredTypeNames = null) { var all = new HashSet(StringComparer.Ordinal); foreach (var ns in generatedNamespaces) @@ -103,6 +121,7 @@ public TypeReferenceQualifier(IEnumerable generatedNamespaces) } AllNamespaces = all; + DeclaredTypeNames = new HashSet(declaredTypeNames ?? [], StringComparer.Ordinal); } /// @@ -137,6 +156,11 @@ private string QualifyHead(string simpleName, string currentNamespace) private bool IsShadowed(string simpleName, string currentNamespace) { + if (DeclaredTypeNames.Contains(simpleName)) + { + return true; + } + if (AllNamespaces.Contains(simpleName)) { return true; diff --git a/src/Daml.Codegen.CSharp/CodeGen/VariantEmitter.cs b/src/Daml.Codegen.CSharp/CodeGen/VariantEmitter.cs index 6a4e34b..a130166 100644 --- a/src/Daml.Codegen.CSharp/CodeGen/VariantEmitter.cs +++ b/src/Daml.Codegen.CSharp/CodeGen/VariantEmitter.cs @@ -44,7 +44,14 @@ internal void WriteVariantType(IndentWriter indent, DamlDataType dataType, DamlV EmitterHelpers.WriteTypeParamDocs(indent, dataType.TypeParams); } - indent.AppendLine($"public abstract record {fullClassName} : {context.Qualifier.Qualify(RuntimeTypeNames.IDamlVariant, context.RootNamespace)}"); + var qualifiedDamlValue = context.Qualifier.Qualify(RuntimeTypeNames.DamlValue, context.RootNamespace); + var toVariantParameters = EmitterHelpers.SerializeConverterParameters(dataType.TypeParams, qualifiedDamlValue); + var fromVariantConverters = EmitterHelpers.DeserializeConverterParameters(dataType.TypeParams, qualifiedDamlValue); + var fromVariantParameters = $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} variant{Prefixed(fromVariantConverters)}"; + var delegates = EmitterHelpers.ConverterNameMap(dataType.TypeParams); + + var variantInterface = InterfaceDeclaration(dataType.TypeParams); + indent.AppendLine($"public abstract record {fullClassName}{variantInterface}"); indent.AppendLine("{"); indent.Indent(); @@ -59,14 +66,14 @@ internal void WriteVariantType(IndentWriter indent, DamlDataType dataType, DamlV { indent.AppendLine("/// Converts to a DamlVariant."); } - indent.AppendLine($"public abstract {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} ToVariant();"); + indent.AppendLine($"public abstract {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} ToVariant({toVariantParameters});"); indent.AppendLine(); if (options.GenerateXmlDocs) { indent.AppendLine($"/// Reconstructs {IndefiniteArticleFor(className)} {className} by dispatching on the DamlVariant constructor tag."); } - indent.AppendLine($"public static {fullClassName} FromVariant({context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} variant) =>"); + indent.AppendLine($"public static {fullClassName} FromVariant({fromVariantParameters}) =>"); indent.Indent(); indent.AppendLine("variant.Constructor switch"); indent.AppendLine("{"); @@ -76,7 +83,7 @@ internal void WriteVariantType(IndentWriter indent, DamlDataType dataType, DamlV var ctorName = VariantConstructorName(ctor.Name, className); if (HasVariantPayload(ctor)) { - indent.AppendLine($"\"{ctor.Name}\" => new {ctorName}({mapper.FromValue(ctor.ArgumentType!, "variant.Value")}),"); + indent.AppendLine($"\"{ctor.Name}\" => new {ctorName}({mapper.FromValue(ctor.ArgumentType!, "variant.Value", delegates)}),"); } else { @@ -123,13 +130,13 @@ internal void WriteVariantType(IndentWriter indent, DamlDataType dataType, DamlV indent.AppendLine($"public override string Tag => \"{ctor.Name}\";"); indent.AppendLine(); var payload = hasArg - ? mapper.ToValue(ctor.ArgumentType!, "Value") + ? mapper.ToValue(ctor.ArgumentType!, "Value", delegates) : $"{context.Qualifier.Qualify(RuntimeTypeNames.DamlUnit, context.RootNamespace)}.Instance"; if (options.GenerateXmlDocs) { indent.AppendLine("/// "); } - indent.AppendLine($"public override {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} ToVariant() => {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)}.Create(\"{ctor.Name}\", {payload});"); + indent.AppendLine($"public override {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)} ToVariant({toVariantParameters}) => {context.Qualifier.Qualify(RuntimeTypeNames.DamlVariant, context.RootNamespace)}.Create(\"{ctor.Name}\", {payload});"); indent.Dedent(); indent.AppendLine("}"); @@ -140,6 +147,20 @@ internal void WriteVariantType(IndentWriter indent, DamlDataType dataType, DamlV indent.AppendLine("}"); } + /// + /// Returns the variant's interface clause: : IDamlVariant for non-generic variants, + /// or empty for generic variants, whose ToVariant takes one converter delegate per + /// type parameter and so cannot satisfy the parameterless IDamlVariant.ToVariant() + /// contract — matching the hand-written stdlib generic Either. + /// + private string InterfaceDeclaration(IReadOnlyList typeParams) => + typeParams.Count == 0 + ? $" : {context.Qualifier.Qualify(RuntimeTypeNames.IDamlVariant, context.RootNamespace)}" + : string.Empty; + + private static string Prefixed(string parameters) => + string.IsNullOrEmpty(parameters) ? string.Empty : $", {parameters}"; + private static bool HasVariantPayload(DamlVariantConstructor ctor) => ctor.ArgumentType is not null && ctor.ArgumentType is not DamlPrimitiveType { Primitive: DamlPrimitive.Unit }; diff --git a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Asset.cs b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Asset.cs index 2a69b37..6eeea46 100644 --- a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Asset.cs +++ b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Asset.cs @@ -20,13 +20,13 @@ namespace Daml.Codegen.Testing.Conformance.Richtypes; /// /// Generated from Daml template RichTypes:Asset /// -public sealed partial record Asset([property: DamlFieldAttribute("issuer")] Party Issuer, [property: DamlFieldAttribute("amount")] decimal Amount) : ITemplate +public sealed partial record Asset([property: DamlFieldAttribute("issuer")] Party Issuer, [property: DamlFieldAttribute("amount")] decimal Amount) : ITemplate, IImplements { /// Gets the template identifier. - public static Identifier TemplateId { get; } = new("22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491", "RichTypes", "Asset"); + public static Identifier TemplateId { get; } = new("8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef", "RichTypes", "Asset"); /// Gets the package ID. - public static string PackageId => "22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491"; + public static string PackageId => "8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef"; /// Gets the package name. public static string PackageName => "richtypes"; @@ -57,7 +57,7 @@ public DamlRecord ToRecord() => DamlRecord.Create( { Name = new ChoiceName("Archive"), Consuming = true, - ArgumentEncoder = _ => DamlUnit.Instance, + ArgumentEncoder = _ => DamlRecord.Create(), ResultDecoder = _ => DamlUnit.Instance }; @@ -112,3 +112,86 @@ public static Task>> CreateAsync( return client.TryCreateAsync(payload, submitter, cancellationToken: cancellationToken); } } + +/// +/// Async exerciser extensions for contract IDs whose choices +/// return a non-contract-id payload (Decimal, records, lists, Unit, etc.). +/// Each method submits the choice via +/// ILedgerWriter.TrySubmitAndWaitForTransactionAsync and lifts the typed result +/// into ExerciseOutcome<TReturn>. +/// +public static class AssetNonContractExtensions +{ + /// + /// Builds the for the Archive choice on this contract id. + /// + /// The contract on which to exercise the choice. + public static ExerciseCommand ArchiveCommand( + this ContractId contractId) + { + ArgumentNullException.ThrowIfNull(contractId); + return new ExerciseCommand( + Asset.TemplateId, + contractId, + new ChoiceName("Archive"), + DamlRecord.Create()); + } + + /// + /// Exercises the Archive choice and lifts the choice's exercise result to + /// over Unit. Structured Canton/Daml errors + /// and infrastructure/transport errors pass through unchanged. + /// + /// The contract on which to exercise the choice. + /// The ledger client. + /// The party submitting the command. + /// Optional workflow id; passed through to the ledger when supplied. No default — workflow IDs are correlation keys, and a per-choice default would bucket every submission of the same choice under one ID. + /// Optional command id for deduplication; a fresh id is minted only when omitted. Pass the same id across a retry of a lost-but-accepted submission so the ledger deduplicates the resubmission instead of re-executing the choice. + /// Optional per-call deadline, enforced server-side; the default null applies no deadline. An overrun surfaces as an InfraError outcome. + /// Cancellation token. + public static async Task> ArchiveAsync( + this ContractId contractId, + ILedgerWriter client, + Party actAs, + string? workflowId = null, + CommandId? commandId = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(client); + + var command = contractId.ArchiveCommand(); + + var submission = CommandsSubmission.Single(command) + .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); + if (!string.IsNullOrEmpty(workflowId)) + { + submission = submission.WithWorkflowId(new WorkflowId(workflowId)); + } + + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + + return outcome.ProjectCommitted(tx => ProjectArchiveResult(tx, contractId.Value)); + } + + private static ExerciseOutcome ProjectArchiveResult(TransactionResult tx, string contractId) + { + foreach (var exercised in tx.ExercisedEvents) + { + if (string.Equals(exercised.ContractId, contractId, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.ModuleName, Asset.TemplateId.ModuleName, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.EntityName, Asset.TemplateId.EntityName, StringComparison.Ordinal) + && string.Equals(exercised.ChoiceName, "Archive", StringComparison.Ordinal)) + { + return new ExerciseOutcome.One(Unit.Value); + } + } + + throw new InvalidOperationException( + $"Submission succeeded but no 'Archive' exercise on contract '{contractId}' was recorded on transaction {tx.UpdateId}. " + + "This is most often caused by the ILedgerWriter implementation not populating TransactionResult.ExercisedEvents — " + + "your ILedgerWriter implementation must project the transaction's exercised events into TransactionResult.ExercisedEvents. " + + "If your implementation does populate ExercisedEvents, ensure the participant is configured to return " + + "LedgerEffects with verbose events so the exercise event survives projection."); + } +} diff --git a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Holding.cs b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Holding.cs index 87aa5eb..3acd647 100644 --- a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Holding.cs +++ b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Holding.cs @@ -17,27 +17,40 @@ namespace Daml.Codegen.Testing.Conformance.Richtypes; /// satisfies its where T : ITemplate constraint, but every static /// metadata accessor throws — interface placeholders carry no template identity. /// +/// +/// Interface-instance choices need a concrete template to compile against, but the Daml +/// interface RichTypes:Holding has none, so every static metadata accessor on this +/// placeholder throws by design. This does not affect interface choices, which the generated +/// extension methods exercise directly on ContractId<Holding> — no coercion +/// needed. There is no supported way to recover a concrete ContractId<TConcrete> +/// from this placeholder; obtain one independently if concrete metadata is required. +/// public sealed record Holding : ITemplate { /// Always throws — the RichTypes:Holding interface placeholder carries no template identity. + /// Thrown unconditionally — this placeholder carries no template identity to report. public static Identifier TemplateId => - throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Coerce ContractId to a typed ContractId before reading template metadata or exercising commands."); + throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Exercise interface choices directly on ContractId; there is no supported coercion to a concrete ContractId."); /// Always throws — the RichTypes:Holding interface placeholder carries no package identity. + /// Thrown unconditionally — this placeholder carries no package identity to report. public static string PackageId => - throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Coerce ContractId to a typed ContractId before reading template metadata or exercising commands."); + throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Exercise interface choices directly on ContractId; there is no supported coercion to a concrete ContractId."); /// Always throws — the RichTypes:Holding interface placeholder carries no package identity. + /// Thrown unconditionally — this placeholder carries no package identity to report. public static string PackageName => - throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Coerce ContractId to a typed ContractId before reading template metadata or exercising commands."); + throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Exercise interface choices directly on ContractId; there is no supported coercion to a concrete ContractId."); /// Always throws — the RichTypes:Holding interface placeholder carries no package identity. + /// Thrown unconditionally — this placeholder carries no package identity to report. public static Version PackageVersion => - throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Coerce ContractId to a typed ContractId before reading template metadata or exercising commands."); + throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Exercise interface choices directly on ContractId; there is no supported coercion to a concrete ContractId."); /// Always throws — the RichTypes:Holding interface placeholder carries no Daml type identity. + /// Thrown unconditionally — this placeholder carries no Daml type identity to report. public static DamlTypeDescriptor DamlTypeId => - throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Coerce ContractId to a typed ContractId before reading template metadata or exercising commands."); + throw new InvalidOperationException("'Holding' is the C# placeholder for the Daml interface 'RichTypes:Holding' and carries no template metadata. Exercise interface choices directly on ContractId; there is no supported coercion to a concrete ContractId."); /// Converts this placeholder to an empty DamlRecord. public DamlRecord ToRecord() => DamlRecord.Create(); diff --git a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/IHolding.cs b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/IHolding.cs index 156e84a..b3f5967 100644 --- a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/IHolding.cs +++ b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/IHolding.cs @@ -25,10 +25,10 @@ public interface IHolding : IDamlInterface, IHasView static Identifier IDamlInterface.InterfaceId => InterfaceId; /// Gets the interface identifier. - public static new Identifier InterfaceId { get; } = new("22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491", "RichTypes", "Holding"); + public static new Identifier InterfaceId { get; } = new("8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef", "RichTypes", "Holding"); /// Gets the package ID. - static string IDamlInterface.PackageId => "22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491"; + static string IDamlInterface.PackageId => "8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef"; /// Gets the package name. static string IDamlInterface.PackageName => "richtypes"; @@ -37,7 +37,7 @@ public interface IHolding : IDamlInterface, IHasView static Version IDamlInterface.PackageVersion => new(0, 0, 1); /// Gets the compile-time Daml type descriptor. - static DamlTypeDescriptor global::Daml.Runtime.IDamlType.DamlTypeId => new(new Identifier("22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491", "RichTypes", "Holding"), DamlTypeKind.Interface, "richtypes"); + static DamlTypeDescriptor global::Daml.Runtime.IDamlType.DamlTypeId => new(new Identifier("8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef", "RichTypes", "Holding"), DamlTypeKind.Interface, "richtypes"); // Interface method Archive. // Choice Archive() -> DamlUnit @@ -57,11 +57,24 @@ public interface IHolding : IDamlInterface, IHasView public static class IHoldingExtensions { /// - /// Exercises the Archive interface choice on this contract id. + /// Builds the interface-typed for the Archive choice on this contract id. /// The wire-level template_id slot carries the interface id — Canton's /// ledger API resolves the concrete implementing template at the participant. /// /// The interface-typed contract id to exercise on. + public static ExerciseCommand ArchiveCommand( + this ContractId contractId) + { + ArgumentNullException.ThrowIfNull(contractId); + return ExerciseCommand.ForInterface(contractId, new ChoiceName("Archive"), DamlRecord.Create()); + } + + /// + /// Exercises the Archive interface choice on this contract id, submitting the + /// resulting through + /// and awaiting the outcome. + /// + /// The interface-typed contract id to exercise on. /// The ledger client. /// The party submitting the command. /// Optional workflow id; passed through to the ledger when supplied. No default — workflow IDs are correlation keys, and a per-choice default would bucket every submission of the same choice under one ID. @@ -77,18 +90,16 @@ public static async Task> ArchiveAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contractId); ArgumentNullException.ThrowIfNull(client); - var command = ExerciseCommand.ForInterface(contractId, new ChoiceName("Archive"), DamlUnit.Instance); + var command = contractId.ArchiveCommand(); var submission = CommandsSubmission.Single(command) - .WithActAs(actAs) .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); if (!string.IsNullOrEmpty(workflowId)) { submission = submission.WithWorkflowId(new WorkflowId(workflowId)); } - return await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + return await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Marker.cs b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Marker.cs index f5ac65e..cd6d236 100644 --- a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Marker.cs +++ b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/Marker.cs @@ -23,10 +23,10 @@ namespace Daml.Codegen.Testing.Conformance.Richtypes; public sealed partial record Marker([property: DamlFieldAttribute("owner")] Party Owner) : ITemplate { /// Gets the template identifier. - public static Identifier TemplateId { get; } = new("22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491", "RichTypes", "Marker"); + public static Identifier TemplateId { get; } = new("8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef", "RichTypes", "Marker"); /// Gets the package ID. - public static string PackageId => "22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491"; + public static string PackageId => "8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef"; /// Gets the package name. public static string PackageName => "richtypes"; @@ -55,7 +55,7 @@ public DamlRecord ToRecord() => DamlRecord.Create( { Name = new ChoiceName("Archive"), Consuming = true, - ArgumentEncoder = _ => DamlUnit.Instance, + ArgumentEncoder = _ => DamlRecord.Create(), ResultDecoder = _ => DamlUnit.Instance }; @@ -110,3 +110,86 @@ public static Task>> CreateAsync( return client.TryCreateAsync(payload, submitter, cancellationToken: cancellationToken); } } + +/// +/// Async exerciser extensions for contract IDs whose choices +/// return a non-contract-id payload (Decimal, records, lists, Unit, etc.). +/// Each method submits the choice via +/// ILedgerWriter.TrySubmitAndWaitForTransactionAsync and lifts the typed result +/// into ExerciseOutcome<TReturn>. +/// +public static class MarkerNonContractExtensions +{ + /// + /// Builds the for the Archive choice on this contract id. + /// + /// The contract on which to exercise the choice. + public static ExerciseCommand ArchiveCommand( + this ContractId contractId) + { + ArgumentNullException.ThrowIfNull(contractId); + return new ExerciseCommand( + Marker.TemplateId, + contractId, + new ChoiceName("Archive"), + DamlRecord.Create()); + } + + /// + /// Exercises the Archive choice and lifts the choice's exercise result to + /// over Unit. Structured Canton/Daml errors + /// and infrastructure/transport errors pass through unchanged. + /// + /// The contract on which to exercise the choice. + /// The ledger client. + /// The party submitting the command. + /// Optional workflow id; passed through to the ledger when supplied. No default — workflow IDs are correlation keys, and a per-choice default would bucket every submission of the same choice under one ID. + /// Optional command id for deduplication; a fresh id is minted only when omitted. Pass the same id across a retry of a lost-but-accepted submission so the ledger deduplicates the resubmission instead of re-executing the choice. + /// Optional per-call deadline, enforced server-side; the default null applies no deadline. An overrun surfaces as an InfraError outcome. + /// Cancellation token. + public static async Task> ArchiveAsync( + this ContractId contractId, + ILedgerWriter client, + Party actAs, + string? workflowId = null, + CommandId? commandId = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(client); + + var command = contractId.ArchiveCommand(); + + var submission = CommandsSubmission.Single(command) + .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); + if (!string.IsNullOrEmpty(workflowId)) + { + submission = submission.WithWorkflowId(new WorkflowId(workflowId)); + } + + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + + return outcome.ProjectCommitted(tx => ProjectArchiveResult(tx, contractId.Value)); + } + + private static ExerciseOutcome ProjectArchiveResult(TransactionResult tx, string contractId) + { + foreach (var exercised in tx.ExercisedEvents) + { + if (string.Equals(exercised.ContractId, contractId, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.ModuleName, Marker.TemplateId.ModuleName, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.EntityName, Marker.TemplateId.EntityName, StringComparison.Ordinal) + && string.Equals(exercised.ChoiceName, "Archive", StringComparison.Ordinal)) + { + return new ExerciseOutcome.One(Unit.Value); + } + } + + throw new InvalidOperationException( + $"Submission succeeded but no 'Archive' exercise on contract '{contractId}' was recorded on transaction {tx.UpdateId}. " + + "This is most often caused by the ILedgerWriter implementation not populating TransactionResult.ExercisedEvents — " + + "your ILedgerWriter implementation must project the transaction's exercised events into TransactionResult.ExercisedEvents. " + + "If your implementation does populate ExercisedEvents, ensure the participant is configured to return " + + "LedgerEffects with verbose events so the exercise event survives projection."); + } +} diff --git a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/RichRecord.cs b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/RichRecord.cs index 7f2b5ee..eca1a4b 100644 --- a/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/RichRecord.cs +++ b/src/Daml.Codegen.Testing.Conformance/Generated/Richtypes/RichRecord.cs @@ -25,10 +25,10 @@ namespace Daml.Codegen.Testing.Conformance.Richtypes; public sealed partial record RichRecord([property: DamlFieldAttribute("owner")] Party Owner, [property: DamlFieldAttribute("count")] long Count, [property: DamlFieldAttribute("amount")] decimal Amount, [property: DamlFieldAttribute("label")] string Label, [property: DamlFieldAttribute("active")] bool Active, [property: DamlFieldAttribute("asOf")] DateOnly AsOf, [property: DamlFieldAttribute("observedAt")] DateTimeOffset ObservedAt, [property: DamlFieldAttribute("note")] string? Note, [property: DamlFieldAttribute("tags")] IReadOnlyList Tags, [property: DamlFieldAttribute("attributes")] IReadOnlyDictionary Attributes, [property: DamlFieldAttribute("marker")] ContractId Marker, [property: DamlFieldAttribute("holdingCid")] ContractId HoldingCid, [property: DamlFieldAttribute("holdingCids")] IReadOnlyList> HoldingCids, [property: DamlFieldAttribute("profile")] Profile Profile, [property: DamlFieldAttribute("outcome")] Outcome Outcome, [property: DamlFieldAttribute("suit")] Suit Suit, [property: DamlFieldAttribute("fee")] decimal Fee) : ITemplate { /// Gets the template identifier. - public static Identifier TemplateId { get; } = new("22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491", "RichTypes", "RichRecord"); + public static Identifier TemplateId { get; } = new("8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef", "RichTypes", "RichRecord"); /// Gets the package ID. - public static string PackageId => "22047ae2d2f5de6f0baaa0080343fe0c5d5e59507a5dfafc5c8ca141cfa40491"; + public static string PackageId => "8fe55b3b757427d13c28d7d3e39d95b3e7079dfe6ded9dd6daccec57ec7803ef"; /// Gets the package name. public static string PackageName => "richtypes"; @@ -89,7 +89,7 @@ public DamlRecord ToRecord() => DamlRecord.Create( { Name = new ChoiceName("Archive"), Consuming = true, - ArgumentEncoder = _ => DamlUnit.Instance, + ArgumentEncoder = _ => DamlRecord.Create(), ResultDecoder = _ => DamlUnit.Instance }; @@ -186,6 +186,24 @@ public static ExerciseOutcome FromCreatedContracts(IEnumerable public static class RichRecordExtensions { + /// + /// Builds the for the Relabel choice on this contract id. + /// + /// The contract on which to exercise the choice. + /// The choice argument. + public static ExerciseCommand RelabelCommand( + this ContractId contractId, + RichRecord.Relabel argument) + { + ArgumentNullException.ThrowIfNull(contractId); + ArgumentNullException.ThrowIfNull(argument); + return new ExerciseCommand( + RichRecord.TemplateId, + contractId, + new ChoiceName("Relabel"), + argument.ToRecord()); + } + /// /// Exercises the Relabel choice and projects the resulting transaction's created contracts to a typed . /// One Party parameter is emitted per Daml controller (declaration order). @@ -210,27 +228,20 @@ public static async Task> RelabelAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contractId); ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(argument); SubmitterInfo submitter = owner; - var command = new ExerciseCommand( - RichRecord.TemplateId, - contractId, - new ChoiceName("Relabel"), - argument.ToRecord()); + var command = contractId.RelabelCommand(argument); var submission = CommandsSubmission.Single(command) - .WithSubmitter(submitter) .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); if (!string.IsNullOrEmpty(workflowId)) { submission = submission.WithWorkflowId(new WorkflowId(workflowId)); } - var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); return outcome.ProjectCommitted(tx => RelabelResult.FromCreatedContracts(tx.CreatedContracts)); } @@ -259,25 +270,18 @@ public static async Task> RelabelAsync( TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(contractId); ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(argument); - var command = new ExerciseCommand( - RichRecord.TemplateId, - contractId, - new ChoiceName("Relabel"), - argument.ToRecord()); + var command = contractId.RelabelCommand(argument); var submission = CommandsSubmission.Single(command) - .WithSubmitter(submitter) .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); if (!string.IsNullOrEmpty(workflowId)) { submission = submission.WithWorkflowId(new WorkflowId(workflowId)); } - var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); return outcome.ProjectCommitted(tx => RelabelResult.FromCreatedContracts(tx.CreatedContracts)); } @@ -354,3 +358,86 @@ public static Task>> CreateAsync( return client.TryCreateAsync(payload, submitter, cancellationToken: cancellationToken); } } + +/// +/// Async exerciser extensions for contract IDs whose choices +/// return a non-contract-id payload (Decimal, records, lists, Unit, etc.). +/// Each method submits the choice via +/// ILedgerWriter.TrySubmitAndWaitForTransactionAsync and lifts the typed result +/// into ExerciseOutcome<TReturn>. +/// +public static class RichRecordNonContractExtensions +{ + /// + /// Builds the for the Archive choice on this contract id. + /// + /// The contract on which to exercise the choice. + public static ExerciseCommand ArchiveCommand( + this ContractId contractId) + { + ArgumentNullException.ThrowIfNull(contractId); + return new ExerciseCommand( + RichRecord.TemplateId, + contractId, + new ChoiceName("Archive"), + DamlRecord.Create()); + } + + /// + /// Exercises the Archive choice and lifts the choice's exercise result to + /// over Unit. Structured Canton/Daml errors + /// and infrastructure/transport errors pass through unchanged. + /// + /// The contract on which to exercise the choice. + /// The ledger client. + /// The party submitting the command. + /// Optional workflow id; passed through to the ledger when supplied. No default — workflow IDs are correlation keys, and a per-choice default would bucket every submission of the same choice under one ID. + /// Optional command id for deduplication; a fresh id is minted only when omitted. Pass the same id across a retry of a lost-but-accepted submission so the ledger deduplicates the resubmission instead of re-executing the choice. + /// Optional per-call deadline, enforced server-side; the default null applies no deadline. An overrun surfaces as an InfraError outcome. + /// Cancellation token. + public static async Task> ArchiveAsync( + this ContractId contractId, + ILedgerWriter client, + Party actAs, + string? workflowId = null, + CommandId? commandId = null, + TimeSpan? timeout = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(client); + + var command = contractId.ArchiveCommand(); + + var submission = CommandsSubmission.Single(command) + .WithCommandId(commandId ?? new CommandId(Guid.NewGuid().ToString())); + if (!string.IsNullOrEmpty(workflowId)) + { + submission = submission.WithWorkflowId(new WorkflowId(workflowId)); + } + + var outcome = await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken).ConfigureAwait(false); + + return outcome.ProjectCommitted(tx => ProjectArchiveResult(tx, contractId.Value)); + } + + private static ExerciseOutcome ProjectArchiveResult(TransactionResult tx, string contractId) + { + foreach (var exercised in tx.ExercisedEvents) + { + if (string.Equals(exercised.ContractId, contractId, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.ModuleName, RichRecord.TemplateId.ModuleName, StringComparison.Ordinal) + && string.Equals(exercised.TemplateId.EntityName, RichRecord.TemplateId.EntityName, StringComparison.Ordinal) + && string.Equals(exercised.ChoiceName, "Archive", StringComparison.Ordinal)) + { + return new ExerciseOutcome.One(Unit.Value); + } + } + + throw new InvalidOperationException( + $"Submission succeeded but no 'Archive' exercise on contract '{contractId}' was recorded on transaction {tx.UpdateId}. " + + "This is most often caused by the ILedgerWriter implementation not populating TransactionResult.ExercisedEvents — " + + "your ILedgerWriter implementation must project the transaction's exercised events into TransactionResult.ExercisedEvents. " + + "If your implementation does populate ExercisedEvents, ensure the participant is configured to return " + + "LedgerEffects with verbose events so the exercise event survives projection."); + } +} diff --git a/src/Daml.Ledger.Abstractions.Testing.Conformance/LedgerClientConformanceTests.cs b/src/Daml.Ledger.Abstractions.Testing.Conformance/LedgerClientConformanceTests.cs index 209b6ce..55b1313 100644 --- a/src/Daml.Ledger.Abstractions.Testing.Conformance/LedgerClientConformanceTests.cs +++ b/src/Daml.Ledger.Abstractions.Testing.Conformance/LedgerClientConformanceTests.cs @@ -9,6 +9,8 @@ using AwesomeAssertions; using Daml.Runtime; using Daml.Runtime.Commands; +using Daml.Runtime.Contracts; +using Daml.Runtime.Outcomes; using Daml.Runtime.Streams; using Xunit; @@ -19,8 +21,9 @@ namespace Daml.Ledger.Abstractions.Testing.Conformance; /// Adopters subclass with a concrete client factory and a probe Daml marker; the seeded /// client must expose the canonical scenario: at least one active contract, one /// unclassifiable row, a terminal checkpoint, at least one event on both the ACS-delta -/// subscription (, which surfaces archival -/// as a first-class and never an +/// subscription +/// (, +/// which surfaces archival as a first-class and never an /// ) and the ledger-effects subscription /// (, which signals archival /// with a consuming and never an @@ -63,6 +66,15 @@ public abstract class LedgerClientConformanceTests /// protected virtual ILedgerClient? CreateFaultingSnapshotClient() => null; + /// + /// A write-capable client and submission proving the submitter-authority contract of + /// / , + /// or null if the adopter cannot seed a deterministic authorization boundary + /// (e.g. a fake with no notion of authorized/unauthorized parties). Defaults to + /// null, which skips the write-path checks below. + /// + protected virtual WriteConformanceFixture? CreateWriteFixture() => null; + /// A cancelled live subscription surfaces cancellation, not an in-band error. [Fact] public async Task Cancelling_a_live_subscription_throws_OperationCanceledException() @@ -233,6 +245,110 @@ public async Task Acs_delta_subscription_never_yields_Exercised() "the ACS-delta shape surfaces archival as a first-class Archived event, never an Exercised variant"); } + /// + /// must apply the + /// submitter parameter authoritatively via CommandsSubmission.WithSubmitter, + /// overwriting any already set on the submission — + /// not dispatch whatever the caller pre-set. Opt-in: skipped unless the adopter overrides + /// . + /// + [Fact] + public async Task TrySubmitAndWaitForTransactionAsync_submitter_parameter_overrides_pre_set_ActAs() + { + var maybeFixture = CreateWriteFixture(); + Assert.SkipWhen(maybeFixture is null, WriteFixtureSkipReason); + await using var fixture = maybeFixture!; + + var submissionWithWrongActAs = fixture.Submission.WithActAs(fixture.Unauthorized); + + var outcome = await fixture.Client.TrySubmitAndWaitForTransactionAsync(submissionWithWrongActAs, fixture.Authorized); + + outcome.Should().BeOfType.One>( + "the submitter parameter must win over the pre-set (unauthorized) ActAs; an implementation " + + "that dispatches the pre-set ActAs instead of applying the submitter authoritatively would be " + + "rejected by the seeded client as unauthorized"); + } + + /// + /// must apply the submitter parameter + /// authoritatively via CommandsSubmission.WithSubmitter, overwriting any + /// already set on the submission — not dispatch + /// whatever the caller pre-set. Opt-in: skipped unless the adopter overrides + /// . + /// + [Fact] + public async Task SubmitAndWaitAsync_submitter_parameter_overrides_pre_set_ActAs() + { + var maybeFixture = CreateWriteFixture(); + Assert.SkipWhen(maybeFixture is null, WriteFixtureSkipReason); + await using var fixture = maybeFixture!; + + var submissionWithWrongActAs = fixture.Submission.WithActAs(fixture.Unauthorized); + + var act = () => fixture.Client.SubmitAndWaitAsync(submissionWithWrongActAs, fixture.Authorized); + + await act.Should().NotThrowAsync( + "the submitter parameter must win over the pre-set (unauthorized) ActAs; an implementation " + + "that dispatches the pre-set ActAs instead of applying the submitter authoritatively would be " + + "rejected by the seeded client as unauthorized"); + } + + /// + /// must not merge the + /// submitter parameter with already set on + /// the submission — an authorized pre-set ActAs must not leak through and rescue an + /// unauthorized submitter. Opt-in: skipped unless the adopter overrides + /// . Uses its own fresh + /// call (a distinct seeded client/contract from the sibling override check) so a stateful + /// adopter's already-consumed contract from that check cannot masquerade as this one's + /// authorization failure. + /// + [Fact] + public async Task TrySubmitAndWaitForTransactionAsync_submitter_parameter_is_not_merged_with_pre_set_ActAs() + { + var maybeFixture = CreateWriteFixture(); + Assert.SkipWhen(maybeFixture is null, WriteFixtureSkipReason); + await using var fixture = maybeFixture!; + + var submissionWithAuthorizedActAs = fixture.Submission.WithActAs(fixture.Authorized); + + var outcome = await fixture.Client.TrySubmitAndWaitForTransactionAsync(submissionWithAuthorizedActAs, fixture.Unauthorized); + + outcome.Should().NotBeOfType.One>( + "the submitter parameter must replace, not merge with, the pre-set ActAs; an implementation " + + "that unions the submitter into the existing ActAs instead of overwriting it would let the " + + "authorized pre-set ActAs rescue an unauthorized submitter"); + } + + /// + /// must not merge the submitter + /// parameter with already set on the submission — an + /// authorized pre-set ActAs must not leak through and rescue an unauthorized + /// submitter. Opt-in: skipped unless the adopter overrides . + /// Uses its own fresh call (a distinct seeded client/contract + /// from the sibling override check) so a stateful adopter's already-consumed contract from + /// that check cannot masquerade as this one's authorization failure. + /// + [Fact] + public async Task SubmitAndWaitAsync_submitter_parameter_is_not_merged_with_pre_set_ActAs() + { + var maybeFixture = CreateWriteFixture(); + Assert.SkipWhen(maybeFixture is null, WriteFixtureSkipReason); + await using var fixture = maybeFixture!; + + var submissionWithAuthorizedActAs = fixture.Submission.WithActAs(fixture.Authorized); + + var act = () => fixture.Client.SubmitAndWaitAsync(submissionWithAuthorizedActAs, fixture.Unauthorized); + + await act.Should().ThrowAsync( + "the submitter parameter must replace, not merge with, the pre-set ActAs; an implementation " + + "that unions the submitter into the existing ActAs instead of overwriting it would let the " + + "authorized pre-set ActAs rescue an unauthorized submitter"); + } + + private const string WriteFixtureSkipReason = + "adopter opted out of the submitter-authority check: CreateWriteFixture() returned null"; + private Task>> CollectSnapshot( ILedgerClient client, LedgerOffset? activeAtOffset = null) => CollectWithinBudget( diff --git a/src/Daml.Ledger.Abstractions.Testing.Conformance/README.md b/src/Daml.Ledger.Abstractions.Testing.Conformance/README.md index d7e7972..bba544f 100644 --- a/src/Daml.Ledger.Abstractions.Testing.Conformance/README.md +++ b/src/Daml.Ledger.Abstractions.Testing.Conformance/README.md @@ -42,6 +42,13 @@ public class MyClientConformanceTests : LedgerClientConformanceTests.StreamError` and no `Checkpoint`). Leaving it at its `null` default skips only the fault-surfacing check. +To also cover submitter authority, override `CreateWriteFixture()` to return a +`WriteConformanceFixture`: a fresh client plus a submission it accepts from an +`Authorized` party and rejects from an `Unauthorized` one. Leaving it at its `null` +default skips only the submitter-authority checks. + Not for production use. diff --git a/src/Daml.Ledger.Abstractions.Testing.Conformance/WriteConformanceFixture.cs b/src/Daml.Ledger.Abstractions.Testing.Conformance/WriteConformanceFixture.cs new file mode 100644 index 0000000..dc73923 --- /dev/null +++ b/src/Daml.Ledger.Abstractions.Testing.Conformance/WriteConformanceFixture.cs @@ -0,0 +1,44 @@ +// Copyright 2026 Peaceful Studio OÜ +// SPDX-License-Identifier: Apache-2.0 + +using System.Threading.Tasks; +using Daml.Runtime.Commands; +using Daml.Runtime.Data; + +namespace Daml.Ledger.Abstractions.Testing.Conformance; + +/// +/// A write-capable client and a submission it accepts, used to prove that an +/// / +/// implementation applies the submitter parameter authoritatively — via +/// , overwriting any +/// already set on the submission — instead of +/// dispatching whatever the caller pre-set. +/// +/// +/// A fresh client, seeded so that succeeds when dispatched +/// with as the submitter and is rejected when dispatched +/// with instead. +/// +/// +/// A submission the seeded accepts from +/// and rejects from . The conformance checks set its +/// to before +/// dispatching with submitter , to prove the submitter +/// parameter wins over whatever was pre-set. +/// +/// The party the seeded client accepts as the acting party. +/// +/// A party distinct from that the seeded client rejects. +/// +public sealed record WriteConformanceFixture( + ILedgerClient Client, + CommandsSubmission Submission, + Party Authorized, + Party Unauthorized) : IAsyncDisposable +{ + /// Delegates to today; gives an adopter that seeds + /// supplementary resources alongside the client (e.g. a pre-created contract handle) + /// a place to add their cleanup later. + public ValueTask DisposeAsync() => Client.DisposeAsync(); +} diff --git a/src/Daml.Ledger.Abstractions/Extensions/CreateByExercise.cs b/src/Daml.Ledger.Abstractions/Extensions/CreateByExercise.cs index 876b7b9..dac8bc4 100644 --- a/src/Daml.Ledger.Abstractions/Extensions/CreateByExercise.cs +++ b/src/Daml.Ledger.Abstractions/Extensions/CreateByExercise.cs @@ -46,21 +46,13 @@ public static async Task>> TryCreateOneByE where TTemplate : IDamlType { ArgumentNullException.ThrowIfNull(writer); - var submission = CommandsSubmission.Single(choice).WithSubmitter(submitter).WithOptionalWorkflowId(workflowId); + var submission = CommandsSubmission.Single(choice).WithOptionalWorkflowId(workflowId); var outcome = await writer - .TrySubmitAndWaitForTransactionAsync(submission, timeout, cancellationToken) + .TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout, cancellationToken) .ConfigureAwait(false); - return outcome switch - { - ExerciseOutcome.One one => ProjectSingleCreated(one.Result), - ExerciseOutcome.None => new ExerciseOutcome>.None(), - ExerciseOutcome.Many many => new ExerciseOutcome>.Many(many.Count, many.ContractIds), - ExerciseOutcome.DamlError e => new ExerciseOutcome>.DamlError(e.Category, e.ErrorId, e.Message, e.Metadata), - ExerciseOutcome.InfraError e => new ExerciseOutcome>.InfraError(e.StatusCode, e.Message, e.SourceException), - _ => throw new UnreachableException($"Unexpected outcome {outcome.GetType().Name} from TrySubmitAndWaitForTransactionAsync."), - }; + return RemapExerciseOutcome(outcome, ProjectSingleCreated); } /// @@ -103,21 +95,15 @@ public static async Task>>> where TTemplate : IDamlType { ArgumentNullException.ThrowIfNull(writer); - var submission = CommandsSubmission.Single(choice).WithSubmitter(submitter).WithOptionalWorkflowId(workflowId); + var submission = CommandsSubmission.Single(choice).WithOptionalWorkflowId(workflowId); var outcome = await writer - .TrySubmitAndWaitForTransactionAsync(submission, timeout, cancellationToken) + .TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout, cancellationToken) .ConfigureAwait(false); - return outcome switch - { - ExerciseOutcome.One one => new ExerciseOutcome>>.One(one.Result.All()), - ExerciseOutcome.None => new ExerciseOutcome>>.None(), - ExerciseOutcome.Many many => new ExerciseOutcome>>.Many(many.Count, many.ContractIds), - ExerciseOutcome.DamlError e => new ExerciseOutcome>>.DamlError(e.Category, e.ErrorId, e.Message, e.Metadata), - ExerciseOutcome.InfraError e => new ExerciseOutcome>>.InfraError(e.StatusCode, e.Message, e.SourceException), - _ => throw new UnreachableException($"Unexpected outcome {outcome.GetType().Name} from TrySubmitAndWaitForTransactionAsync."), - }; + return RemapExerciseOutcome( + outcome, + result => new ExerciseOutcome>>.One(result.All())); } /// @@ -248,6 +234,21 @@ public static Task>> CreateManyByExerciseAsy return writer.CreateManyByExerciseAsync(choice, submitter, workflowId, timeout, cancellationToken); } + private static ExerciseOutcome RemapExerciseOutcome( + ExerciseOutcome outcome, + Func> projectCreated) + { + return outcome switch + { + ExerciseOutcome.One one => projectCreated(one.Result), + ExerciseOutcome.None => new ExerciseOutcome.None(), + ExerciseOutcome.Many many => new ExerciseOutcome.Many(many.Count, many.ContractIds), + ExerciseOutcome.DamlError e => new ExerciseOutcome.DamlError(e.Category, e.ErrorId, e.Message, e.Metadata), + ExerciseOutcome.InfraError e => new ExerciseOutcome.InfraError(e.StatusCode, e.Message, e.SourceException), + _ => throw new UnreachableException($"Unexpected outcome {outcome.GetType().Name} from TrySubmitAndWaitForTransactionAsync."), + }; + } + private static ExerciseOutcome> ProjectSingleCreated(TransactionResult result) where TTemplate : IDamlType { diff --git a/src/Daml.Ledger.Abstractions/Extensions/PartyOverloads.cs b/src/Daml.Ledger.Abstractions/Extensions/PartyOverloads.cs index ecd2231..96ff0f1 100644 --- a/src/Daml.Ledger.Abstractions/Extensions/PartyOverloads.cs +++ b/src/Daml.Ledger.Abstractions/Extensions/PartyOverloads.cs @@ -60,6 +60,24 @@ public static IAsyncEnumerable> SubscribeAsync( return streamer.SubscribeAsync(submitter, fromOffset, toOffset, cancellationToken); } + /// + /// Subscribes to contract events acting as a single party, resuming a + /// ticket from a prior snapshot's + /// . + /// + public static IAsyncEnumerable> SubscribeAsync( + this ILedgerStreamer streamer, + Party actAs, + StakeholderResume resumeFrom, + LedgerOffset? toOffset = null, + CancellationToken cancellationToken = default) + where T : IDamlType + { + ArgumentNullException.ThrowIfNull(streamer); + SubmitterInfo submitter = actAs; + return streamer.SubscribeAsync(submitter, resumeFrom, toOffset, cancellationToken); + } + /// Subscribes to the ACS snapshot acting as a single party. public static IAsyncEnumerable> SubscribeActiveAsync( this ILedgerStreamer streamer, @@ -72,4 +90,18 @@ public static IAsyncEnumerable> SubscribeActiveAsync( SubmitterInfo submitter = actAs; return streamer.SubscribeActiveAsync(submitter, activeAtOffset, cancellationToken); } + + /// Subscribes to ledger-effects events acting as a single party. + public static IAsyncEnumerable> SubscribeLedgerEffectsAsync( + this ILedgerStreamer streamer, + Party actAs, + LedgerOffset? fromOffset = null, + LedgerOffset? toOffset = null, + CancellationToken cancellationToken = default) + where T : IDamlType + { + ArgumentNullException.ThrowIfNull(streamer); + SubmitterInfo submitter = actAs; + return streamer.SubscribeLedgerEffectsAsync(submitter, fromOffset, toOffset, cancellationToken); + } } diff --git a/src/Daml.Ledger.Abstractions/Extensions/ThrowingExercise.cs b/src/Daml.Ledger.Abstractions/Extensions/ThrowingExercise.cs index d4244a2..69b46eb 100644 --- a/src/Daml.Ledger.Abstractions/Extensions/ThrowingExercise.cs +++ b/src/Daml.Ledger.Abstractions/Extensions/ThrowingExercise.cs @@ -68,9 +68,9 @@ public static async Task ExerciseAsync( CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(writer); - var submission = CommandsSubmission.Single(command, actAs).WithOptionalWorkflowId(workflowId); + var submission = CommandsSubmission.Single(command).WithOptionalWorkflowId(workflowId); var outcome = await writer - .TrySubmitAndWaitForTransactionAsync(submission, cancellationToken: cancellationToken) + .TrySubmitAndWaitForTransactionAsync(submission, actAs, cancellationToken: cancellationToken) .ConfigureAwait(false); outcome.ThrowIfError(cancellationToken); } @@ -90,9 +90,9 @@ public static async Task ExerciseAsync( CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(writer); - var submission = CommandsSubmission.Single(command).WithSubmitter(submitter).WithOptionalWorkflowId(workflowId); + var submission = CommandsSubmission.Single(command).WithOptionalWorkflowId(workflowId); var outcome = await writer - .TrySubmitAndWaitForTransactionAsync(submission, cancellationToken: cancellationToken) + .TrySubmitAndWaitForTransactionAsync(submission, submitter, cancellationToken: cancellationToken) .ConfigureAwait(false); outcome.ThrowIfError(cancellationToken); } diff --git a/src/Daml.Ledger.Abstractions/ILedgerStreamer.cs b/src/Daml.Ledger.Abstractions/ILedgerStreamer.cs index 8ab7566..fc04912 100644 --- a/src/Daml.Ledger.Abstractions/ILedgerStreamer.cs +++ b/src/Daml.Ledger.Abstractions/ILedgerStreamer.cs @@ -49,6 +49,8 @@ public interface ILedgerStreamer /// from a previously returned offset — a persisted /// offset or a completion /// offset — never re-delivers the event already seen at that offset. + /// See Canton + /// Ledger API — update stream offsets (begin exclusive, end inclusive). /// /// /// Inclusive, terminal upper bound: the event at is @@ -63,19 +65,53 @@ IAsyncEnumerable> SubscribeAsync( CancellationToken cancellationToken = default) where T : IDamlType; + /// + /// Resumes the stakeholder-based live stream from a + /// snapshot's terminal checkpoint — the typed counterpart of + /// + /// that guards the snapshot↔stream pairing at compile time. + /// + /// + /// Default implementation forwards to + /// + /// via ; implementations may override for a more + /// direct path. + /// + /// A Daml template or interface marker. + /// The submitter authorization whose combined parties scope visibility. + /// The resume ticket from a + /// snapshot's terminal . + /// + /// Inclusive, terminal upper bound: the event at is + /// delivered and then the bounded stream completes. null follows the live + /// stream, which does not complete on its own. + /// + /// Cancels the underlying stream cleanly. + IAsyncEnumerable> SubscribeAsync( + SubmitterInfo submitter, + StakeholderResume resumeFrom, + LedgerOffset? toOffset = null, + CancellationToken cancellationToken = default) + where T : IDamlType => + SubscribeAsync(submitter, resumeFrom.Offset, toOffset, cancellationToken); + /// Streams the active-contract-set snapshot for . /// /// A successful snapshot stream terminates with a single terminal /// carrying the snapshot's - /// effective offset — emitted even when the snapshot is empty. Resume a live - /// from that offset for a gapless, - /// duplicate-free handover: because treats its - /// lower bound as exclusive, the event at the snapshot offset is not delivered - /// twice across the handover. A mid-snapshot transport fault surfaces in-band - /// as a terminal in place of that - /// , so a caller draining the + /// effective offset as a ticket — emitted even + /// when the snapshot is empty. Resume a live + /// + /// from that ticket for a gapless, duplicate-free handover: because that overload + /// treats its lower bound as exclusive, the event at the snapshot offset is not + /// delivered twice across the handover. A mid-snapshot transport fault surfaces + /// in-band as a terminal in place of + /// that , so a caller draining the /// snapshot handles faults as values rather than exceptions — the same fault - /// contract as . + /// contract as . + /// See Canton + /// Ledger API — active contracts snapshot and its active-at offset for the snapshot→stream + /// handover this mirrors. /// /// A Daml template or interface marker. /// The submitter authorization whose combined parties scope visibility. @@ -105,18 +141,18 @@ IAsyncEnumerable> SubscribeActiveAsync( /// , and /// . It never emits /// ; that variant appears only on - /// the ACS-delta shape exposed by . A consumer - /// maintaining a contract cache must therefore evict on the consuming + /// the ACS-delta shape exposed by + /// . + /// A consumer maintaining a contract cache must therefore evict on the consuming /// , not on /// — waiting on the latter here /// leaves archived contracts cached forever. /// /// This shape matches on witnesses, whereas - /// matches on stakeholders, so the two reconstruct different contract sets. Do - /// not hand a snapshot over to this stream - /// for a snapshot-then-resume cache rebuild — that pairing is unsound; use - /// , whose ACS-delta shape shares the snapshot's - /// stakeholder visibility, for the snapshot-resume handover. + /// matches on stakeholders, so a snapshot's ticket + /// only resumes + /// — there is no overload of this method accepting it; use + /// if a cross-basis resume is deliberate. /// /// For an event boundary (e.g. the target contract's consuming /// event), break out of the diff --git a/src/Daml.Ledger.Abstractions/ILedgerWriter.cs b/src/Daml.Ledger.Abstractions/ILedgerWriter.cs index c6a9b6a..f3e529e 100644 --- a/src/Daml.Ledger.Abstractions/ILedgerWriter.cs +++ b/src/Daml.Ledger.Abstractions/ILedgerWriter.cs @@ -57,6 +57,11 @@ Task> TryExerciseAsync( /// completion. /// /// The commands submission. + /// + /// The submitter authorization (act-as parties and optional read-as parties). Applied via + /// before dispatch, overwriting any + /// act-as/read-as parties already set on . + /// /// /// Optional per-call deadline, applied best-effort by the transport — see /// @@ -81,6 +86,7 @@ Task> TryExerciseAsync( /// Task SubmitAndWaitAsync( CommandsSubmission submission, + SubmitterInfo submitter, TimeSpan? timeout = null, CancellationToken cancellationToken = default); @@ -94,6 +100,11 @@ Task SubmitAndWaitAsync( /// created contracts to typed values. /// /// The commands submission. + /// + /// The submitter authorization (act-as parties and optional read-as parties). Applied via + /// before dispatch, overwriting any + /// act-as/read-as parties already set on . + /// /// /// Optional per-call deadline, applied best-effort by the transport — see /// @@ -110,6 +121,7 @@ Task SubmitAndWaitAsync( /// The outcome of the submission. Task> TrySubmitAndWaitForTransactionAsync( CommandsSubmission submission, + SubmitterInfo submitter, TimeSpan? timeout = null, CancellationToken cancellationToken = default); diff --git a/src/Daml.Runtime/Commands/Choice.cs b/src/Daml.Runtime/Commands/Choice.cs index 2575b93..142517d 100644 --- a/src/Daml.Runtime/Commands/Choice.cs +++ b/src/Daml.Runtime/Commands/Choice.cs @@ -21,7 +21,7 @@ public interface IExercises where T : ITemplate /// Exercises the Archive choice (available on all templates). /// ExerciseCommand ExerciseArchive() => - ExerciseCommand.For(ContractId, new ChoiceName("Archive"), DamlUnit.Instance); + ExerciseCommand.For(ContractId, new ChoiceName("Archive"), DamlRecord.Create()); } /// diff --git a/src/Daml.Runtime/StakeholderResume.cs b/src/Daml.Runtime/StakeholderResume.cs new file mode 100644 index 0000000..29b48c1 --- /dev/null +++ b/src/Daml.Runtime/StakeholderResume.cs @@ -0,0 +1,17 @@ +// Copyright 2026 Peaceful Studio OÜ +// SPDX-License-Identifier: Apache-2.0 + +namespace Daml.Runtime; + +/// +/// A resume ticket for the stakeholder-based live stream, handed back by an active-contract-set +/// snapshot's terminal checkpoint. Only the stakeholder-based subscription accepts it — there is +/// deliberately no implicit conversion to , so passing it to a +/// witness-based subscription does not compile. The raw offset stays reachable via +/// for a deliberate cross-basis resume. +/// See Canton +/// Ledger API — ACS-delta (stakeholder) vs ledger-effects (witness) update filtering +/// for the visibility basis this ticket protects. +/// +/// The underlying ledger offset. +public readonly record struct StakeholderResume(LedgerOffset Offset); diff --git a/src/Daml.Runtime/Stdlib/GenericStub.cs b/src/Daml.Runtime/Stdlib/GenericStub.cs index 4e7e9e4..819671a 100644 --- a/src/Daml.Runtime/Stdlib/GenericStub.cs +++ b/src/Daml.Runtime/Stdlib/GenericStub.cs @@ -14,9 +14,9 @@ namespace Daml.Runtime.Stdlib; /// usable, while loudly failing if anyone actually tries to serialize a generic /// instance. /// -/// Call sites: see the DamlTypeVar arms of GetToValueConversion and -/// GetFromValueConversion in CSharpCodeGenerator. Proper -/// static-abstract dispatch for generic records is not yet implemented. +/// Call sites: the DamlTypeVar fallback arms of DamlTypeMapper.ToValue +/// and DamlTypeMapper.FromValue, reached only for a type variable outside any +/// generic record or variant body whose converter delegates would otherwise resolve it. /// /// public static class GenericStub @@ -29,8 +29,8 @@ public static class GenericStub /// /// /// Identifier shown in the exception message. Codegen call sites pass either the - /// field name (in GetToValueConversion) or the Daml type-var name (in - /// GetFromValueConversion), whichever is most useful at that site. + /// field name (in DamlTypeMapper.ToValue) or the Daml type-var name (in + /// DamlTypeMapper.FromValue), whichever is most useful at that site. /// [DoesNotReturn] public static T NotImplemented(string context) => diff --git a/src/Daml.Runtime/Streams/AcsSnapshotEntry.cs b/src/Daml.Runtime/Streams/AcsSnapshotEntry.cs index cc5a8e8..000cc90 100644 --- a/src/Daml.Runtime/Streams/AcsSnapshotEntry.cs +++ b/src/Daml.Runtime/Streams/AcsSnapshotEntry.cs @@ -50,13 +50,14 @@ public sealed record Unclassified( /// /// The single terminal marker that always ends the snapshot stream — emitted - /// even when the snapshot is empty — carrying the snapshot's effective offset. + /// even when the snapshot is empty — carrying the snapshot's effective offset + /// as a ticket. /// - /// The offset the snapshot is valid at — resume a live - /// ILedgerStreamer.SubscribeAsync from this offset for a gapless, - /// duplicate-free handover; that subscription's lower bound is exclusive, so - /// the event at this offset is not re-delivered. - public sealed record Checkpoint(LedgerOffset Offset) : AcsSnapshotEntry; + /// The resume ticket for the snapshot's effective offset — pass it to + /// ILedgerStreamer.SubscribeAsync for a gapless, duplicate-free handover; that + /// subscription's lower bound is exclusive, so the event at this offset is not re-delivered. + /// The raw offset is reachable via . + public sealed record Checkpoint(StakeholderResume Resume) : AcsSnapshotEntry; /// /// The transport stream failed mid-snapshot. Surfaced in-band rather than diff --git a/tests/Daml.Codegen.CSharp.Cli.Tests/CliExitCodeTests.cs b/tests/Daml.Codegen.CSharp.Cli.Tests/CliExitCodeTests.cs index a545f90..6975e2b 100644 --- a/tests/Daml.Codegen.CSharp.Cli.Tests/CliExitCodeTests.cs +++ b/tests/Daml.Codegen.CSharp.Cli.Tests/CliExitCodeTests.cs @@ -11,7 +11,7 @@ namespace Daml.Codegen.CSharp.Cli.Tests; public class CliExitCodeTests { [Fact] - public async Task only_unknown_args_returns_nonzero_exit_code() + public async Task Program_only_unknown_args_returns_nonzero_exit_code() { var exit = await Program.Main(["--unknown-flag"]); exit.Should().NotBe(0, @@ -19,7 +19,7 @@ public async Task only_unknown_args_returns_nonzero_exit_code() } [Fact] - public async Task no_args_returns_exit_code_one() + public async Task Program_no_args_returns_exit_code_one() { var exit = await Program.Main([]); exit.Should().Be(1, diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceAsyncExerciserTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceAsyncExerciserTests.cs index 8686310..ba90908 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceAsyncExerciserTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceAsyncExerciserTests.cs @@ -209,7 +209,7 @@ public void Generate_should_call_TrySubmitAndWaitForTransactionAsync_in_emitted_ var code = GenerateAndReadTemplate(module, "Agreement"); - code.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + code.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken)"); } [Fact] diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceCodeGenTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceCodeGenTests.cs index 8dacce5..d352782 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceCodeGenTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceCodeGenTests.cs @@ -219,7 +219,7 @@ public void Generate_should_use_DamlUnit_for_Unit_argument() } [Fact] - public void Generate_should_use_DamlUnit_for_Archive_choice() + public void Generate_should_encode_Archive_choice_argument_as_empty_record() { // Arrange - Archive is a special external type reference var module = new DamlModule @@ -265,9 +265,9 @@ public void Generate_should_use_DamlUnit_for_Archive_choice() templateFile.Should().NotBeNull(); var code = templateFile!.Content; - // Archive should use DamlUnit as it's an external type code.Should().Contain("Choice"); - code.Should().Contain("ArgumentEncoder = _ => DamlUnit.Instance"); + code.Should().Contain("ArgumentEncoder = _ => DamlRecord.Create()"); + code.Should().NotContain("ArgumentEncoder = _ => DamlUnit.Instance"); } [Fact] @@ -486,7 +486,7 @@ public void Generate_should_handle_multiple_choices_with_different_arg_types() // GetBalance - uses Unit argument code.Should().Contain("Choice ChoiceGetBalance"); - // Archive - uses DamlUnit for external type + // Archive - DamlUnit type parameters, but the wire argument encodes as an empty DamlRecord code.Should().Contain("Choice ChoiceArchive"); } diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterArchiveChoiceTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterArchiveChoiceTests.cs index 7972c74..6207c88 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterArchiveChoiceTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterArchiveChoiceTests.cs @@ -114,14 +114,16 @@ private static string EmitInterfaceExtensions(DamlInterface iface, string interf } [Fact] - public void archive_choice_skips_synthetic_stdlib_archive_in_non_contract_wrappers() + public void archive_choice_emits_synthetic_stdlib_archive_in_non_contract_wrappers() { var resolver = new StubResolver(packages: Packages(StdlibPackage)); var output = EmitNonContract(ItemTemplate(StdlibPackageId), resolver); - output.Should().NotContain("ItemNonContractExtensions"); - output.Should().NotContain("ArchiveAsync("); + output.Should().Contain("ItemNonContractExtensions"); + output.Should().Contain("public static async Task> ArchiveAsync("); + output.Should().Contain("DamlRecord.Create()"); + output.Should().NotContain("DamlUnit.Instance"); } [Fact] @@ -170,4 +172,34 @@ public void archive_choice_uses_actual_argument_type_not_damlunit_for_user_archi output.Should().Contain("User.Package.Archive argument,"); output.Should().Contain("argument.ToRecord()"); } + + [Fact] + public void archive_choice_encodes_synthetic_stdlib_archive_argument_as_empty_record_in_descriptor() + { + var resolver = new StubResolver(packages: Packages(StdlibPackage)); + var template = ItemTemplate(StdlibPackageId); + + var descriptor = EmitDescriptors(template, resolver); + + descriptor.Should().Contain("public static Choice ChoiceArchive { get; } = new()"); + descriptor.Should().Contain("ArgumentEncoder = _ => DamlRecord.Create(),"); + descriptor.Should().NotContain("ArgumentEncoder = _ => DamlUnit.Instance"); + } + + [Fact] + public void archive_choice_encodes_synthetic_stdlib_interface_archive_argument_as_empty_record() + { + var iface = new DamlInterface + { + Name = "Archivable", + Choices = [ArchiveChoice(StdlibPackageId)], + ViewType = null, + }; + var resolver = new StubResolver(packages: Packages(StdlibPackage)); + + var output = EmitInterfaceExtensions(iface, "IArchivable", resolver); + + output.Should().Contain("ExerciseCommand.ForInterface(contractId, new ChoiceName(\"Archive\"), DamlRecord.Create());"); + output.Should().NotContain("DamlUnit.Instance"); + } } diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterContractIdExerciserTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterContractIdExerciserTests.cs index 2340640..1f5fcc8 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterContractIdExerciserTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterContractIdExerciserTests.cs @@ -172,7 +172,7 @@ public void create_bearing_choice_exerciser_forwards_optional_timeout() var (_, exercisers) = Emit(template); exercisers.Should().Contain("TimeSpan? timeout = null,"); - exercisers.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + exercisers.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, submitter, timeout: timeout, cancellationToken: cancellationToken)"); var idxCommandId = exercisers.IndexOf("CommandId? commandId = null,", StringComparison.Ordinal); var idxTimeout = exercisers.IndexOf("TimeSpan? timeout = null,", StringComparison.Ordinal); @@ -181,6 +181,31 @@ public void create_bearing_choice_exerciser_forwards_optional_timeout() idxTimeout.Should().BeLessThan(idxCancellationToken); } + [Fact] + public void create_bearing_choice_emits_a_command_builder_shared_by_both_async_overloads() + { + var template = Template( + [new DamlFieldDefinition("owner", new DamlPrimitiveType(DamlPrimitive.Party))], + Choice("Spawn", ContractIdOf("Token"), controllers: StaticParties("owner"))); + + var (_, exercisers) = Emit(template); + + exercisers.Should().Contain("public static ExerciseCommand SpawnCommand("); + exercisers.Should().Contain("this ContractId contractId)"); + + const string commandConstructionMarker = "new ExerciseCommand("; + var firstConstruction = exercisers.IndexOf(commandConstructionMarker, StringComparison.Ordinal); + firstConstruction.Should().BeGreaterThanOrEqualTo(0); + exercisers.IndexOf(commandConstructionMarker, firstConstruction + 1, StringComparison.Ordinal).Should().Be(-1); + + const string commandCallMarker = "var command = contractId.SpawnCommand();"; + var firstCall = exercisers.IndexOf(commandCallMarker, StringComparison.Ordinal); + firstCall.Should().BeGreaterThanOrEqualTo(0); + var secondCall = exercisers.IndexOf(commandCallMarker, firstCall + 1, StringComparison.Ordinal); + secondCall.Should().BeGreaterThan(firstCall); + exercisers.IndexOf(commandCallMarker, secondCall + 1, StringComparison.Ordinal).Should().Be(-1); + } + [Fact] public void contract_overload_forwards_timeout_positionally_to_the_contract_id_overload() { diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterInterfaceExtensionTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterInterfaceExtensionTests.cs index 96c6cb7..e2525b6 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterInterfaceExtensionTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterInterfaceExtensionTests.cs @@ -114,7 +114,7 @@ public void interface_exerciser_forwards_optional_timeout() var output = EmitExtensions(Interface(Choice("Transfer", new DamlPrimitiveType(DamlPrimitive.Unit)))); output.Should().Contain("TimeSpan? timeout = null,"); - output.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + output.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken)"); var idxCommandId = output.IndexOf("CommandId? commandId = null,", StringComparison.Ordinal); var idxTimeout = output.IndexOf("TimeSpan? timeout = null,", StringComparison.Ordinal); @@ -122,4 +122,36 @@ public void interface_exerciser_forwards_optional_timeout() idxCommandId.Should().BeLessThan(idxTimeout); idxTimeout.Should().BeLessThan(idxCancellationToken); } + + [Fact] + public void interface_choice_emits_a_command_builder_that_returns_an_exercise_command() + { + var output = EmitExtensions(Interface(Choice("Transfer", new DamlPrimitiveType(DamlPrimitive.Unit)))); + + output.Should().Contain("public static ExerciseCommand TransferCommand("); + output.Should().Contain("this ContractId contractId)"); + output.Should().Contain("return ExerciseCommand.ForInterface(contractId, new ChoiceName(\"Transfer\"), DamlUnit.Instance);"); + } + + [Fact] + public void interface_choice_async_method_delegates_to_the_command_builder_instead_of_building_inline() + { + var output = EmitExtensions(Interface(Choice("Transfer", new DamlPrimitiveType(DamlPrimitive.Unit)))); + + output.Should().Contain("var command = contractId.TransferCommand();"); + output.Should().Contain("var submission = CommandsSubmission.Single(command)"); + output.Should().NotContain("var command = ExerciseCommand.ForInterface(contractId, new ChoiceName(\"Transfer\")"); + } + + [Fact] + public void interface_choice_command_builder_accepts_the_typed_argument_when_the_choice_has_one() + { + var output = EmitExtensions(Interface(Choice("Transfer", new DamlTypeRef(LocalPackageId, "Main", "TransferArg")))); + + output.Should().Contain("public static ExerciseCommand TransferCommand("); + output.Should().Contain("this ContractId contractId,"); + output.Should().Contain("TransferArg argument)"); + output.Should().Contain("return ExerciseCommand.ForInterface(contractId, new ChoiceName(\"Transfer\"), argument.ToRecord());"); + output.Should().Contain("var command = contractId.TransferCommand(argument);"); + } } diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterNonContractExerciserTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterNonContractExerciserTests.cs index e9603c9..845d883 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterNonContractExerciserTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterNonContractExerciserTests.cs @@ -136,7 +136,7 @@ public void value_returning_choice_exerciser_forwards_optional_timeout() var output = Emit(Template(Choice("Quote", new DamlPrimitiveType(DamlPrimitive.Numeric)))); output.Should().Contain("TimeSpan? timeout = null,"); - output.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + output.Should().Contain("client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken)"); var idxCommandId = output.IndexOf("CommandId? commandId = null,", StringComparison.Ordinal); var idxTimeout = output.IndexOf("TimeSpan? timeout = null,", StringComparison.Ordinal); @@ -144,4 +144,31 @@ public void value_returning_choice_exerciser_forwards_optional_timeout() idxCommandId.Should().BeLessThan(idxTimeout); idxTimeout.Should().BeLessThan(idxCancellationToken); } + + [Fact] + public void value_returning_choice_emits_a_command_builder_that_returns_an_exercise_command() + { + var output = Emit(Template(Choice("Quote", new DamlPrimitiveType(DamlPrimitive.Numeric)))); + + output.Should().Contain("public static ExerciseCommand QuoteCommand("); + output.Should().Contain("this ContractId contractId)"); + output.Should().Contain("return new ExerciseCommand("); + } + + [Fact] + public void value_returning_choice_async_method_delegates_to_the_command_builder_instead_of_building_inline() + { + var output = Emit(Template(Choice("Quote", new DamlPrimitiveType(DamlPrimitive.Numeric)))); + + output.Should().Contain("var command = contractId.QuoteCommand();"); + output.Should().Contain("var submission = CommandsSubmission.Single(command)"); + + const string inlineConstructionMarker = "var command = new ExerciseCommand("; + output.Should().NotContain(inlineConstructionMarker); + + const string commandConstructionMarker = "new ExerciseCommand("; + var firstConstruction = output.IndexOf(commandConstructionMarker, StringComparison.Ordinal); + firstConstruction.Should().BeGreaterThanOrEqualTo(0); + output.IndexOf(commandConstructionMarker, firstConstruction + 1, StringComparison.Ordinal).Should().Be(-1); + } } diff --git a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterValueReturnExerciserTests.cs b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterValueReturnExerciserTests.cs index ae75f6d..1cd9286 100644 --- a/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterValueReturnExerciserTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/ChoiceEmitterValueReturnExerciserTests.cs @@ -84,7 +84,7 @@ public void non_contract_exerciser_emits_typed_wrapper_delegating_to_ProjectComm output.Should().Contain("public static async Task> GetTrailingTwapAsync("); output.Should().Contain("this ContractId contractId,"); output.Should().Contain("ILedgerWriter client,"); - output.Should().Contain(".TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + output.Should().Contain(".TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken)"); output.Should().Contain("return outcome.ProjectCommitted(tx => ProjectGetTrailingTwapResult(tx, contractId.Value));"); output.Should().NotContain("Unhandled outcome"); output.Should().NotContain("ExerciseOutcome.DamlError"); diff --git a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.Conversions.cs b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.Conversions.cs index ed39db6..ce57b0e 100644 --- a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.Conversions.cs +++ b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.Conversions.cs @@ -37,7 +37,7 @@ public partial class CodeGenEdgeCaseTests false, ".ToDamlEnum()", ".ToRecord()")] - public void generate_dispatches_same_named_token_field_by_module_qualifier( + public void Generate_dispatches_same_named_token_field_by_module_qualifier( string referencedModule, bool includeRecordToken, string expectedFragment, diff --git a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.ForeignPackages.cs b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.ForeignPackages.cs index ddf2a24..fb8f961 100644 --- a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.ForeignPackages.cs +++ b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.ForeignPackages.cs @@ -275,6 +275,8 @@ public void Generate_should_map_Archive_choice_to_DamlUnit_when_template_package holding.Should().NotBeNull(); holding!.Content.Should().Contain("Choice DamlRecord.Create()"); + holding.Content.Should().NotContain("ArgumentEncoder = _ => DamlUnit.Instance"); holding.Content.Should().NotContain("No.Package.Metadata"); csproj.Should().NotBeNull(); @@ -283,7 +285,7 @@ public void Generate_should_map_Archive_choice_to_DamlUnit_when_template_package } [Fact] - public void Generate_should_map_Archive_interface_choice_to_DamlUnit_when_argument_package_is_placeholder_named() + public void Generate_should_encode_Archive_interface_choice_argument_as_empty_record_when_argument_package_is_placeholder_named() { const string PlaceholderPackageId = "lf1x-prim-id"; @@ -350,7 +352,8 @@ public void Generate_should_map_Archive_interface_choice_to_DamlUnit_when_argume iface.Should().NotBeNull(); iface!.Content.Should().Contain("ArchiveAsync("); - iface.Content.Should().Contain("DamlUnit.Instance"); + iface.Content.Should().Contain("DamlRecord.Create()"); + iface.Content.Should().NotContain("DamlUnit.Instance"); iface.Content.Should().NotContain("No.Package.Metadata"); csproj.Should().NotBeNull(); diff --git a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.InterfacePlaceholders.cs b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.InterfacePlaceholders.cs index 01a07b4..440060d 100644 --- a/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.InterfacePlaceholders.cs +++ b/tests/Daml.Codegen.CSharp.Tests/CodeGenEdgeCaseTests.InterfacePlaceholders.cs @@ -75,7 +75,7 @@ public void Generate_should_emit_interface_placeholder_record_as_ITemplate_with_ } [Fact] - public void placeholder_emits_throwing_daml_type_descriptor() + public void Generate_emits_throwing_daml_type_descriptor_for_placeholder() { var module = new DamlModule { @@ -323,7 +323,7 @@ public void Generate_should_emit_extension_class_for_interface_choices() code.Should().Contain("ExerciseCommand.ForInterface(contractId, new ChoiceName(\"Transfer\"), argument.ToRecord())"); // Submission is funnelled through ILedgerWriter.TrySubmitAndWaitForTransactionAsync // — same submission path as concrete-template Async. - code.Should().Contain("await client.TrySubmitAndWaitForTransactionAsync(submission, timeout: timeout, cancellationToken: cancellationToken)"); + code.Should().Contain("await client.TrySubmitAndWaitForTransactionAsync(submission, actAs, timeout: timeout, cancellationToken: cancellationToken)"); // Unit-argument choice: no `argument` parameter, DamlUnit.Instance is passed code.Should().Contain("public static async Task> LockAsync("); diff --git a/tests/Daml.Codegen.CSharp.Tests/DamlTypeMapperTests.cs b/tests/Daml.Codegen.CSharp.Tests/DamlTypeMapperTests.cs index 15df1b5..b2a8e74 100644 --- a/tests/Daml.Codegen.CSharp.Tests/DamlTypeMapperTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/DamlTypeMapperTests.cs @@ -322,6 +322,107 @@ public void from_value_handles_type_var_with_a_runtime_stub() .Should().Be("GenericStub.NotImplemented(\"a\")"); } + private static DamlPackage PackageWithGenericType(string module, string name, DamlDataTypeDefinition definition) => + new() + { + PackageId = CrossPackageId, + Name = "acme", + Version = new Version(1, 0, 0), + LfVersion = "2.1", + Modules = + [ + new DamlModule + { + Name = module, + Templates = [], + Interfaces = [], + DataTypes = + [ + new DamlDataType + { + Name = name, + TypeParams = ["a"], + Definition = definition, + } + ], + } + ], + DependencyReferences = [], + }; + + private static StubResolver ResolverWith(string resolvedName, DamlPackage package) => + new(resolvedName, new Dictionary { [CrossPackageId] = package }); + + private static DamlTypeApp GenericAppOfText(string module, string name) => + new(new DamlTypeRef(CrossPackageId, module, name), [Prim(DamlPrimitive.Text)]); + + [Fact] + public void to_value_serializes_a_user_generic_record_through_converter_lambdas() + { + var record = new DamlRecordDefinition([new DamlFieldDefinition("value", new DamlTypeVar("a"))]); + var resolver = ResolverWith("Acme.Box", PackageWithGenericType("Acme.Boxes", "Box", record)); + + Mapper(resolver).ToValue(GenericAppOfText("Acme.Boxes", "Box"), "Payload") + .Should().Be("Payload.ToRecord(__t0 => (DamlValue)(new DamlText(__t0)))"); + } + + [Fact] + public void from_value_deserializes_a_user_generic_record_through_converter_lambdas() + { + var record = new DamlRecordDefinition([new DamlFieldDefinition("value", new DamlTypeVar("a"))]); + var resolver = ResolverWith("Acme.Box", PackageWithGenericType("Acme.Boxes", "Box", record)); + + Mapper(resolver).FromValue(GenericAppOfText("Acme.Boxes", "Box"), "value") + .Should().Be("Acme.Box.FromRecord(value.As(), __v0 => __v0.As().Value)"); + } + + [Fact] + public void to_value_serializes_a_user_generic_variant_through_converter_lambdas() + { + var variant = new DamlVariantDefinition([new DamlVariantConstructor("Wrap", new DamlTypeVar("a"))]); + var resolver = ResolverWith("Acme.Choice", PackageWithGenericType("Acme.Choices", "Choice", variant)); + + Mapper(resolver).ToValue(GenericAppOfText("Acme.Choices", "Choice"), "Payload") + .Should().Be("Payload.ToVariant(__t0 => (DamlValue)(new DamlText(__t0)))"); + } + + [Fact] + public void from_value_deserializes_a_user_generic_variant_through_converter_lambdas() + { + var variant = new DamlVariantDefinition([new DamlVariantConstructor("Wrap", new DamlTypeVar("a"))]); + var resolver = ResolverWith("Acme.Choice", PackageWithGenericType("Acme.Choices", "Choice", variant)); + + Mapper(resolver).FromValue(GenericAppOfText("Acme.Choices", "Choice"), "value") + .Should().Be("Acme.Choice.FromVariant(value.As(), __v0 => __v0.As().Value)"); + } + + [Fact] + public void to_value_maps_a_type_var_field_to_its_injected_converter_delegate() + { + var delegates = new Dictionary { ["a"] = "convertTA" }; + + Mapper().ToValue(new DamlTypeVar("a"), "Value", delegates) + .Should().Be("convertTA(Value)"); + } + + [Fact] + public void from_value_maps_a_type_var_field_to_its_injected_converter_delegate() + { + var delegates = new Dictionary { ["a"] = "convertTA" }; + + Mapper().FromValue(new DamlTypeVar("a"), "value", delegates) + .Should().Be("convertTA(value)"); + } + + [Fact] + public void from_value_falls_back_to_the_stub_for_a_type_var_absent_from_the_delegate_map() + { + var delegates = new Dictionary { ["b"] = "convertTB" }; + + Mapper().FromValue(new DamlTypeVar("a"), "value", delegates) + .Should().Be("GenericStub.NotImplemented(\"a\")"); + } + private static readonly IReadOnlyDictionary SubtypeRepresentatives = new Dictionary { diff --git a/tests/Daml.Codegen.CSharp.Tests/DriftDetectionTests.cs b/tests/Daml.Codegen.CSharp.Tests/DriftDetectionTests.cs index d0ae651..545ddf1 100644 --- a/tests/Daml.Codegen.CSharp.Tests/DriftDetectionTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/DriftDetectionTests.cs @@ -21,6 +21,12 @@ namespace Daml.Codegen.CSharp.Tests; /// per-family Splice NuGet packages (the drift-detection suite). When /// codegen output legitimately changes, refresh the snapshot by following the /// procedure in Snapshots/<name>/README.md. +/// +/// A snapshot for a package that emits no C# types (a Daml helper library of +/// functions and re-exports, for example) is pinned by placing an +/// emits-no-types marker file in its snapshot directory. Such a snapshot +/// asserts that codegen produces zero .cs output; the day the package +/// starts emitting a type, the drift test fails so the change is not missed. /// public class DriftDetectionTests { @@ -114,12 +120,26 @@ public async Task Codegen_output_matches_snapshot(string snapshotName) $"If the change is unintentional, fix the codegen. " + $"Re-run only this snapshot with: dotnet test --filter \"FullyQualifiedName~DriftDetectionTests&DisplayName~{snapshotName}\""; - expectedFiles.Should().Contain( - f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), - "the snapshot must contain at least one .cs file; an empty fixture would let the test pass vacuously. " + refreshHint); - actualFiles.Should().Contain( - f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), - "codegen must emit at least one .cs file from the proto snapshot; zero .cs output indicates a regression in IntermediateDarReader.Read or Generate."); + var pinnedEmpty = File.Exists(Path.Combine(snapshotDir, "emits-no-types")); + + if (pinnedEmpty) + { + expectedFiles.Should().NotContain( + f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), + "the 'emits-no-types' marker pins this package as emitting no C# types, but the expected/ tree contains a .cs; refresh the snapshot or remove the marker. " + refreshHint); + actualFiles.Should().NotContain( + f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), + "this package is pinned as emitting no C# types (its 'emits-no-types' marker), but codegen now produced a .cs — its emitted surface changed. If the package legitimately gained types, remove the marker and refresh the snapshot. " + refreshHint); + } + else + { + expectedFiles.Should().Contain( + f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), + "the snapshot must contain at least one .cs file; an empty fixture would let the test pass vacuously (pin a genuinely type-less package with an 'emits-no-types' marker instead). " + refreshHint); + actualFiles.Should().Contain( + f => f.RelativePath.EndsWith(".cs", StringComparison.Ordinal), + "codegen must emit at least one .cs file from the proto snapshot; zero .cs output indicates a regression in IntermediateDarReader.Read or Generate."); + } actualFiles.Select(f => f.RelativePath).Should().Equal( expectedFiles.Select(f => f.RelativePath), diff --git a/tests/Daml.Codegen.CSharp.Tests/EmittedTemplateChoiceCompilesTests.cs b/tests/Daml.Codegen.CSharp.Tests/EmittedTemplateChoiceCompilesTests.cs index 3b5abcf..67e9ef5 100644 --- a/tests/Daml.Codegen.CSharp.Tests/EmittedTemplateChoiceCompilesTests.cs +++ b/tests/Daml.Codegen.CSharp.Tests/EmittedTemplateChoiceCompilesTests.cs @@ -277,6 +277,61 @@ public void Emitted_choice_returning_a_local_placeholder_interface_contract_id_c "the local placeholder record backing `Holdable` exposes no InterfaceId member — the projector must not reference one"); } + [Fact] + public void Emitted_template_implementing_a_local_interface_compiles() + { + var module = new DamlModule + { + Name = "Test.Module", + Templates = + [ + new DamlTemplate + { + Name = "Vault", + Fields = [new DamlFieldDefinition("owner", new DamlPrimitiveType(DamlPrimitive.Party))], + Choices = [], + Implements = [new DamlTypeRef("", "Test.Module", "Holdable")], + }, + ], + DataTypes = + [ + new DamlDataType + { + Name = "Vault", + Definition = new DamlRecordDefinition([new DamlFieldDefinition("owner", new DamlPrimitiveType(DamlPrimitive.Party))]), + }, + new DamlDataType + { + Name = "Holdable", + Definition = new DamlRecordDefinition([]), + }, + ], + Interfaces = [new DamlInterface { Name = "Holdable", Choices = [], ViewType = null }], + }; + + var package = new DamlPackage + { + PackageId = "test-package-id", + Name = "test-package", + Version = new Version(1, 0, 0), + LfVersion = "2.1", + Modules = [module], + DependencyReferences = [], + }; + + var dar = new DarModel { MainPackage = package, Dependencies = [] }; + var files = CreateGenerator().Generate(dar).ToList(); + + var vault = files.First(f => f.RelativePath.EndsWith("Vault.cs", StringComparison.Ordinal)).Content; + vault.Should().Contain("IImplements"); + + var diagnostics = CompileEmittedFiles(files); + var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + errors.Should().BeEmpty( + "a template implementing a local interface must emit IImplements and satisfy the where TInterface : IDamlInterface constraint, but got: {0}", + string.Join("\n", errors.Select(e => e.GetMessage() + " @ " + e.Location))); + } + [Fact] public void Emitted_create_bearing_choice_with_static_controllers_compiles_both_contractid_and_contract_overloads() { @@ -469,7 +524,7 @@ public void Emitted_template_with_payload_derived_observers_compiles() // -