Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DSDL compatibility analysis #9

Closed
kjetilkjeka opened this issue Aug 9, 2018 · 16 comments
Closed

DSDL compatibility analysis #9

kjetilkjeka opened this issue Aug 9, 2018 · 16 comments

Comments

@kjetilkjeka
Copy link
Contributor

I've open this issue to discuss how changes to data types should be handled. This issue is a follow up on OpenCyphal/public_regulated_data_types#35, #5 and #6 but tries to isolate the compatibility discussion without blending it together with other review related items.

I will attempt to keep the text under the line updated to reflect the consensus of the discussion. The initial text is based on previous discussion and (where there are little discussion) my first instincts. Please criticize so we can improve upon it.


Compatibility

Compatibility is a relationship that message definition A can have with message definition B. If A is compatible with B and B is compatible with A we say they are mutually compatible. The different forms of compatibility is defined below.

Bit compatibility

A structure definition A is bit-compatible with a structure definition B if any valid encoded representation of B is also a valid encoded representation of A.

Semantic compatibility

A data structure definition A is semantically compatible with a data structure definition B if an application that correctly uses A exhibits a functionally equivalent behavior to an application that correctly uses B, and A is bit-compatible with B.

Code compatibility

We say that A is code compatible with B if valid use of code generated from B also is valid use of code generated from A with the same spec-adhering compiler.

For a compiler to be spec-adhering it must generate compatible code if:

  • All variable names that exists in B must also exist in A. They must also have the same type.
  • All const names that exists in B must also exist in A. They must also have the same type.

ID compatible

We say that A is ID compatible with B If B has a default ID, and A has exact same default ID or B does not have a default ID.

Compatibility guarantees

  • For definitions with different major version numbers, no compatibility guarantees are given.
  • For definitions with major version equal 0, no compatibility guarantees are given.
  • For definitions with major version not equal 0 and identical path the version with the highest version number will have the following compatibilities with the other version.
    • Bit compatibility
    • Semantic compatibility
    • Code compatibility
    • ID compatible

Mutability guarantees

All definitions are immutable. This means that after a definition is published a new definition with the same namespace and name must have a higher version number. There are a few exceptions:

  • Whitespace formating is allowed to change on published types
  • Comments are allowed to change on published types, as long as this doesn't affect semantic compatibility.
  • Version 0.0 is not considered in any way immutable and may change in any way it might please.

Open questions

  • How do we express deprecation?
  • Should all definitions with major version == 0 be considered mutable or only 0.0?
  • In the specification semantic compatibility implies bit compatibility. Should this be the rule, or should it be orthogonal concepts?
@kjetilkjeka
Copy link
Contributor Author

@pavel-kirienko i think the most important question is the code compatibility. You seem to opting out of it completely when writing the specification. We should discuss the value of being able to do invasive changes within one major version vs stability in code when using the same major version.

Even though I've described the most conservative approach there exists middle grounds. For instance not guaranteeing that fields will be of the same type, but all valid const assignments before the update are also valid after the update.

The argument for doing invasive changes within one major version you presented the last time was #5 (comment) i think this problem is not very big as it can be fixed with bit masks and would probably have been fixed in the 0.X phase, and therefore not a good reason to allow breaking code compatibility. Do you have examples with more serious problems?

The reason we need (even if it turns out to be a more relaxed sort) code compatibility is that the code generated with the dsdl compiler will also need to adher to semantic versioning for the dsdl types to be truly useful in dependency systems using semver (like cargo). I think a stricter form of code compatibility will produce code that is nicer to use, so this will be a compromise.

@pavel-kirienko
Copy link
Member

How do we express deprecation?

I have added a @deprecated keyword to the current draft a couple of weeks ago; it's supposed to be translated into language-specific deprecation markers, e.g. [[deprecated]] in C++ or DeprecationWarning in Python.

Code compatibility

I think this is a bad idea. The specification should only care about bit compatibility and semantic compatibility; adding more guarantees undermines the extensibility of the standard. Yes, that implies that when an application migrates from one minor datatype version to another it may have to update the code that uses the datatype, but that is acceptable and doesn't even go against the principles of semantic versioning. What we need to be focused on is whether different nodes can successfully communicate and cooperate using data type definitions with different minor version numbers. The following example provided in the specification illustrates that well:

First definition:

# demo.Pair
float16 first
float16 second
# demo.PairVector
demo.Pair[3] vector
demo.PairVector pair_vector

Second definition:

float16 first_0     # pair_vector.vector[0].first
float16 second_0    # pair_vector.vector[0].second
float16 first_1     # pair_vector.vector[1].first
float16 second_1    # pair_vector.vector[1].second
float16 first_2     # pair_vector.vector[2].first
float16 second_2    # pair_vector.vector[2].second

The code compatibility guarantee would partially defeat the advantages of the new version management logic.


On the subject of binary compatibility: I've been silent lately because of this short statement you made in a different thread:

3.6.2.1 Valid encoded representation needs defining. One of the things im unsure of: Is a void field encoded to a non-zero value valid encoded?

I thought that I'm just going to throw in a few words to clarify things. However, what actually happened is that I opened a huge can of worms with a whole universe in it. I was sketching some bit patterns in my notepad as I received a notification about this issue; now let me go back to it.

@kjetilkjeka
Copy link
Contributor Author

kjetilkjeka commented Aug 9, 2018

How do we express deprecation?

Let's address this concern before moving on to the bigger concerns.

I have added a @deprecated keyword to the current draft a couple of weeks ago; it's supposed to be translated into language-specific deprecation markers, e.g. [[deprecated]] in C++ or DeprecationWarning in Python.

That's nice. I was playing around with a similar idea myself. What i was thinking about was adding
@lifecycle <pre-release|released|deprecated|removed>, this way we can add a new type without releasing it, yank a buggy type and deprecate old types. This is how i vision it would work.

  • Pre-realease - do not generate code for this definition unless some dangerous keyword is presented.
  • Released - As normal (as it currently works)
  • Deprecated - Issue a warning that the type is deprecated and will be removed in the future.
  • Removed - Issue an error that this type has been through the deprecation process and is no longer usable.

Even if we initially only supported deprecated It would allow us to add other lifecycle items (like removed) later on without making a new directive. Both our solutions would of course require the directive related to deprecated to be an exception to the immutability guarantees together with comments and whitespaces (which is of course totally fine).

@pavel-kirienko
Copy link
Member

I like the flexibility argument, but I have two questions:

  1. What's the point of the removed option if the definition can be just physically removed to the same effect?
  2. pre-release goes against the versioning guarantees that once something is published it can't be removed or changed without a proper deprecation process. If you want tentative volatile definitions, use version 0. I think we can make this work but that would be at the cost of extra complexity in the specification, so I am against it.

If the two options mentioned above are removed, we're back to just deprecated and released. I think the meaning of the keyword @lifecycle might be slightly less transparent than @deprecated, i.e. somebody who is not familiar with the UAVCAN specification might fail to understand what it means; @deprecated seems more approachable. Also, most published data type definitions will be in the release stage, meaning that it would make sense to assume release by default if not specified, so @lifecycle release becomes redundant.

I suggest keeping @deprecated.

@kjetilkjeka
Copy link
Contributor Author

I split the deprecation issue out to: #10 for making the issues easier to follow from the outisde

@kjetilkjeka
Copy link
Contributor Author

kjetilkjeka commented Aug 9, 2018

I thought that I'm just going to throw in a few words to clarify things. However, what actually happened is that I opened a huge can of worms with a whole universe in it. I was sketching some bit patterns in my notepad as I received a notification about this issue; now let me go back to it.

Isn't the problem here essentially that a "valid encoding" and a "valid decoding" is two seperate things, and therefore defining a single "valid encoded representation" term is impossible (or at least needs to be the most liberal of these). I think if we can change the definition to:

Bit compatibility

A structure definition A is bit-compatible with a structure definition B if both the following statements are true:

  1. The encoding of any valid frame of type B can be decoded to a an element of type A.
  2. For every value that can be decoded into an element of type B there exists (one or more) an element of type A that will be encoded into this value.

Or a bit more verbose.

Let's define A as the set of possible instantiations of a given dsdl definition.

Let's define S(A) as the bit_masks that respects ranges of dynamic structures when decoding DSDL. This means that if an element from S(A) was decoded into A: the total length, all array lengths and union discriminators would be valid. (this can be specified more constructively if needed, OpenCyphal-Garage/uavcan.github.io#40 can be used as a start if needed )

An encoding is a function from A to S (e_A: A -> S(A)). We do not require this function to be surjective but we do require it to be injective (two elements in A can only have the same encoding if they are equal).

On the other hand, a decoding is a function from S(A) to A (d_A: S(A) -> A`). We do not require this function to be injective but we require it to be surjective (an element cannot exist without at least one valid encoding)

Let's define B as the set of possible instantiations for some other given dsdl definition.

For bit compatibility between the definitions that constructs A and B we require:

@pavel-kirienko
Copy link
Member

Or a bit more verbose.

So my approach was different - I was attempting to avoid defining the decoding function completely, expressing the relations in terms of the encoding function only: A is compatible with B if the set of all possible encoded representations of A is a superset of all possible encoded representations of B. However, void fields were giving me trouble, because they are by definition to be encoded as zero, which breaks the set relationship if at least one void bit is to be replaced with a non-void bit. I also tried defining an extended set of all possible encoded representations by allowing the void fields to take arbitrary values, but that created some ugly corner cases with dynamic fields (specifically, array length prefixes and union discriminators could no longer consume void bits because that broke the set relationship as well, making the types incompatible when they should be).

Your very last equation makes perfect sense. Let me try continuing with your approach.

@pavel-kirienko
Copy link
Member

Wait no, loss of information may occur at d_A(e_B(b)) and d_B(e_A(b)) - fields that are present in one definition may be missing in the other, so your last equation does not hold.

@kjetilkjeka
Copy link
Contributor Author

kjetilkjeka commented Aug 10, 2018

Wait no, loss of information may occur at d_A(e_B(b)) and d_B(e_A(b)) - fields that are present in one definition may be missing in the other, so your last equation does not hold.

Since the definition is concerned with A being bit compatible with B loss of information may occur when going from A to B (since a void field may have been populated) but not the other way around.

First observe that d_X(e_X(a)) = a always holds (this is the identity function on the set X). But e_ X(d_X(s)) = s only holds when s is an element in the range of e_X

If we first define c as c = e_ B(b) we can then see that the equation c = d_A(e_A(c)) always holds iff (if and only if) c is in the range of e_A. This boils the second conditions down to the same as the range of e_B being a subset of e_A (perhaps this is a useful formulation?).

The consequence is that it's not backwards compatible to remove void fields (or generally remove information) but it is backwards compatible to populate void fields (or generally add information), i think this was what we wanted? Ìf you instead would like the relaxed definition where only the first condition are present. You're essentially asking for serialization compatibility as defined in OpenCyphal-Garage/uavcan.github.io#40

@pavel-kirienko
Copy link
Member

The consequence is that it's not backwards compatible to remove void fields (or generally remove information) but it is backwards compatible to populate void fields (or generally add information), i think this was what we wanted?

That seems overly strict. One can imagine a use case where a new void field would need to be introduced, e.g. if a non-void field became deprecated or split into several smaller fields. I suggest staying generic; perhaps we should move this conversation back to #11 where I have proposed a very minimalistic formulation.

@kjetilkjeka
Copy link
Contributor Author

I think we need to talk about the concrete things we expect to have from compatibility data types and their concrete value:

Problems

Populating void fields

I think populating void fields is the prime example of what we're 100% sure that we want and doesn't cost us anything. The constraint is that newly populated fields must always be considered as an "optional" with a None variant of the default void serialization value. The following example is overly pedantic about this.

Example

# Version 1.0
uint8 foo
void8
# Version 1.1
uint8 foo

uint8 BAR_NONE = 0
uint8 bar

Removing information

Any form of removing information must be forbidden. If this is not true we must expect that every important piece for information can be removed in the future. This is a destructive way to have to think when working with uavcan. And removing fields should always be semantically incompatible.

Example fields

# Version 1.0
uint8 a
uint8 b
# Version 1.1
uint8 a
void8

Examples consts

# Version 1.0
uint8 FOO = 42
uint8 bar
# Version 1.1
uint8 bar

Unrolling arrays/structs or merging fields

Unrolling or enrolling structs and arrays and merging of fields can make the data types more convenient for the user, but it does not add or remove any information. The problem with this is that it also breaks code compatibility which is inconvenient for users.

Instead of doing this on released data types we should experiment enough in the pre-releases that we "get it right". If we fail, we should still not do this for released datatypes as the code breakage is much more severe and annoying than the benefit of being able to make these changes.

Example unrolling/enrolling (stolen from Pavel)

# demo.Pair
float16 first
float16 second
# demo.PairVector
demo.Pair[3] vector
demo.PairVector pair_vector

Second definition:

float16 first_0     # pair_vector.vector[0].first
float16 second_0    # pair_vector.vector[0].second
float16 first_1     # pair_vector.vector[1].first
float16 second_1    # pair_vector.vector[1].second
float16 first_2     # pair_vector.vector[2].first
float16 second_2    # pair_vector.vector[2].second

Example merging fields (stolen from Pavel)

# Alert flags are allocated at the bottom (from bit 0 upwards)
uint64 DC_UNDERVOLTAGE                       = 1 << 0
uint64 DC_OVERVOLTAGE                        = 1 << 1
uint64 DC_UNDERCURRENT                       = 1 << 2
uint64 DC_OVERCURRENT                        = 1 << 3
uint64 CPU_COLD                              = 1 << 4
uint64 CPU_OVERHEATING                       = 1 << 5
uint64 VSI_COLD                              = 1 << 6
uint64 VSI_OVERHEATING                       = 1 << 7
uint64 MOTOR_COLD                            = 1 << 8
uint64 MOTOR_OVERHEATING                     = 1 << 9
uint64 HARDWARE_LVPS_MALFUNCTION             = 1 << 10
uint64 HARDWARE_FAULT                        = 1 << 11
uint64 HARDWARE_OVERLOAD                     = 1 << 12
uint64 PHASE_CURRENT_MEASUREMENT_MALFUNCTION = 1 << 13

# Non-error flags are allocated at the top (from bit 63 downwards)
uint64 UAVCAN_NODE_UP                        = 1 << 56
uint64 CAN_DATA_LINK_UP                      = 1 << 57
uint64 USB_CONNECTED                         = 1 << 58
uint64 USB_POWER_SUPPLIED                    = 1 << 59
uint64 RCPWM_SIGNAL_DETECTED                 = 1 << 60
uint64 PHASE_CURRENT_AGC_HIGH_GAIN_SELECTED  = 1 << 61
uint64 VSI_MODULATING                        = 1 << 62
uint64 VSI_ENABLED                           = 1 << 63

# Status flags, see the constants above
uint64 flags
# Alert flags are allocated at the bottom (from bit 0 upwards)
uint32 ALERT_DC_UNDERVOLTAGE                       = 1 << 0
uint32 ALERT_DC_OVERVOLTAGE                        = 1 << 1
uint32 ALERT_DC_UNDERCURRENT                       = 1 << 2
uint32 ALERT_DC_OVERCURRENT                        = 1 << 3
uint32 ALERT_CPU_COLD                              = 1 << 4
uint32 ALERT_CPU_OVERHEATING                       = 1 << 5
uint32 ALERT_VSI_COLD                              = 1 << 6
uint32 ALERT_VSI_OVERHEATING                       = 1 << 7
uint32 ALERT_MOTOR_COLD                            = 1 << 8
uint32 ALERT_MOTOR_OVERHEATING                     = 1 << 9
uint32 ALERT_HARDWARE_LVPS_MALFUNCTION             = 1 << 10
uint32 ALERT_HARDWARE_FAULT                        = 1 << 11
uint32 ALERT_HARDWARE_OVERLOAD                     = 1 << 12
uint32 ALERT_PHASE_CURRENT_MEASUREMENT_MALFUNCTION = 1 << 13
uint32 alert_flags

uint32 STATUS_UAVCAN_NODE_UP                       = 1 << 24
uint32 STATUS_CAN_DATA_LINK_UP                     = 1 << 25
uint32 STATUS_USB_CONNECTED                        = 1 << 26
uint32 STATUS_USB_POWER_SUPPLIED                   = 1 << 27
uint32 STATUS_RCPWM_SIGNAL_DETECTED                = 1 << 28
uint32 STATUS_PHASE_CURRENT_AGC_HIGH_GAIN_SELECTED = 1 << 29
uint32 STATUS_VSI_MODULATING                       = 1 << 30
uint32 STATUS_VSI_ENABLED                          = 1 << 31
uint32 status_flags

Conclusion

If we were to break "code compatibility" it should be for the ability to add new features. I do not see enough value in mixing around with existing datatypes to justify doing so.

@kjetilkjeka
Copy link
Contributor Author

That seems overly strict. One can imagine a use case where a new void field would need to be introduced, e.g. if a non-void field became deprecated or split into several smaller fields. I suggest staying generic; perhaps we should move this conversation back to #11 where I have proposed a very minimalistic formulation.

What is the point of deprecating non-void fields? We cannot repopulate it in a sound way.

If every field can be removed you do not have much guarantee as a user of a data type either. We should not be able to remove features (anything) and still call something compatible.

@pavel-kirienko
Copy link
Member

I think it is important to ensure that our reasoning here is guided by the top-level design objectives and not by one's vision on how to implement the best diagnostic tools. ;)

It is crucial to ensure that we have all possible tools at our disposal when it comes to the ability to advance data type definitions while not breaking backward compatiblity with existing nodes, because updating fielded nodes is hard and expensive. The balance between ease of coding and ease of maintenance should be skewed towards the latter, which is why I argue that making code compatiblity mandatory is a step in a wrong direction.

What is the point of deprecating non-void fields? We cannot repopulate it in a sound way.
If every field can be removed you do not have much guarantee as a user of a data type either. We should not be able to remove features (anything) and still call something compatible.

This issue is supposed to be managed by the semantic compatiblity requirement. It should be possible to remove constants that ended up being unnecessary, or fields that turned out to be redundant. My example that you've provided in your earlier post here can be easily modified such that the existing wide integer field is split into two smaller fields with a void field in the middle - this is an example of a meaningful transition from a non-void field to a void field.

@kjetilkjeka
Copy link
Contributor Author

I think it is important to ensure that our reasoning here is guided by the top-level design objectives and not by one's vision on how to implement the best diagnostic tools. ;)

Please arrest me wherever I make this mistake.

It is crucial to ensure that we have all possible tools at our disposal when it comes to the ability to advance data type definitions while not breaking backward compatiblity with existing nodes, because updating fielded nodes is hard and expensive.

I agree in the essence but would phrase this differently. Our main objective should be to never risk breaking fielded nodes that are using only stabilized definitions. If I were going to put these definitions in a "fielded node" I would want a strong guarantee of stability. More than "some guy thought this would be fine for most applications". Being able to upgrade definitions is super-duper-mega-nice, but secondary to not breaking stuff. Before the ongoing protocol update there were not even a concept of updating without breaking compatibility, so I don't agree that it's essential to be in the "extreme liberal" corner when it comes to changes we allow to do to definitions.

Our main line of defense should be to only stabilize definitions after they are audited and tested thoroughly. We should not expect compatible updating to be a super flexible "save us from anything" kind of tool. But instead allow stabilizing definitions a bit earlier since we can accommodate for adding information at a later point.

The balance between ease of coding and ease of maintenance should be skewed towards the latter, which is why I argue that making code compatiblity mandatory is a step in a wrong direction.

In my point of view this is actually about "ease of coding" vs "ease of coding". Conceptually you will not be able to remove any stabilized features without risking to break how fielded nodes work. This means that not having code compatibility will not allow you to do more invasive changes to functionality. The changes you could potentially do are already (or at least should be) limited by "other types of compatibility". The only changes you will be able to do is changes to representation. Unrolling static arrays and such.

For the record: If ditching code compatibility can give us any concrete tools to not break fielded nodes I'm all for it. I just currently think that ditching code compatibility give us very little in return. And there are more powerful and robust ways to achieve what we actually wants.

This issue is supposed to be managed by the semantic compatiblity requirement. It should be possible to remove constants that ended up being unnecessary, or fields that turned out to be redundant. My example that you've provided in your earlier post here can be easily modified such that the existing wide integer field is split into two smaller fields with a void field in the middle - this is an example of a meaningful transition from a non-void field to a void field.

The point about stabilization is committing to something. Once something is stabilized we should absolutely be stuck with this commitment. You can never know if something is "unnecessary" for everyone in the whole wide world. Meaning, once something is stabilized it might be in use, and to avoid breaking fielded nodes we must not remove it.

To take your concrete example. If the void field you mentioned were to be reused, old nodes might receive a different status code than what was sent due to the "new field" interfering with the long version of the old field. Meaning that this concrete change you're describing, must be forbidden. If we want to make this possible, it's our responsibility to provide an initial definition that will not be possible to break fielded nodes by changing.


Defining data types in a smart way.

I think we already have the required tools to make your example possible. We just have to think things through from the start. Since we're able to populate void fields now (contrary to when the definition you're using as an example was made) the following definition should be used if it's important that the unused space can be re-purposed to anything.

uint1 alert_dc_undervoltage
uint1 alert_dc_overvoltage 
uint1 alert_dc_undercurrent
uint1 alter_dc_overcurrent
uint1 alert_cpu_cold
uint1 alert_cpu_overheating
uint1 alert_vsi_cold
uint1 alert_vsi_overheating
uint1 alert_motor_cold
uint1 alert_motor_overheating
uint1 alert_hardware_lvps_malfunction
uint1 alert_hardware_fault
uint1 alert_hardware_overload
uint1 alert_phase_current_measurement_malfunction

void42 # This field can be used to grow status upwards or error downwards, or for something else.

uint1 status_uavcan_node_up
uint1 status_can_data_link_up
uint1 status_usb_connected
uint1 status_usb_power_supplied
uint1 status_rcpwm_signal_detected
uint1 status_phase_current_agc_high_gain_selected
uint1 status_vsi_modulating
uint1 status_vsi_enabled

Having your cake and eating it too

The DSDL semantics are are not optimized towards writing "upgradable definitions", this makes sense as upgrading definitions was not a part of the original specification. Even though your last example is impossible to make fully backwards compatible with the current semantics, some small changes might make it achievable. If we want to retain the possibility of making as many changes as possible post stabilization we will need to look at other parts of the protocol to make this happen.

Note: The following features I'm about to present is not suggested for inclusion. It's only meant as an example of how features that encapsulate information can give more flexible ways of upgrading without breaking any type of compatibility. I created these examples in 3mins, and there probably exists much better types that would allow the same thing with more flexibility.

Typedefs

Let's introduce typesdefs. We can make it a rule that typedefs are considered private and can be changed with minor versions. The definition may then look like the following:

type Alert = uint14
type Status = uint8

Alert alert
void42 # This field can be used to grow status upwards or error downwards, or for something else.
Status status

Ad-hoc structs

What if we allowed ad-hoc structs that we don't expose/assume to much about (this is actually quite similar to typedef in expressive power). We will not remove any fields, but we're free to add fields/consts as long as the top level structure remains compatible.

struct alert {
    uint1 alert_dc_undervoltage
    uint1 alert_dc_overvoltage 
    uint1 alert_dc_undercurrent
    uint1 alter_dc_overcurrent
    uint1 alert_cpu_cold
    uint1 alert_cpu_overheating
    uint1 alert_vsi_cold
    uint1 alert_vsi_overheating
    uint1 alert_motor_cold
    uint1 alert_motor_overheating
    uint1 alert_hardware_lvps_malfunction
    uint1 alert_hardware_fault
    uint1 alert_hardware_overload
    uint1 alert_phase_current_measurement_malfunction
}

void42 # This field can be used to grow status upwards or error downwards, or for something else.

struct status {
    uint1 status_uavcan_node_up
    uint1 status_can_data_link_up
    uint1 status_usb_connected
    uint1 status_usb_power_supplied
    uint1 status_rcpwm_signal_detected
    uint1 status_phase_current_agc_high_gain_selected
    uint1 status_vsi_modulating
    uint1 status_vsi_enabled
}

Dynamic range uints

This feature is not very elegant or smart. It's also way to obscure to be considered for addition. I'm aware of all this. Remember: It is only included as an example

Let's introduce the dyn_uint(X-Y) type. It's a dynamic type that contains information about how long a bitfield is. It's serialized equivalently to the following type

uint1[<=(Y-X)]
uint1[X]

Our original definition may then might look like this:

dyn_uint(13-28) NO_ERROR = 0 # will be serialized as 4 + 13 bits as small enough to fit inside 13 bits

dyn_uint(13-28) alert # serializes to maximum 32 bits
dyn_void(0-27) # can be replaced with other type of bit fields can be added here.
dyn_uint(13-28) status # serializes to maximum 32 bits

This will allow both for checking if alert == 0, growing both alert and status fields. And retaining some bits between alert and status that can be used for other things.

Breaking the law of excluded third

There exists a possibility of doing the following:

  • Not allowing liberal changes at this time (only allowing simple addition of information without changing types etc)
  • Not guaranteeing code compatibility at this time.
  • Not making any changes to DSDL format at this time.

We would then get some experience with stabilization before deciding if we want the liberal or conservative approach to compatibility. The drawback is that if we decide to introduce more advanced DSDL concepts that encapsulate information we would not be able to use them for the types we had already stabilized.

@pavel-kirienko
Copy link
Member

Please arrest me wherever I make this mistake.

Will do. I apologize for the assumption; the world seems different from where I sit.

First, let me introduce two simple empirical counter-arguments to the following two propositions:

Meaning, once something is stabilized it might be in use, and to avoid breaking fielded nodes we must not remove it.

Consider the field sub_mode in uavcan.protocol.NodeStatus. It is a non-void field that is mandated to be set to zero when emitting and ignored when receiving, which makes it a sensible candidate for replacement with a void field.

This means that not having code compatibility will not allow you to do more invasive changes to functionality.

In the ticket #13 there is a suggestion to use compound fixed-point arithmetic scalars consisting of two adjacent integer types until there is proper support at the language level:

uint16 integer_bits
uint8 fractional_bits

Which one could foresee replaced with the below alternative once the required support is in place:

ufix16_8 number

Now, let's put that aside. Continuing the conversation at the object level will not move us any further.


There is a spectrum of possible approaches to the problem of data type maintenance, ranging from the most conservative, where data types are frozen forever once released, to the most liberal, where things are never set in stone and always changing. Let me recap the options:

  • The Ultra-conservative Extreme: Data types are released once and frozen forever. This is what we have now. There is no sensible way of modifying an existing definition save for changing comments, reformatting, and adding/removing constants.
  • The Conservative Extreme: See above.
  • The Liberal Extreme: Any changes are possible as long as all definitions under the same major version are semantically compatible and bit compatible. This approach is described in the current draft of the specification.

The Conservative Extreme shields us from mistakes introduced after a major version is released, assuming that we do the job of stabilizing it once, and we do it right.

The Liberal Extreme tolerates some mistakes introduced while stabilizing major versions by allowing us to make drastic changes to the following newer minor versions (although they still have to be semantically compatible and bit compatible). The catch here is that while bit-compatibility has a strict formal definition and therefore can be ensured by a computer, semantic compatibility does not, which leaves the question "are these changes safe to introduce" dependent on human judgment.

The catch described last is seemingly the main perceived deal-breaker for the case of the Liberal Extreme:

If I were going to put these definitions in a "fielded node" I would want a strong guarantee of stability. More than "some guy thought this would be fine for most applications".

The hypothetical guy thinking about what is good and what is bad for most applications is the driving force behind every complex technical standard out there. It works for USB 3.0, IPv6, and ISO C++; it works for UAVCAN, and there doesn't seem to be a convincing reason it won't work at a smaller scope of individual data types.

Disregarding even that: one can see that the freedoms of data type evolution provided by the Conservative Extreme are a subset of those offered by the Liberal Extreme, meaning that one could establish a workflow permitting only the Conservative option, leaving the Liberal option as a very last resort.

I would like the specification to be very permissive here. I would like it to put as few constraints as possible, letting data type designers work around the dangers of the Liberal approach on a per-project base, as necessary.

@pavel-kirienko
Copy link
Member

I am closing this as the discussion seems to have migrated to the forum completely:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants