Align the runtime emitter with the generator naming convention - #19
Merged
Conversation
Reuse a cached payload class whenever a structured register names the same interfaceType, matching the payload lookup both generator targets perform. The previous check compared the cached payload against the element size times the declared length, which is only how a payload is sized when a length is given; a payloadSpec spanning several elements without one takes its size from the member offsets instead, so two identical registers sharing an interfaceType were rejected. The check also passed a genuine mismatch whose total width happened to agree. Cover the reuse with the anchor and merge form the published schemas use, and with an offset-sized payload carrying no declared length.
Payload members take a payload_ prefix or become private, so the public surface is payload_dtype, payload_array, payload_from_buffer and payload_as_columns. A field name is barred from those prefixes rather than from a list of the members that exist today, and is also rejected when it is a Python keyword or not a valid identifier. A schema field can no longer shadow a payload member. Keys such as ToColumns, RawPayload and Dtype now decode to ordinary fields, where before they replaced the member of that name, and a key such as Break is rejected rather than producing a field reachable only through getattr. A rejected field or an unusable register is reported against the register as the schema spells it, rather than against the payload class name the emitter derives from it.
parse_device_schema and create_device_module take str or bytes, so a schema can be read with read_bytes() and the YAML stream declares its own encoding. Reading it with read_text() and no explicit encoding follows the locale instead, which silently mangles a non-ASCII description under cp1252 and fails to parse at all under latin-1. The benchmark report and its README name payload_as_columns, matching the method they measure.
bruno-f-cruz
commented
Aug 11, 2026
bruno-f-cruz
left a comment
Member
Author
There was a problem hiding this comment.
One small issue about a public interface/naming, otherwise looks good.
The auto-generated batch sibling is _PayloadBatchType rather than _batch, so the attribute reads as the class it holds. It stays private, since the Batch protocol is what callers annotate against.
glopesdev
approved these changes
Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the naming divergence #13 introduced deliberately. That PR kept schema identifiers verbatim and recorded the cost in its own description.
harp.device._schemanow produces the same identifiers a statically generated package does, so code written against either lines up name for name.The divergence mattered more after #17. Registers are now reached by name off the module, so a schema-built device and a generated one are meant to be interchangeable at the call site, and identifiers were the last place they were not.
What this changes
Payload fields become
snake_caseand enum membersSCREAMING_SNAKE_CASE:Type-level names are untouched. Register classes, enum classes and
{Name}Payloadstay verbatim from the yml, which is what the generator emits too. This is a breaking change for anything reading a runtime-emitted register by field name, including dataframe columns and payload keyword construction.The hand-written models in
harp-benchmarksmove to the same convention, so identifiers are consistent across the repository.Where the convention comes from
New
harp.device._schema._naming, a port ofFirmwareNamingConvention.Applyinharp-tech/generators. The Python target defines no casing of its own.Python.csroutes every identifier through that one class,GetPythonFieldNamebeingApply(name).ToLowerInvariant(), andPyDevice.ttonly prints names already computed. Porting anything else would have been guessing at the generator rather than matching it.Two public functions named for what they produce rather than for the C# class they came from, since nothing here concerns firmware:
The algorithm is not a word-boundary regex. A run of capitals stays one word and a trailing digit never separates, so
TestDIPort1isTEST_DI_PORT1,PortDIO1isPORT_DIO1, andDIO0is unchanged. Two details of the C# are reproduced rather than tidied: the regex match index falls on the separator when one is present, and the replacement lookahead reads the pre-substitution string.What deliberately keeps its YML name
A custom converter symbol derives from the pre-rename key, matching
Python.cscapturingconverterBaseNamebefore renaming, so memberDatastill resolvesDataConverterrather thandataConverter.ConverterContext.nametherefore stays verbatim and the factory contract from #13 is unchanged.Payload members carry a reserved prefix
Lowercasing field names put them in the same namespace as the payload API, where a schema field would silently replace a method. A field named
ToColumnswould have overwrittento_columns, and one namedRawPayloadwould have madeformatemit a short frame with no error anywhere.Payload members therefore take a
payload_prefix or become private, and a field name is barred from the prefixes rather than from a list of the members that exist today:This is a breaking change to
harp-protocol. It also closes a case no list could have covered. A field renaming to a Python keyword, such asBreakbecomingbreak, is now rejected rather than producing an attribute reachable only throughgetattr.Payload class sharing follows the generator
A structured register declaring an
interfaceTypenames its payload after that type and registers sharing the type share one class, as both generator targets do. An earlier revision of this PR added a size check on that reuse. That check rejected two identical registers whosepayloadSpecspans several elements without a declaredlength, which is howRgb0andRgb1indevice.behaviorare written, and it passed a genuine mismatch whose total width happened to agree. The reuse is now unvalidated here exactly as it is in the generator, and the underlying gap is recorded at harp-tech/generators#122.Schema text or bytes
parse_device_schemaandcreate_device_moduleacceptstrorbytes, so a schema can be read withread_bytes()and the YAML stream declares its own encoding. Reading it withread_text()and no explicit encoding follows the locale instead, which silently mangles a non-ASCII description under cp1252 and fails to parse at all under latin-1.Testing
Full suite passes at 357 tests, with ruff, pyright, codespell and
mkdocs build --strictclean.test_naming.py, every pair lifted from the Python generator committed expected output read against its test metadata, so the port is pinned to the C# behavior rather than to a re-derivation of it. Plus fixed-point and degenerate inputs._layout()intest_emit.pyno longer strips field names, so the existing register-for-register comparison againstexpected_device.pycovers identifiers alongside byte layout. All 15 device registers match, and enum parity is asserted per enum-backed field.decode_enumsis back on, so emitted and generated classes are compared on column names and decoded enum labels rather than raw codes by position.expected_device.pyis unchanged.