Change Shape.cast(range(1)) to unsigned(0)

Summary

Change Shape.cast(range(1)) to return unsigned(0) instead of unsigned(1).

Motivation

Currently, Shape.cast(range(1)) returns unsigned(1). This is inconsistent with the expectation that casting range to a shape will return the minimal shape capable of representing all elements of that range, which is clearly unsigned(0).

This behavior is an accidental result of using bits_for internally to determine required width for both endpoints, which returns 1 for an input of 0.

The behavior introduces edge cases in unexpected places. For example, one may expect that Memory(depth=depth).read_port().addr.width == ceil_log2(depth). This is currently false for depth of 1 for no particularly good reason.

Guide-level explanation

Shape.cast(range(1)) is changed to return unsigned(0). The same applies to any other range whose only element is 0, like Shape.cast(range(0, 2, 2)).

Arguably, this change requires a negative amount of exlanation, since it removes an edge case and brings the behavior into alignment with the language reference.

Reference-level explanation

See above.

Drawbacks

This is a minor backwards compatibility hazard.

Rationale and alternatives

The change itself is simple enough that it cannot really be done any other way.

It would be possible to introduce a compatibility warning to the 0.4 branch. However, a range() signal having a shape that's slightly too large is unlikely to cause problems in the first place, so the warning would cause a mass of false positives without a nice way to turn it off.

Prior art

None.

Unresolved questions

None.

Future possibilities

The bits_for function, which led to this issue in the first place, could be deprecated and removed from public interface to avoid introducing similar problems to external code. Otherwise, it should at the very least be documented and loudly call out this special case.