Skip to content

Clear nullable warnings in the generated interface - #148

Merged
glopesdev merged 4 commits into
harp-tech:mainfrom
glopesdev:generated-csharp-nullable
Sep 20, 2026
Merged

glopesdev merged 4 commits into
harp-tech:mainfrom
glopesdev:generated-csharp-nullable

Conversation

@glopesdev

Copy link
Copy Markdown
Contributor

Roslyn already treats any file whose name ends in .generated.cs as generated code, and the C# target emits Device.Generated.cs and AsyncDevice.Generated.cs, so no downstream analyzer had ever reported a diagnostic against a generated interface. Emitting the <auto-generated> marker states that explicitly instead of leaving it to a file-naming coincidence, and it makes the correctness of the emitted code this repository's own responsibility.

Specifically, nullable analysis had never been run against generated output, and running it for the first time reported 22 warnings for the core interface and 35 for the device interface. These turned out to be real defects, so this PR fixes them and adds regression tests to hold this standard going forward.

What the analysis found

Three causes account for all 57:

  • ArraySegment<byte>.Array is nullable on net8.0, and the emitted PayloadMarshal helpers and payload converters dereferenced it. 21 warnings in each output, as CS8602 and CS8604.
  • Reference-typed payload properties on the generated operators were left uninitialized. One in the core interface and twelve in the device interface, as CS8618.
  • DeviceDataWriter.Path returned the result of Path.GetDirectoryName, which is null when the file name is unset. Two in the device interface, as CS8603.

The uninitialized properties matter because those are operator properties a workflow author fills in from the property grid. For example, the default converter for an array is ArrayConverter, which expands a value into one editable row per element, so an array initialized to its declared length offers exactly the right number of rows to fill in, while a null expands into nothing. The declared length is not recoverable from the editor, since Bonsai is not a code editor and nothing in a workflow surfaces the register length or relates it to the size of the array.

Emitting an initializer also makes the compiler check that the declared interface type can be constructed, which is required by XmlSerializer so that a workflow containing the operator can be saved.

What changed in the generated output

PayloadMarshal and the payload converters take an array with an offset and a count instead of an ArraySegment, and generated code unwraps the segment returned by HarpMessage.GetPayload once in each register accessor. The nine primitive write overloads take no count, since the value type fixes it. Reference-typed payload properties are initialized, strings to empty, arrays to their declared length, and other types through a parameterless constructor. DeviceDataWriter.Path returns an empty string for an unset file name.

Timestamped accessors deconstruct the payload and the timestamp in one statement rather than binding the pair and reading Value and Seconds separately, so every converting accessor has the same shape.

Breaking changes

Any hand-written converter has to be updated. The register-level and member-level converter stubs take an array with an offset and a count, and a converter that calls a primitive PayloadMarshal.Write has to drop one argument. That last one reports as CS1503 naming string, because a four-argument call binds to the string overload, so the diagnostic does not point at the cause.

RegisterInfo.PayloadInterfaceType and PayloadMemberInfo.GetConverterInterfaceType are removed from the public surface and now sit on the internal template helper. Neither had a consumer, so no downstream code breaks.

No known published device is affected, as no use of a custom converter could be found in downstream repositories.

Why the tests carry their own HarpVersion

Bonsai.Harp.HarpVersion is sealed, has no parameterless constructor, and exposes Major and Minor with private setters, so it cannot currently be serialized by XmlSerializer. A workflow containing CreateVersionPayload cannot be saved today, independently of this branch. The generator now emits = new() for a reference interfaceType carrying no other initializer, which turns that into a compile error rather than a failure at workflow save. tests/HarpVersion.cs is a test-only stand-in so the suite can exercise the path until the upstream type gains a parameterless constructor or IXmlSerializable.

Regression tests

CompilerTestHelper strips the auto-generated header, compiles with nullable reference types enabled, and fails on any nullable warning, so every test that compiles generated output is covered. It also fails when a header is present and cannot be stripped, so a drifting marker cannot make the check pass by suppressing what it looks for.

Closes #138

Both C# templates now open with the System.CodeDom banner around an
auto-generated element, matching the two sentences the Python target
already emits. Compilers, analyzers and formatters then treat the
output as generated code whatever the file is named, so a consuming
project with nullable reference types enabled reports none of the
nullable warnings the emitted PayloadMarshal helper would otherwise
raise.

Closes harp-tech#138
PayloadMarshal and the payload converters no longer take an
ArraySegment. Generated code unwraps the segment from
HarpMessage.GetPayload once in each register accessor, so nothing
downstream reads the nullable Array property.

Reference-typed payload properties are initialized instead of left
null, strings to empty, arrays to their declared length, and other
types through a parameterless constructor. DeviceDataWriter.Path
returns an empty string for an unset file name.

With nullable reference types enabled the core and device interfaces
now compile with no warnings. A test-only HarpVersion stands in for
the Bonsai.Harp type, which has no parameterless constructor. The
metadata model no longer exposes PayloadInterfaceType or
GetConverterInterfaceType, which move to the internal template helper.
The compiler test helper now strips the auto-generated header,
compiles with nullable reference types enabled, and fails on any
nullable warning. Every test that compiles generated output goes
through the helper.
@glopesdev glopesdev added the feature New planned feature label Sep 20, 2026
@glopesdev
glopesdev merged commit f077de7 into harp-tech:main Sep 20, 2026
9 checks passed
@glopesdev
glopesdev deleted the generated-csharp-nullable branch September 20, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New planned feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emit an auto-generated file header in the C# interface output

1 participant