Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,114 @@ because they are versioned in lockstep:

### Security

## [0.2.0-preview.2] — 2026-07-02

### Added

- `Daml.Runtime.Commands.DisclosedContract(string ContractId, Identifier TemplateId,
ReadOnlyMemory<byte> CreatedEventBlob)` — a new record type carrying an explicitly
disclosed contract (Daml 3.x explicit disclosure). `CommandsSubmission` gains an
optional trailing `IReadOnlyList<DisclosedContract>? DisclosedContracts` parameter and
a `WithDisclosedContracts(params DisclosedContract[])` fluent method, defaulting to
`null` so existing submissions are unaffected; calling it with no arguments, `null`,
or an empty array clears the field back to `null`. Record equality compares `CreatedEventBlob` by content, not by
memory reference. This repo only carries the value — mapping it onto the gRPC
`DisclosedContract` message lives in the ledger-client repo.
- `Daml.Runtime.Commands.CommandsSubmission` gains an optional trailing
`SynchronizerId? SynchronizerId` parameter and a `WithSynchronizerId(SynchronizerId)`
fluent method, mirroring `WithWorkflowId`/`WithCommandId`, so callers can carry a
submission-time synchronizer pin alongside a submission. This repo only carries the
value — wiring it into `BuildCommands`/proto conversion lives in the ledger-client
repo.
- `Daml.Runtime`: `ContractStreamEvent<T>.Unclassified(long Offset, string Kind)`
— new variant surfaced when a transport delivers an event that cannot be
mapped to any other discriminated-union case, so consumers can honour a
no-silent-drop policy instead of the event being dropped. Code that
exhaustively switches over `ContractStreamEvent<T>` needs a new arm.
- `Daml.Runtime.Contracts`: new `CaughtException(string ErrorId, string Message,
IReadOnlyDictionary<string, string> Metadata)` record and an
`ExercisedEvent.CaughtExceptions` init-only property (defaults to empty), so
consumers can tell whether a successful exercise recovered from a Daml
`try`/`catch`. Additive — existing positional `ExercisedEvent` constructions
stay source-compatible. Populating `CaughtExceptions` from the ledger wire
format is client-side and not yet implemented.
- `Daml.Runtime.Contracts.TransactionTree` and `TreeEvent` (`Created`/`Exercised`
cases) — a transport-neutral, tree-shaped sibling of `TransactionResult` that
preserves the parent/child hierarchy of a transaction's events (which creates
and sub-exercises a given exercise caused), with wire-level `DamlValue`
payloads consistent with `ExercisedEvent`. `TreeEvent.DescendantEvents()` and
`TransactionTreeExtensions.AllEvents`/`ToTransactionResult` give depth-first
traversal and compat-flattening to the existing `TransactionResult` shape.
Additive — `TransactionResult` is unchanged.
- `Daml.Codegen.Testing.Conformance.Richtypes.Suit` — a new pure
nullary-constructor Daml `enum` type in the `richtypes` conformance corpus,
plus a `SuitExtensions` class (`ToDamlEnum()`/`FromDamlEnum()`) and a new
`Suit Suit` field on `RichRecord` (positional constructor argument added
after `Outcome`). Closes the enum coverage gap flagged as a follow-up in
the bundle-level determinism gate.

### Changed

- **BREAKING: `ContractStreamEvent<T>.Created`, `.Archived`, and `.Exercised` now carry
a `SynchronizerId SynchronizerId` parameter**, positioned right after `Offset` (before
`WitnessParties`), matching where `Assigned`/`Unassigned` already carry
`Source`/`Target : SynchronizerId`. Every positional construction of these three
records must pass a `SynchronizerId` argument in the new position; regenerate/update
call sites accordingly.
- **The 4th NuGet version segment (`M.m.p.g`) of generated Splice/Daml.Finance
packages is now a uniform codegen-generation ordinal.** It is keyed to the
codegen-tool version and shared by every package — and every co-produced sibling
dependency floor — in a release, incrementing only when the codegen version
changes rather than per DAR-content change. This replaces the former per-package,
content-hash-driven revision counter and fixes two publish-time failures a codegen
upgrade could trigger: a new codegen version that changed emitted C# but not the
DAR proto hash previously froze the 4th segment, so regenerated packages collided
at the same version as the already-published set (`CS8920` on build, `NU1605` on
restore); and because all packages in a release now share one ordinal, co-produced
sibling `<PackageReference>` floors can no longer diverge from the versions actually
published together. The first post-upgrade ordinal is seeded above every published
revision (Splice → 3, Daml.Finance → 2).

### Fixed

- Generated Daml `enum` types now carry an XML doc comment (`/// <summary>...
enum constructor.</summary>`) above each constructor, matching every other
generated member. Previously the emitter produced undocumented constructors,
which built fine only because no pure nullary-constructor `enum` had ever
been generated; the first one (`Richtypes.Suit`, added in this release) failed
the build with `CS1591` under `TreatWarningsAsErrors`.
- A Daml template whose name equals another interface's generated `I`-prefixed
marker name (e.g. template `IFactory` alongside interface `Factory`) no longer
collides with it. A package's generated types all share one flat C# namespace,
so both were previously declared as public `IFactory` types in that namespace,
and the generated set failed to compile with `CS0101`. The interface marker
name now appends a trailing `_` until it no longer collides with a template in
its own package, consistently wherever the marker is referenced (declaration,
file name, and every in-package or cross-package type reference to it).

- The `<Choice>Result` projector (`FromCreatedContracts`) now matches an
interface-typed created slot against the created contract's `InterfaceIds`
rather than its `TemplateId`. A choice returning `ContractId I` (where `I` is a
Daml interface) previously emitted `IFactory.TemplateId.ModuleName` — but
generated interface markers expose no public `TemplateId` (it is an explicit
`IDamlType` member), so the projector failed to compile with `CS0117`. Slots to
a concrete template are unchanged. Surfaced by the full Splice/Daml.Finance
release build (`daml-finance-interface-holding-v4`,
`daml-finance-interface-instrument-base-v4`).
- Generated interface markers now expose a plain `public static Identifier InterfaceId
{ get; }` alongside the existing explicit `IDamlInterface.InterfaceId`
implementation. For a `ContractId I` choice-result slot targeting a foreign
(cross-package) interface, the `<Choice>Result` projector's interface-matching
branch reads `{Interface}.InterfaceId.ModuleName`/`.EntityName` off this new member
instead of baking the interface's module/entity as string literals into the
emitted source — robustness/consistency with the template branch, which already
reads `{Template}.TemplateId`. Slots targeting a *local* interface ref keep
matching via string literals baked at codegen time: the LF-mandated record
RecordEmitter always emits alongside a local `interface I where ...` declaration
is a throwing `ITemplate` placeholder stub with no `InterfaceId` member, so those
slots cannot safely reference a generated symbol.


## [0.2.0-preview.1] — 2026-06-30

### Added
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PackageIcon>icon.png</PackageIcon>

<!-- Versioning -->
<Version>0.2.0-preview.1</Version>
<Version>0.2.0-preview.2</Version>
<AssemblyVersion>0.2.0.0</AssemblyVersion>
<FileVersion>0.2.0.0</FileVersion>

Expand Down
46 changes: 37 additions & 9 deletions src/Daml.Codegen.CSharp.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ internal static async Task<int> Main(string[] args)

var emitterCounterOption = new Option<int>("--emitter-counter")
{
Description = "4th segment of the generated NuGet version (Major.Minor.Patch.Revision). Defaults to 0; set a monotonic counter to distinguish republished builds of the same source.",
Description = "4th segment of the generated NuGet version (Major.Minor.Patch.Generation). Defaults to 0; set a monotonic counter to distinguish republished builds of the same source. Overridden by --release-counters, which resolves the segment as a codegen-generation ordinal.",
DefaultValueFactory = _ => 0
};
emitterCounterOption.Validators.Add(result =>
Expand All @@ -122,9 +122,31 @@ internal static async Task<int> Main(string[] args)

var releaseCountersOption = new Option<FileInfo?>("--release-counters")
{
Description = "Path to a JSON release-counter store. Requires --intermediate (the content hash that keys the store is computed from the IntermediateDar proto bytes). When set, the 4th NuGet version segment is resolved from this store, overriding --emitter-counter. The store is created on first use and atomically updated on each run."
Description = "Path to a JSON release-counter store. When set, the 4th NuGet version segment is resolved from this store as a codegen-generation ordinal keyed by --codegen-version, overriding --emitter-counter. The store is created on first use and atomically updated when a new codegen version is first seen."
};

var codegenVersionOption = new Option<string?>("--codegen-version")
{
Description = "Codegen-tool version that keys the release-counter generation ordinal (the 4th NuGet version segment). Every package produced by one codegen version shares the ordinal, which increments when the version changes. Defaults to this emitter build's informational version (AssemblyInformationalVersionAttribute) with any '+' build metadata stripped."
};
codegenVersionOption.Validators.Add(result =>
{
var value = result.GetValue(codegenVersionOption);
if (value is null)
{
return;
}
if (string.IsNullOrWhiteSpace(value))
{
result.AddError("--codegen-version must be a non-empty version string when specified (e.g. 0.2.0-preview.3).");
return;
}
if (result.GetValue(releaseCountersOption) is null)
{
result.AddError("--codegen-version has no effect without --release-counters; supply --release-counters <path> to key the generation ordinal, or drop --codegen-version.");
}
});

var versionSuffixOption = new Option<string?>("--version-suffix")
{
Description = "SemVer prerelease suffix appended to generated package versions, e.g. 'preview.2'. Mirrors the emitter prerelease tag. No leading dash."
Expand Down Expand Up @@ -178,6 +200,7 @@ internal static async Task<int> Main(string[] args)
rootCommand.Options.Add(contractIdentifiersOption);
rootCommand.Options.Add(emitterCounterOption);
rootCommand.Options.Add(releaseCountersOption);
rootCommand.Options.Add(codegenVersionOption);
rootCommand.Options.Add(packageLicenseOption);
rootCommand.Options.Add(versionSuffixOption);
rootCommand.Options.Add(repositoryUrlOption);
Expand All @@ -198,6 +221,7 @@ internal static async Task<int> Main(string[] args)
parseResult.GetValue(contractIdentifiersOption),
parseResult.GetValue(emitterCounterOption),
parseResult.GetValue(releaseCountersOption),
parseResult.GetValue(codegenVersionOption),
parseResult.GetValue(packageLicenseOption)!,
parseResult.GetValue(versionSuffixOption),
parseResult.GetValue(repositoryUrlOption)),
Expand Down Expand Up @@ -265,29 +289,32 @@ private static async Task GenerateFromIntermediate(FileInfo file, CodegenArgs ar
logger.Debug($" Dependencies: {dar.Dependencies.Count}");

var effectiveCounter = args.ReleaseCountersFile is not null
? ResolveReleaseCounter(args.ReleaseCountersFile, proto, dar.MainPackage.Name, dar.MainPackage.Version, logger)
? ResolveReleaseCounter(args.ReleaseCountersFile, ResolveCodegenVersion(args), dar.MainPackage.Name, dar.MainPackage.Version, logger)
: args.EmitterCounter;

var generator = new CSharpCodeGenerator(BuildOptions(args, effectiveCounter), logger);
var generatedFiles = generator.Generate(dar);
await WriteGeneratedFiles(generatedFiles, args, logger, cancellationToken);
}

private static string ResolveCodegenVersion(CodegenArgs args) =>
string.IsNullOrWhiteSpace(args.CodegenVersion)
? ProjectFileGenerator.EmitterLockstepVersion
: args.CodegenVersion;

private static int ResolveReleaseCounter(
FileInfo storeFile,
IntermediateDar proto,
string codegenVersion,
string packageName,
Version packageVersion,
ConsoleLogger logger)
{
var hash = IntermediatePackageContentHash.Compute(proto.Main);
var store = JsonReleaseCounterStore.OpenOrCreate(storeFile.FullName);
var version = NuGetVersionResolver.Compute(packageName, packageVersion, hash, store);
var version = NuGetVersionResolver.Compute(packageVersion, codegenVersion, store);

var truncated = hash[..Math.Min(12, hash.Length)];
logger.Info($" Release counter: {packageName}@{packageVersion.Major}.{packageVersion.Minor}.{Math.Max(0, packageVersion.Build)} content_hash={truncated}… version={version}");
logger.Info($" Release counter: codegen_version={codegenVersion}; {packageName} {packageVersion.Major}.{packageVersion.Minor}.{Math.Max(0, packageVersion.Build)} version={version}");

return version.Revision;
return version.Generation;
}

private static CodeGenOptions BuildOptions(CodegenArgs args, int emitterCounter) =>
Expand Down Expand Up @@ -348,6 +375,7 @@ internal sealed record CodegenArgs(
bool GenerateContractIdentifiers,
int EmitterCounter,
FileInfo? ReleaseCountersFile,
string? CodegenVersion,
string PackageLicenseExpression,
string? VersionSuffix,
string? RepositoryUrl);
2 changes: 1 addition & 1 deletion src/Daml.Codegen.CSharp/CodeGen/CSharpCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private IEnumerable<GeneratedFile> GeneratePackage(ICrossPackageResolver resolve
}

var code = GenerateInterface(context, interfaceEmitter, package, module, iface);
var path = RelativeFilePath(rootNamespace, $"{Identifiers.InterfaceMarkerName(iface.Name)}.cs");
var path = RelativeFilePath(rootNamespace, $"{Identifiers.InterfaceMarkerName(iface.Name, context.LocalTemplateClassNames)}.cs");

yield return GeneratedFile.Text(path, code);
}
Expand Down
Loading
Loading